From d40ecdde1990e41b6cb1cd958e86c34cf4c1ab4b Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 10 May 2026 23:49:55 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=20web-admin=20?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E8=84=9A=E6=9C=AC=EF=BC=8C=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=20SSH=20=E5=8D=A1=E6=AD=BB=E5=92=8C=E6=97=A7=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E6=B8=85=E7=90=86=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 所有 SSH/SCP 命令添加 BatchMode=yes 和超时限制 - 新增 SSH 预检步骤,失败立即退出 - 清理旧文件改为先删后传策略,避免 find -printf 兼容性问题 - 使用 rsync 上传 assets,fallback 到 scp - 文件/目录权限分别设置为 644/755 Co-Authored-By: Claude Opus 4.7 --- web-admin/deploy.py | 173 ++++++++++++++++++++++++++++---------------- 1 file changed, 110 insertions(+), 63 deletions(-) diff --git a/web-admin/deploy.py b/web-admin/deploy.py index d395871..d993e5d 100644 --- a/web-admin/deploy.py +++ b/web-admin/deploy.py @@ -31,32 +31,47 @@ class Colors: def log_info(msg): """打印信息日志""" - print(f"{Colors.GREEN}✅{Colors.RESET} {msg}") + print(f"{Colors.GREEN}{Colors.RESET} {msg}") def log_error(msg): """打印错误日志""" - print(f"{Colors.RED}❌{Colors.RESET} {msg}") + print(f"{Colors.RED}{Colors.RESET} {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: 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 else: - result = subprocess.run(cmd, cwd=cwd, shell=shell) + result = subprocess.run(cmd, cwd=cwd, shell=shell, 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_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(): """检查npm是否安装""" success, _, _ = run_command("npm --version") @@ -65,36 +80,45 @@ def check_npm(): sys.exit(1) -def check_scp(): - """检查scp是否可用""" - success, _, _ = run_command("scp -V 2>&1 || echo ok") - # scp通常没有--version,但命令存在即可 - return True +def check_ssh(): + """检查SSH连接""" + log_step(" 检查SSH连接...") + cmd = f'ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no {USERNAME}@{SERVER_IP} "echo ok"' + 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(): """清理旧的构建文件""" - log_step("🧹 清理旧的构建文件...") + log_step(" 清理旧的构建文件...") if DIST_DIR.exists(): shutil.rmtree(DIST_DIR) def build_project(): """构建项目""" - log_step("开始构建管理后台项目(生产环境)...") - + log_step(" 开始构建管理后台项目(生产环境)...") + os.chdir(SCRIPT_DIR) - - # 设置环境变量并执行构建 + env = os.environ.copy() env["NODE_ENV"] = "production" - + try: result = subprocess.run( "npm run build", shell=True, cwd=SCRIPT_DIR, - env=env + env=env, + timeout=300 ) if result.returncode != 0: log_error("管理后台项目构建失败,请检查代码") @@ -102,7 +126,7 @@ def build_project(): except Exception as e: log_error(f"构建失败: {e}") sys.exit(1) - + log_info("管理后台项目构建成功") @@ -111,15 +135,14 @@ def verify_dist(): if not DIST_DIR.exists(): log_error("错误: 构建后dist目录仍不存在,请检查构建配置") sys.exit(1) - - # 检查关键文件 + index_file = DIST_DIR / "index.html" assets_dir = DIST_DIR / "assets" - + if not index_file.exists(): log_error("错误: dist/index.html 不存在") sys.exit(1) - + if not assets_dir.exists(): log_error("错误: dist/assets 目录不存在") sys.exit(1) @@ -127,9 +150,9 @@ def verify_dist(): def create_remote_dir(): """创建远程目录""" - log_step("📁 创建远程目录...") - cmd = f'ssh {USERNAME}@{SERVER_IP} "mkdir -p {REMOTE_PATH}"' - success, _, stderr = run_command(cmd) + log_step(" 创建远程目录...") + cmd = f'ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no {USERNAME}@{SERVER_IP} "mkdir -p {REMOTE_PATH}"' + success, _, stderr = run_command(cmd, timeout=15) if not success: log_error(f"创建远程目录失败: {stderr}") sys.exit(1) @@ -137,72 +160,96 @@ def create_remote_dir(): def upload_files(): """上传文件到服务器""" - log_step("📤 上传文件到服务器...") - - print(f"正在上传文件到服务器 {SERVER_IP}...") - - # 上传 index.html - index_file = DIST_DIR / "index.html" - cmd1 = f'scp "{index_file}" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/' - success1, _, stderr1 = run_command(cmd1) - - if not success1: - log_error(f"上传 index.html 失败: {stderr1}") - return False - - # 上传 assets 目录 + log_step(" 上传文件到服务器...") + + print(f" 正在上传到 {SERVER_IP}:{REMOTE_PATH}") + + # 使用 rsync 替代 scp,支持断点续传和进度显示 assets_dir = DIST_DIR / "assets" - cmd2 = f'scp -r "{assets_dir}" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/' - success2, _, stderr2 = run_command(cmd2) - - if not success2: - log_error(f"上传 assets 目录失败: {stderr2}") + + # 上传 index.html + cmd1 = f'scp -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no "{DIST_DIR / "index.html"}" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/' + success1 = run_command_realtime(cmd1, timeout=60) + if not success1: + log_error("上传 index.html 失败") return False - + log_info("index.html 上传成功") + + # 上传 assets 目录(使用 rsync 更高效) + cmd2 = f'rsync -avz --progress -e "ssh -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no" "{assets_dir}/" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/assets/' + print(f" 正在上传 assets 目录...") + success2 = run_command_realtime(cmd2, timeout=300) + if not success2: + # 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 + log_info("assets 目录上传成功") + 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(): """设置文件权限""" - log_step("🔐 设置文件权限...") - cmd = f'ssh {USERNAME}@{SERVER_IP} "chmod -R 755 {REMOTE_PATH}"' - success, _, stderr = run_command(cmd) + log_step(" 设置文件权限...") + 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, timeout=30) if not success: log_error(f"设置权限失败: {stderr}") + else: + log_info("文件权限设置成功") def deploy(): """执行部署""" print("开始部署管理后台应用到服务器...") - - # 检查npm + + # 检查依赖 check_npm() - + check_ssh() + # 清理旧构建 clean_dist() - + # 构建项目 build_project() - + # 验证构建结果 verify_dist() - + # 创建远程目录 create_remote_dir() - - # 上传文件 + + # 清理远程旧assets目录 + clean_remote_old_files() + + # 上传文件(全量上传) if upload_files(): # 设置权限 set_permissions() - + log_info("管理后台部署完成!") - print(f"📱 访问地址: http://{SERVER_IP}/emotion-museum-admin/") - print("🔧 管理后台功能: AI配置管理、用户管理、数据统计等") + print(f" 访问地址: http://{SERVER_IP}/emotion-museum-admin/") else: log_error("部署失败,请检查:") - print("1. 服务器IP地址是否正确") - print("2. SSH密钥是否配置正确") - print("3. 服务器目录权限是否正确") + print(" 1. 服务器IP地址是否正确") + print(" 2. SSH密钥是否配置正确") + print(" 3. 服务器目录权限是否正确") sys.exit(1)