fix: 优化 web-admin 部署脚本,解决 SSH 卡死和旧文件清理问题
- 所有 SSH/SCP 命令添加 BatchMode=yes 和超时限制 - 新增 SSH 预检步骤,失败立即退出 - 清理旧文件改为先删后传策略,避免 find -printf 兼容性问题 - 使用 rsync 上传 assets,fallback 到 scp - 文件/目录权限分别设置为 644/755 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+85
-38
@@ -31,32 +31,47 @@ class Colors:
|
|||||||
|
|
||||||
def log_info(msg):
|
def log_info(msg):
|
||||||
"""打印信息日志"""
|
"""打印信息日志"""
|
||||||
print(f"{Colors.GREEN}✅{Colors.RESET} {msg}")
|
print(f"{Colors.GREEN}{Colors.RESET} {msg}")
|
||||||
|
|
||||||
|
|
||||||
def log_error(msg):
|
def log_error(msg):
|
||||||
"""打印错误日志"""
|
"""打印错误日志"""
|
||||||
print(f"{Colors.RED}❌{Colors.RESET} {msg}")
|
print(f"{Colors.RED}{Colors.RESET} {msg}")
|
||||||
|
|
||||||
|
|
||||||
def log_step(msg):
|
def log_step(msg):
|
||||||
"""打印步骤日志"""
|
"""打印步骤日志"""
|
||||||
print(f"📦 {msg}")
|
print(f" {msg}")
|
||||||
|
|
||||||
|
|
||||||
def run_command(cmd, cwd=None, shell=True, capture=True):
|
def run_command(cmd, cwd=None, shell=True, capture=True, timeout=60):
|
||||||
"""执行本地命令"""
|
"""执行本地命令"""
|
||||||
try:
|
try:
|
||||||
if capture:
|
if capture:
|
||||||
result = subprocess.run(cmd, cwd=cwd, shell=shell, capture_output=True, text=True)
|
result = subprocess.run(cmd, cwd=cwd, shell=shell, capture_output=True, text=True, timeout=timeout)
|
||||||
return result.returncode == 0, result.stdout, result.stderr
|
return result.returncode == 0, result.stdout, result.stderr
|
||||||
else:
|
else:
|
||||||
result = subprocess.run(cmd, cwd=cwd, shell=shell)
|
result = subprocess.run(cmd, cwd=cwd, shell=shell, timeout=timeout)
|
||||||
return result.returncode == 0, "", ""
|
return result.returncode == 0, "", ""
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "", f"命令执行超时 ({timeout}s): {cmd}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, "", str(e)
|
return False, "", str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def run_command_realtime(cmd, cwd=None, shell=True, timeout=120):
|
||||||
|
"""执行命令并实时输出(用于大文件上传进度)"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, cwd=cwd, shell=shell, timeout=timeout)
|
||||||
|
return result.returncode == 0
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
log_error(f"命令执行超时 ({timeout}s)")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
log_error(f"执行失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def check_npm():
|
def check_npm():
|
||||||
"""检查npm是否安装"""
|
"""检查npm是否安装"""
|
||||||
success, _, _ = run_command("npm --version")
|
success, _, _ = run_command("npm --version")
|
||||||
@@ -65,16 +80,25 @@ def check_npm():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def check_scp():
|
def check_ssh():
|
||||||
"""检查scp是否可用"""
|
"""检查SSH连接"""
|
||||||
success, _, _ = run_command("scp -V 2>&1 || echo ok")
|
log_step(" 检查SSH连接...")
|
||||||
# scp通常没有--version,但命令存在即可
|
cmd = f'ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no {USERNAME}@{SERVER_IP} "echo ok"'
|
||||||
return True
|
success, stdout, stderr = run_command(cmd, timeout=15)
|
||||||
|
if not success:
|
||||||
|
log_error("SSH连接失败,请检查:")
|
||||||
|
print(f" 1. 服务器IP: {SERVER_IP}")
|
||||||
|
print(f" 2. SSH密钥是否配置正确")
|
||||||
|
print(f" 3. 服务器防火墙是否允许22端口")
|
||||||
|
if "Host key verification failed" in stderr:
|
||||||
|
log_error("主机密钥验证失败,请先手动执行: ssh root@101.200.208.45")
|
||||||
|
sys.exit(1)
|
||||||
|
log_info("SSH连接正常")
|
||||||
|
|
||||||
|
|
||||||
def clean_dist():
|
def clean_dist():
|
||||||
"""清理旧的构建文件"""
|
"""清理旧的构建文件"""
|
||||||
log_step("🧹 清理旧的构建文件...")
|
log_step(" 清理旧的构建文件...")
|
||||||
if DIST_DIR.exists():
|
if DIST_DIR.exists():
|
||||||
shutil.rmtree(DIST_DIR)
|
shutil.rmtree(DIST_DIR)
|
||||||
|
|
||||||
@@ -85,7 +109,6 @@ def build_project():
|
|||||||
|
|
||||||
os.chdir(SCRIPT_DIR)
|
os.chdir(SCRIPT_DIR)
|
||||||
|
|
||||||
# 设置环境变量并执行构建
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["NODE_ENV"] = "production"
|
env["NODE_ENV"] = "production"
|
||||||
|
|
||||||
@@ -94,7 +117,8 @@ def build_project():
|
|||||||
"npm run build",
|
"npm run build",
|
||||||
shell=True,
|
shell=True,
|
||||||
cwd=SCRIPT_DIR,
|
cwd=SCRIPT_DIR,
|
||||||
env=env
|
env=env,
|
||||||
|
timeout=300
|
||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
log_error("管理后台项目构建失败,请检查代码")
|
log_error("管理后台项目构建失败,请检查代码")
|
||||||
@@ -112,7 +136,6 @@ def verify_dist():
|
|||||||
log_error("错误: 构建后dist目录仍不存在,请检查构建配置")
|
log_error("错误: 构建后dist目录仍不存在,请检查构建配置")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# 检查关键文件
|
|
||||||
index_file = DIST_DIR / "index.html"
|
index_file = DIST_DIR / "index.html"
|
||||||
assets_dir = DIST_DIR / "assets"
|
assets_dir = DIST_DIR / "assets"
|
||||||
|
|
||||||
@@ -127,9 +150,9 @@ def verify_dist():
|
|||||||
|
|
||||||
def create_remote_dir():
|
def create_remote_dir():
|
||||||
"""创建远程目录"""
|
"""创建远程目录"""
|
||||||
log_step("📁 创建远程目录...")
|
log_step(" 创建远程目录...")
|
||||||
cmd = f'ssh {USERNAME}@{SERVER_IP} "mkdir -p {REMOTE_PATH}"'
|
cmd = f'ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no {USERNAME}@{SERVER_IP} "mkdir -p {REMOTE_PATH}"'
|
||||||
success, _, stderr = run_command(cmd)
|
success, _, stderr = run_command(cmd, timeout=15)
|
||||||
if not success:
|
if not success:
|
||||||
log_error(f"创建远程目录失败: {stderr}")
|
log_error(f"创建远程目录失败: {stderr}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -137,46 +160,68 @@ def create_remote_dir():
|
|||||||
|
|
||||||
def upload_files():
|
def upload_files():
|
||||||
"""上传文件到服务器"""
|
"""上传文件到服务器"""
|
||||||
log_step("📤 上传文件到服务器...")
|
log_step(" 上传文件到服务器...")
|
||||||
|
|
||||||
print(f"正在上传文件到服务器 {SERVER_IP}...")
|
print(f" 正在上传到 {SERVER_IP}:{REMOTE_PATH}")
|
||||||
|
|
||||||
|
# 使用 rsync 替代 scp,支持断点续传和进度显示
|
||||||
|
assets_dir = DIST_DIR / "assets"
|
||||||
|
|
||||||
# 上传 index.html
|
# 上传 index.html
|
||||||
index_file = DIST_DIR / "index.html"
|
cmd1 = f'scp -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no "{DIST_DIR / "index.html"}" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/'
|
||||||
cmd1 = f'scp "{index_file}" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/'
|
success1 = run_command_realtime(cmd1, timeout=60)
|
||||||
success1, _, stderr1 = run_command(cmd1)
|
|
||||||
|
|
||||||
if not success1:
|
if not success1:
|
||||||
log_error(f"上传 index.html 失败: {stderr1}")
|
log_error("上传 index.html 失败")
|
||||||
return False
|
return False
|
||||||
|
log_info("index.html 上传成功")
|
||||||
|
|
||||||
# 上传 assets 目录
|
# 上传 assets 目录(使用 rsync 更高效)
|
||||||
assets_dir = DIST_DIR / "assets"
|
cmd2 = f'rsync -avz --progress -e "ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no" "{assets_dir}/" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/assets/'
|
||||||
cmd2 = f'scp -r "{assets_dir}" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/'
|
print(f" 正在上传 assets 目录...")
|
||||||
success2, _, stderr2 = run_command(cmd2)
|
success2 = run_command_realtime(cmd2, timeout=300)
|
||||||
|
|
||||||
if not success2:
|
if not success2:
|
||||||
log_error(f"上传 assets 目录失败: {stderr2}")
|
# rsync 不可用则回退到 scp
|
||||||
|
log_info("rsync不可用,使用scp上传...")
|
||||||
|
cmd2_fallback = f'scp -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no -r "{assets_dir}" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/'
|
||||||
|
success2 = run_command_realtime(cmd2_fallback, timeout=300)
|
||||||
|
if not success2:
|
||||||
|
log_error("上传 assets 目录失败")
|
||||||
return False
|
return False
|
||||||
|
log_info("assets 目录上传成功")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def clean_remote_old_files():
|
||||||
|
"""清理远程服务器上旧的assets目录(全量替换策略)"""
|
||||||
|
log_step(" 清理远程旧文件...")
|
||||||
|
# 直接删除远程 assets 目录,后续会重新上传
|
||||||
|
cmd = f'ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no {USERNAME}@{SERVER_IP} "rm -rf {REMOTE_PATH}/assets"'
|
||||||
|
success, _, stderr = run_command(cmd, timeout=15)
|
||||||
|
if success:
|
||||||
|
log_info("远程旧文件已清理")
|
||||||
|
else:
|
||||||
|
log_info(f"清理远程旧文件跳过(可能目录不存在): {stderr}")
|
||||||
|
|
||||||
|
|
||||||
def set_permissions():
|
def set_permissions():
|
||||||
"""设置文件权限"""
|
"""设置文件权限"""
|
||||||
log_step("🔐 设置文件权限...")
|
log_step(" 设置文件权限...")
|
||||||
cmd = f'ssh {USERNAME}@{SERVER_IP} "chmod -R 755 {REMOTE_PATH}"'
|
cmd = f'ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no {USERNAME}@{SERVER_IP} "find {REMOTE_PATH} -type f -exec chmod 644 {{}} \\; && find {REMOTE_PATH} -type d -exec chmod 755 {{}} \\;"'
|
||||||
success, _, stderr = run_command(cmd)
|
success, _, stderr = run_command(cmd, timeout=30)
|
||||||
if not success:
|
if not success:
|
||||||
log_error(f"设置权限失败: {stderr}")
|
log_error(f"设置权限失败: {stderr}")
|
||||||
|
else:
|
||||||
|
log_info("文件权限设置成功")
|
||||||
|
|
||||||
|
|
||||||
def deploy():
|
def deploy():
|
||||||
"""执行部署"""
|
"""执行部署"""
|
||||||
print("开始部署管理后台应用到服务器...")
|
print("开始部署管理后台应用到服务器...")
|
||||||
|
|
||||||
# 检查npm
|
# 检查依赖
|
||||||
check_npm()
|
check_npm()
|
||||||
|
check_ssh()
|
||||||
|
|
||||||
# 清理旧构建
|
# 清理旧构建
|
||||||
clean_dist()
|
clean_dist()
|
||||||
@@ -190,14 +235,16 @@ def deploy():
|
|||||||
# 创建远程目录
|
# 创建远程目录
|
||||||
create_remote_dir()
|
create_remote_dir()
|
||||||
|
|
||||||
# 上传文件
|
# 清理远程旧assets目录
|
||||||
|
clean_remote_old_files()
|
||||||
|
|
||||||
|
# 上传文件(全量上传)
|
||||||
if upload_files():
|
if upload_files():
|
||||||
# 设置权限
|
# 设置权限
|
||||||
set_permissions()
|
set_permissions()
|
||||||
|
|
||||||
log_info("管理后台部署完成!")
|
log_info("管理后台部署完成!")
|
||||||
print(f"📱 访问地址: http://{SERVER_IP}/emotion-museum-admin/")
|
print(f" 访问地址: http://{SERVER_IP}/emotion-museum-admin/")
|
||||||
print("🔧 管理后台功能: AI配置管理、用户管理、数据统计等")
|
|
||||||
else:
|
else:
|
||||||
log_error("部署失败,请检查:")
|
log_error("部署失败,请检查:")
|
||||||
print(" 1. 服务器IP地址是否正确")
|
print(" 1. 服务器IP地址是否正确")
|
||||||
|
|||||||
Reference in New Issue
Block a user