import sys import os import requests import subprocess import paramiko from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTabWidget, QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox, QFileDialog, QGroupBox, QComboBox, QDialog, QFormLayout, QDialogButtonBox, QSplitter, QFrame, QListWidget, QListWidgetItem, QInputDialog, QGridLayout, QTextEdit, QCheckBox, QProgressBar) from PyQt5.QtCore import Qt, QThread, pyqtSignal # Configuration DEFAULT_API_URL = " https://backend.aidg168.uk/api/v1" DEFAULT_ADMIN_TOKEN = "admin-secret-token" import shutil import zipfile from datetime import datetime class ApiClient: def __init__(self, base_url, token): self.base_url = base_url.rstrip("/") self.token = token def get_headers(self): return {"x-admin-token": self.token} def list_groups(self): try: resp = requests.get(f"{self.base_url}/admin/groups", headers=self.get_headers()) resp.raise_for_status() return resp.json() except Exception as e: raise Exception(f"获取组列表失败: {str(e)}") def create_group(self, name, comment): try: payload = {"name": name, "comment": comment} resp = requests.post(f"{self.base_url}/admin/groups", json=payload, headers=self.get_headers()) resp.raise_for_status() return resp.json() except Exception as e: raise Exception(f"创建组失败: {str(e)}") def upload_version(self, file_path): try: filename = os.path.basename(file_path) files = {'file': (filename, open(file_path, 'rb'))} data = {'token': self.token} resp = requests.post(f"{self.base_url}/admin/upload_version", files=files, data=data) resp.raise_for_status() return resp.json().get("filename") except Exception as e: raise Exception(f"上传失败: {str(e)}") def list_archives(self): try: resp = requests.get(f"{self.base_url}/admin/archives", headers=self.get_headers()) resp.raise_for_status() return resp.json() except Exception as e: raise Exception(f"获取归档列表失败: {str(e)}") def update_group_version(self, group_id, filename): try: payload = {"current_version_file": filename} resp = requests.put(f"{self.base_url}/admin/groups/{group_id}", json=payload, headers=self.get_headers()) resp.raise_for_status() return resp.json() except Exception as e: raise Exception(f"更新组版本失败: {str(e)}") def update_group_comment(self, group_id, comment): try: payload = {"comment": comment} resp = requests.put(f"{self.base_url}/admin/groups/{group_id}", json=payload, headers=self.get_headers()) resp.raise_for_status() return resp.json() except Exception as e: raise Exception(f"更新组备注失败: {str(e)}") def list_users(self): try: resp = requests.get(f"{self.base_url}/admin/users", headers=self.get_headers()) resp.raise_for_status() return resp.json() except Exception as e: raise Exception(f"获取用户列表失败: {str(e)}") def update_user_group(self, user_id, group_id): try: # group_id is query param in backend resp = requests.put(f"{self.base_url}/admin/users/{user_id}/group?group_id={group_id}", headers=self.get_headers()) resp.raise_for_status() return resp.json() except Exception as e: raise Exception(f"更新用户组失败: {str(e)}") def update_user_permissions(self, user_id, permissions): try: data = {"permissions": permissions} resp = requests.put(f"{self.base_url}/admin/users/{user_id}/permissions", data=data, headers=self.get_headers()) resp.raise_for_status() return resp.json() except Exception as e: raise Exception(f"更新用户权限失败: {str(e)}") class CreateGroupDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("创建新组") self.layout = QFormLayout(self) self.name_edit = QLineEdit() self.comment_edit = QLineEdit() self.layout.addRow("组名称:", self.name_edit) self.layout.addRow("备注:", self.comment_edit) self.buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttons.accepted.connect(self.accept) self.buttons.rejected.connect(self.reject) self.layout.addRow(self.buttons) def get_data(self): return self.name_edit.text(), self.comment_edit.text() class AdminWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("DesignerCEP 管理工具") self.resize(1000, 700) self.api_client = None self.groups_data = [] # Cache groups self.users_data = [] # Cache users # Main Layout central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 1. Connection Bar conn_group = QGroupBox("服务器连接") conn_layout = QHBoxLayout() self.url_edit = QLineEdit(DEFAULT_API_URL) self.token_edit = QLineEdit(DEFAULT_ADMIN_TOKEN) self.token_edit.setEchoMode(QLineEdit.Password) self.connect_btn = QPushButton("连接 / 刷新") self.connect_btn.clicked.connect(self.init_connection) conn_layout.addWidget(QLabel("API 地址:")) conn_layout.addWidget(self.url_edit, 2) conn_layout.addWidget(QLabel("Token:")) conn_layout.addWidget(self.token_edit, 1) conn_layout.addWidget(self.connect_btn) conn_group.setLayout(conn_layout) main_layout.addWidget(conn_group) # 2. Main Tab Widget self.tabs = QTabWidget() self.tabs.setEnabled(False) main_layout.addWidget(self.tabs) # Tab 1: Group & User Management (Unified View) self.manage_tab = QWidget() self.setup_manage_tab() self.tabs.addTab(self.manage_tab, "组与用户管理") # Tab 2: Release & Upload self.release_tab = QWidget() self.setup_release_tab() self.tabs.addTab(self.release_tab, "发布与上传") # Tab 3: Auto Deploy self.deploy_tab = QWidget() self.setup_deploy_tab() self.tabs.addTab(self.deploy_tab, "自动化部署") # Initial Auto-Connect self.init_connection() def setup_manage_tab(self): layout = QHBoxLayout(self.manage_tab) # Left Panel: Group List left_panel = QWidget() left_layout = QVBoxLayout(left_panel) left_layout.addWidget(QLabel("用户组列表")) self.group_list_widget = QListWidget() self.group_list_widget.currentItemChanged.connect(self.on_group_selected) left_layout.addWidget(self.group_list_widget) self.add_group_btn = QPushButton("新建组") self.add_group_btn.clicked.connect(self.create_group) left_layout.addWidget(self.add_group_btn) layout.addWidget(left_panel, 1) # Right Panel: Group Details & Users right_panel = QWidget() right_layout = QVBoxLayout(right_panel) # Group Details Header self.group_info_group = QGroupBox("组信息") info_layout = QGridLayout() self.lbl_group_name = QLabel("-") self.lbl_group_version = QLabel("-") self.lbl_group_comment = QLabel("-") self.btn_edit_comment = QPushButton("修改备注") self.btn_edit_comment.clicked.connect(self.edit_group_comment) info_layout.addWidget(QLabel("名称:"), 0, 0) info_layout.addWidget(self.lbl_group_name, 0, 1) info_layout.addWidget(QLabel("当前版本:"), 1, 0) info_layout.addWidget(self.lbl_group_version, 1, 1) info_layout.addWidget(QLabel("备注:"), 2, 0) info_layout.addWidget(self.lbl_group_comment, 2, 1) info_layout.addWidget(self.btn_edit_comment, 2, 2) self.group_info_group.setLayout(info_layout) right_layout.addWidget(self.group_info_group) # User List right_layout.addWidget(QLabel("组内用户")) self.user_table = QTableWidget() self.user_table.setColumnCount(4) self.user_table.setHorizontalHeaderLabels(["ID", "用户名", "权限", "操作"]) self.user_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) right_layout.addWidget(self.user_table) # Batch Actions action_layout = QHBoxLayout() self.btn_move_user = QPushButton("移动用户到其他组...") self.btn_move_user.clicked.connect(self.move_selected_user) self.btn_change_perm = QPushButton("修改用户权限...") self.btn_change_perm.clicked.connect(self.change_user_perm) action_layout.addWidget(self.btn_move_user) action_layout.addWidget(self.btn_change_perm) action_layout.addStretch() right_layout.addLayout(action_layout) layout.addWidget(right_panel, 3) def setup_release_tab(self): layout = QVBoxLayout(self.release_tab) # Section 1: Upload upload_group = QGroupBox("1. 上传新版本 (ZIP)") upload_layout = QHBoxLayout() self.file_path_edit = QLineEdit() self.file_path_edit.setReadOnly(True) self.browse_btn = QPushButton("浏览...") self.browse_btn.clicked.connect(self.browse_file) self.upload_btn = QPushButton("上传") self.upload_btn.clicked.connect(self.upload_file) upload_layout.addWidget(self.file_path_edit) upload_layout.addWidget(self.browse_btn) upload_layout.addWidget(self.upload_btn) upload_group.setLayout(upload_layout) layout.addWidget(upload_group) # Section 2: Archives List (New) archives_group = QGroupBox("2. 历史版本列表") archives_layout = QVBoxLayout() self.archives_list_widget = QListWidget() self.archives_list_widget.currentItemChanged.connect(self.on_archive_selected) archives_layout.addWidget(self.archives_list_widget) archives_group.setLayout(archives_layout) layout.addWidget(archives_group) # Section 3: Assign assign_group = QGroupBox("3. 分配版本给用户组") assign_layout = QFormLayout() self.uploaded_filename_label = QLabel("无") self.uploaded_filename_label.setStyleSheet("font-weight: bold; color: blue;") self.target_group_combo = QComboBox() self.assign_btn = QPushButton("更新组版本") self.assign_btn.clicked.connect(self.assign_version) assign_layout.addRow("选中版本:", self.uploaded_filename_label) assign_layout.addRow("目标组:", self.target_group_combo) assign_layout.addRow("", self.assign_btn) assign_group.setLayout(assign_layout) layout.addWidget(assign_group) #layout.addStretch() # remove stretch to let list expand def setup_deploy_tab(self): layout = QVBoxLayout(self.deploy_tab) # Section 1: 服务器配置 server_group = QGroupBox("服务器配置") server_layout = QGridLayout() server_layout.addWidget(QLabel("服务器地址:"), 0, 0) self.deploy_host = QLineEdit("your-server.com") server_layout.addWidget(self.deploy_host, 0, 1) server_layout.addWidget(QLabel("SSH 端口:"), 0, 2) self.deploy_port = QLineEdit("22") server_layout.addWidget(self.deploy_port, 0, 3) server_layout.addWidget(QLabel("用户名:"), 1, 0) self.deploy_user = QLineEdit("root") server_layout.addWidget(self.deploy_user, 1, 1) server_layout.addWidget(QLabel("密码:"), 1, 2) self.deploy_password = QLineEdit() self.deploy_password.setEchoMode(QLineEdit.Password) server_layout.addWidget(self.deploy_password, 1, 3) server_layout.addWidget(QLabel("服务器路径:"), 2, 0) self.deploy_remote_path = QLineEdit("/var/www/DesignerCEP/Server/static") server_layout.addWidget(self.deploy_remote_path, 2, 1, 1, 3) btn_test_conn = QPushButton("测试连接") btn_test_conn.clicked.connect(self.test_ssh_connection) server_layout.addWidget(btn_test_conn, 3, 0) btn_save_config = QPushButton("保存配置") btn_save_config.clicked.connect(self.save_deploy_config) server_layout.addWidget(btn_save_config, 3, 1) btn_load_config = QPushButton("加载配置") btn_load_config.clicked.connect(self.load_deploy_config) server_layout.addWidget(btn_load_config, 3, 2) server_group.setLayout(server_layout) layout.addWidget(server_group) # Section 2: 本地构建配置 build_group = QGroupBox("本地构建配置") build_layout = QGridLayout() build_layout.addWidget(QLabel("项目根目录:"), 0, 0) self.deploy_project_root = QLineEdit(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) build_layout.addWidget(self.deploy_project_root, 0, 1) btn_browse_project = QPushButton("浏览...") btn_browse_project.clicked.connect(self.browse_project_root) build_layout.addWidget(btn_browse_project, 0, 2) build_layout.addWidget(QLabel("版本号:"), 1, 0) self.deploy_version = QLineEdit("1.0.0") build_layout.addWidget(self.deploy_version, 1, 1) build_group.setLayout(build_layout) layout.addWidget(build_group) # Section 3: 部署选项 options_group = QGroupBox("部署选项") options_layout = QVBoxLayout() self.deploy_shell_check = QCheckBox("部署 Shell(在线登录页)") self.deploy_shell_check.setChecked(True) options_layout.addWidget(self.deploy_shell_check) self.deploy_core_check = QCheckBox("部署 Core(核心应用)") self.deploy_core_check.setChecked(True) options_layout.addWidget(self.deploy_core_check) self.deploy_shell_zip_check = QCheckBox("打包 Shell 为 .zip(供 CEP 扩展下载)") self.deploy_shell_zip_check.setChecked(True) options_layout.addWidget(self.deploy_shell_zip_check) self.deploy_build_check = QCheckBox("构建前端(npm run build)") self.deploy_build_check.setChecked(True) options_layout.addWidget(self.deploy_build_check) options_group.setLayout(options_layout) layout.addWidget(options_group) # Section 4: 部署按钮 btn_layout = QHBoxLayout() self.btn_deploy_start = QPushButton("🚀 开始部署") self.btn_deploy_start.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; padding: 10px;") self.btn_deploy_start.clicked.connect(self.start_deploy) btn_layout.addWidget(self.btn_deploy_start) self.btn_deploy_stop = QPushButton("⏹ 停止") self.btn_deploy_stop.setEnabled(False) btn_layout.addWidget(self.btn_deploy_stop) layout.addLayout(btn_layout) # Section 5: 进度条 self.deploy_progress = QProgressBar() layout.addWidget(self.deploy_progress) # Section 6: 日志输出 log_label = QLabel("部署日志:") layout.addWidget(log_label) self.deploy_log = QTextEdit() self.deploy_log.setReadOnly(True) self.deploy_log.setStyleSheet("background-color: #1E1E1E; color: #D4D4D4; font-family: Consolas, monospace;") layout.addWidget(self.deploy_log) # 加载配置 self.load_deploy_config() def init_connection(self): url = self.url_edit.text() token = self.token_edit.text() self.api_client = ApiClient(url, token) try: self.refresh_all_data() self.tabs.setEnabled(True) self.statusBar().showMessage("连接成功", 3000) except Exception as e: QMessageBox.critical(self, "连接错误", str(e)) self.tabs.setEnabled(False) def refresh_all_data(self): if not self.api_client: return try: self.groups_data = self.api_client.list_groups() self.users_data = self.api_client.list_users() self.archives_data = self.api_client.list_archives() self.update_ui_with_data() except Exception as e: QMessageBox.critical(self, "错误", f"刷新数据失败: {e}") def update_ui_with_data(self): # Update Group List self.group_list_widget.clear() self.target_group_combo.clear() # 1. Add "Unassigned" pseudo-group unassigned_group = { 'id': None, 'name': '未分配组 (Unassigned)', 'current_version_file': '-', 'comment': '新注册或未分配的用户' } item_unassigned = QListWidgetItem(unassigned_group['name']) item_unassigned.setData(Qt.UserRole, unassigned_group) self.group_list_widget.addItem(item_unassigned) # 2. Add real groups for g in self.groups_data: item = QListWidgetItem(g['name']) item.setData(Qt.UserRole, g) self.group_list_widget.addItem(item) # Update Combo in Release Tab self.target_group_combo.addItem(g['name'], g['id']) # If there's a selection, refresh the user view, otherwise select first if self.group_list_widget.count() > 0: self.group_list_widget.setCurrentRow(0) # Update Archives List self.archives_list_widget.clear() for filename in self.archives_data: self.archives_list_widget.addItem(filename) def on_group_selected(self, current, previous): if not current: return group = current.data(Qt.UserRole) # Update Header self.lbl_group_name.setText(group['name']) self.lbl_group_version.setText(group['current_version_file'] or "无") self.lbl_group_comment.setText(group['comment'] or "无") # Filter Users if group['id'] is None: # Special handling for unassigned users group_users = [u for u in self.users_data if u['group_id'] is None] else: group_users = [u for u in self.users_data if u['group_id'] == group['id']] self.user_table.setRowCount(len(group_users)) for i, u in enumerate(group_users): self.user_table.setItem(i, 0, QTableWidgetItem(str(u['id']))) self.user_table.setItem(i, 1, QTableWidgetItem(u['username'])) self.user_table.setItem(i, 2, QTableWidgetItem(u['permissions'] or "")) # Store user object in first item self.user_table.item(i, 0).setData(Qt.UserRole, u) def on_archive_selected(self, current, previous): if not current: return filename = current.text() self.uploaded_filename_label.setText(filename) def create_group(self): dialog = CreateGroupDialog(self) if dialog.exec() == QDialog.Accepted: name, comment = dialog.get_data() if not name: QMessageBox.warning(self, "警告", "组名称必填") return try: self.api_client.create_group(name, comment) self.refresh_all_data() QMessageBox.information(self, "成功", f"组 '{name}' 创建成功!") except Exception as e: QMessageBox.critical(self, "错误", str(e)) def edit_group_comment(self): item = self.group_list_widget.currentItem() if not item: return group = item.data(Qt.UserRole) text, ok = QInputDialog.getText(self, "修改备注", "请输入新备注:", text=group['comment']) if ok: try: self.api_client.update_group_comment(group['id'], text) self.refresh_all_data() except Exception as e: QMessageBox.critical(self, "错误", str(e)) def move_selected_user(self): row = self.user_table.currentRow() if row < 0: QMessageBox.warning(self, "警告", "请先在列表中选择一个用户") return user = self.user_table.item(row, 0).data(Qt.UserRole) # Create a simple dialog with combo box dialog = QDialog(self) dialog.setWindowTitle("移动用户") layout = QVBoxLayout(dialog) combo = QComboBox() for g in self.groups_data: if g['id'] != user['group_id']: combo.addItem(g['name'], g['id']) layout.addWidget(QLabel(f"将用户 {user['username']} 移动到:")) layout.addWidget(combo) btns = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) btns.accepted.connect(dialog.accept) btns.rejected.connect(dialog.reject) layout.addWidget(btns) if dialog.exec() == QDialog.Accepted: new_group_id = combo.currentData() if new_group_id: try: self.api_client.update_user_group(user['id'], new_group_id) self.refresh_all_data() QMessageBox.information(self, "成功", "用户移动成功") except Exception as e: QMessageBox.critical(self, "错误", str(e)) def change_user_perm(self): row = self.user_table.currentRow() if row < 0: QMessageBox.warning(self, "警告", "请先在列表中选择一个用户") return user = self.user_table.item(row, 0).data(Qt.UserRole) text, ok = QInputDialog.getText(self, "修改权限", "输入权限 (逗号分隔):", text=user['permissions'] or "") if ok: try: self.api_client.update_user_permissions(user['id'], text) self.refresh_all_data() except Exception as e: QMessageBox.critical(self, "错误", str(e)) def browse_file(self): # Allow selecting either file or directory # QFileDialog doesn't easily support both at once, so we'll use directory for now as requested # Or we can add a second button. # User asked to "change to folder", implies preferring folder selection. # Let's support both but via directory selector if user wants to zip folder, # or maybe we can just use getExistingDirectory. # Better approach: Add a choice or just use getExistingDirectory since user asked for folder support. # But if they select a zip file, they can't. # Let's add a "Browse Folder..." logic or modify existing. # User said: "Change to folder... and auto zip". # I will change the dialog to getExistingDirectory. path = QFileDialog.getExistingDirectory(self, "选择发布文件夹 (自动压缩)") if path: self.file_path_edit.setText(path) def upload_file(self): path = self.file_path_edit.text() if not path or not os.path.exists(path): QMessageBox.warning(self, "警告", "请先选择有效的路径。") return try: self.upload_btn.setEnabled(False) self.upload_btn.setText("处理中...") QApplication.processEvents() final_file_path = path # If directory, zip it first if os.path.isdir(path): self.upload_btn.setText("正在压缩...") QApplication.processEvents() timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") base_name = os.path.basename(os.path.abspath(path)) zip_filename = f"{base_name}_{timestamp}.zip" # Create zip in temp or current dir? Current dir is safer/easier to debug # Or better, use a temporary file to avoid clutter # Exclude patterns def zip_directory(source_dir, output_filename): with zipfile.ZipFile(output_filename, 'w', zipfile.ZIP_DEFLATED) as zipf: for root, dirs, files in os.walk(source_dir): dirs[:] = [d for d in dirs if d not in ['.git', 'node_modules', 'dist', 'archives', '__pycache__']] for file in files: if file == output_filename or file.endswith('.zip') or file.endswith('.pyc'): continue file_path = os.path.join(root, file) arcname = os.path.relpath(file_path, source_dir) zipf.write(file_path, arcname) zip_directory(path, zip_filename) final_file_path = zip_filename self.upload_btn.setText("正在上传...") QApplication.processEvents() filename = self.api_client.upload_version(final_file_path) # If we created a zip, maybe delete it or keep it? # User might want to keep local backup. I'll keep it for now but log it. QMessageBox.information(self, "成功", f"上传成功: {filename}") self.refresh_all_data() except Exception as e: QMessageBox.critical(self, "上传错误", str(e)) finally: self.upload_btn.setEnabled(True) self.upload_btn.setText("上传") def assign_version(self): filename = self.uploaded_filename_label.text() if filename == "无" or not filename: QMessageBox.warning(self, "警告", "请先上传文件。") return group_id = self.target_group_combo.currentData() if group_id is None: QMessageBox.warning(self, "警告", "请选择目标组。") return try: self.api_client.update_group_version(group_id, filename) self.refresh_all_data() QMessageBox.information(self, "成功", f"组版本已更新为 {filename}") except Exception as e: QMessageBox.critical(self, "错误", str(e)) # ==================== 部署相关方法 ==================== def save_deploy_config(self): """保存部署配置到本地文件""" try: config_file = os.path.join(os.path.dirname(__file__), 'deploy_config.txt') with open(config_file, 'w', encoding='utf-8') as f: f.write(f"host={self.deploy_host.text()}\n") f.write(f"port={self.deploy_port.text()}\n") f.write(f"user={self.deploy_user.text()}\n") f.write(f"password={self.deploy_password.text()}\n") f.write(f"remote_path={self.deploy_remote_path.text()}\n") f.write(f"project_root={self.deploy_project_root.text()}\n") f.write(f"version={self.deploy_version.text()}\n") QMessageBox.information(self, "成功", "配置已保存") except Exception as e: QMessageBox.critical(self, "错误", f"保存配置失败: {e}") def load_deploy_config(self): """从本地文件加载部署配置""" try: config_file = os.path.join(os.path.dirname(__file__), 'deploy_config.txt') if not os.path.exists(config_file): return with open(config_file, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if '=' in line: key, value = line.split('=', 1) if key == 'host': self.deploy_host.setText(value) elif key == 'port': self.deploy_port.setText(value) elif key == 'user': self.deploy_user.setText(value) elif key == 'password': self.deploy_password.setText(value) elif key == 'remote_path': self.deploy_remote_path.setText(value) elif key == 'project_root': self.deploy_project_root.setText(value) elif key == 'version': self.deploy_version.setText(value) except Exception as e: self.log_deploy(f"加载配置失败: {e}") def browse_project_root(self): """浏览项目根目录""" path = QFileDialog.getExistingDirectory(self, "选择项目根目录") if path: self.deploy_project_root.setText(path) def test_ssh_connection(self): """测试 SSH 连接""" try: ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) self.log_deploy("正在测试 SSH 连接...") ssh.connect( hostname=self.deploy_host.text(), port=int(self.deploy_port.text()), username=self.deploy_user.text(), password=self.deploy_password.text(), timeout=10 ) stdin, stdout, stderr = ssh.exec_command('pwd') remote_pwd = stdout.read().decode().strip() ssh.close() QMessageBox.information(self, "连接成功", f"SSH 连接测试成功!\n当前目录: {remote_pwd}") self.log_deploy(f"✅ SSH 连接成功,当前目录: {remote_pwd}") except Exception as e: QMessageBox.critical(self, "连接失败", f"SSH 连接测试失败:\n{str(e)}") self.log_deploy(f"❌ SSH 连接失败: {e}") def log_deploy(self, message): """添加部署日志""" self.deploy_log.append(message) QApplication.processEvents() def start_deploy(self): """开始部署流程""" # 验证配置 if not self.deploy_host.text() or not self.deploy_user.text(): QMessageBox.warning(self, "配置不完整", "请先配置服务器信息") return if not os.path.exists(self.deploy_project_root.text()): QMessageBox.warning(self, "路径错误", "项目根目录不存在") return # 确认部署 reply = QMessageBox.question( self, '确认部署', f"即将部署到服务器: {self.deploy_host.text()}\n\n" f"部署项目:\n" f"{'✅ Shell(在线登录页)' if self.deploy_shell_check.isChecked() else '⬜ Shell'}\n" f"{'✅ Core(核心应用)' if self.deploy_core_check.isChecked() else '⬜ Core'}\n" f"{'✅ Shell.zip(下载包)' if self.deploy_shell_zip_check.isChecked() else '⬜ Shell.zip'}\n\n" f"是否继续?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.No: return # 禁用按钮 self.btn_deploy_start.setEnabled(False) self.btn_deploy_stop.setEnabled(True) self.deploy_progress.setValue(0) self.deploy_log.clear() try: self.run_deploy() except Exception as e: QMessageBox.critical(self, "部署失败", str(e)) self.log_deploy(f"\n❌ 部署失败: {e}") finally: self.btn_deploy_start.setEnabled(True) self.btn_deploy_stop.setEnabled(False) self.deploy_progress.setValue(100) def run_deploy(self): """执行部署流程""" project_root = self.deploy_project_root.text() designer_path = os.path.join(project_root, 'Designer') version = self.deploy_version.text() self.log_deploy("="*60) self.log_deploy("🚀 开始自动化部署流程") self.log_deploy("="*60) # Step 1: 构建前端 if self.deploy_build_check.isChecked(): self.log_deploy("\n📦 步骤 1/5: 构建前端...") self.deploy_progress.setValue(10) try: self.log_deploy(f"执行: cd {designer_path} && npm run build") result = subprocess.run( 'npm run build', cwd=designer_path, shell=True, capture_output=True, text=True, timeout=300 ) if result.returncode == 0: self.log_deploy("✅ 前端构建成功") else: self.log_deploy(f"❌ 构建失败: {result.stderr}") raise Exception("前端构建失败") except subprocess.TimeoutExpired: raise Exception("构建超时(5分钟)") else: self.log_deploy("\n⏭ 跳过前端构建") self.deploy_progress.setValue(10) # Step 2: 连接 SSH self.log_deploy("\n🔐 步骤 2/5: 连接服务器...") self.deploy_progress.setValue(30) ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: ssh.connect( hostname=self.deploy_host.text(), port=int(self.deploy_port.text()), username=self.deploy_user.text(), password=self.deploy_password.text(), timeout=30 ) self.log_deploy("✅ SSH 连接成功") except Exception as e: raise Exception(f"SSH 连接失败: {e}") # Step 3: 创建服务器目录 self.log_deploy("\n📁 步骤 3/5: 创建服务器目录...") self.deploy_progress.setValue(40) remote_path = self.deploy_remote_path.text() commands = [ f"mkdir -p {remote_path}/shell", f"mkdir -p {remote_path}/core/{version}", f"mkdir -p {remote_path}/downloads" ] for cmd in commands: stdin, stdout, stderr = ssh.exec_command(cmd) stdout.channel.recv_exit_status() self.log_deploy("✅ 目录创建完成") # Step 4: 上传文件 self.log_deploy("\n📤 步骤 4/5: 上传文件到服务器...") self.deploy_progress.setValue(50) sftp = ssh.open_sftp() dist_path = os.path.join(designer_path, 'dist') try: # 上传 Shell if self.deploy_shell_check.isChecked(): self.log_deploy(" 上传 Shell(在线登录页)...") shell_src = os.path.join(dist_path, 'Shell') shell_dst = f"{remote_path}/shell" self._upload_directory(sftp, shell_src, shell_dst) self.log_deploy(" ✅ Shell 上传完成") self.deploy_progress.setValue(60) # 上传 Core if self.deploy_core_check.isChecked(): self.log_deploy(" 上传 Core(核心应用)...") core_src = os.path.join(dist_path, 'Designer') core_dst = f"{remote_path}/core/{version}" self._upload_directory(sftp, core_src, core_dst) self.log_deploy(" ✅ Core 上传完成") self.deploy_progress.setValue(80) # 打包并上传 Shell.zip if self.deploy_shell_zip_check.isChecked(): self.log_deploy(" 打包 Shell.zip(供 CEP 扩展下载)...") shell_src = os.path.join(dist_path, 'Shell') zip_filename = f"shell-{version}.zip" zip_path = os.path.join(dist_path, zip_filename) # 创建 ZIP import zipfile with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: for root, dirs, files in os.walk(shell_src): for file in files: file_path = os.path.join(root, file) arcname = os.path.relpath(file_path, shell_src) zipf.write(file_path, arcname) # 上传 ZIP remote_zip = f"{remote_path}/downloads/{zip_filename}" sftp.put(zip_path, remote_zip) self.log_deploy(f" ✅ Shell.zip 上传完成: {zip_filename}") # 删除本地 ZIP os.remove(zip_path) finally: sftp.close() self.deploy_progress.setValue(90) # Step 5: 验证部署 self.log_deploy("\n✅ 步骤 5/5: 验证部署...") commands_verify = [] if self.deploy_shell_check.isChecked(): commands_verify.append(f"ls {remote_path}/shell/index.html") if self.deploy_core_check.isChecked(): commands_verify.append(f"ls {remote_path}/core/{version}/index.html") if self.deploy_shell_zip_check.isChecked(): commands_verify.append(f"ls {remote_path}/downloads/shell-{version}.zip") for cmd in commands_verify: stdin, stdout, stderr = ssh.exec_command(cmd) exit_code = stdout.channel.recv_exit_status() if exit_code != 0: self.log_deploy(f" ⚠️ 验证失败: {cmd}") ssh.close() self.deploy_progress.setValue(100) self.log_deploy("\n" + "="*60) self.log_deploy("🎉 部署完成!") self.log_deploy("="*60) self.log_deploy(f"\n访问地址:") if self.deploy_shell_check.isChecked(): self.log_deploy(f" Shell: https://{self.deploy_host.text()}/shell/") if self.deploy_core_check.isChecked(): self.log_deploy(f" Core: https://{self.deploy_host.text()}/core/{version}/") if self.deploy_shell_zip_check.isChecked(): self.log_deploy(f" 下载: https://{self.deploy_host.text()}/downloads/shell-{version}.zip") QMessageBox.information(self, "部署成功", f"部署完成!\n版本: {version}") def _upload_directory(self, sftp, local_dir, remote_dir): """递归上传目录""" if not os.path.exists(local_dir): raise Exception(f"本地目录不存在: {local_dir}") # 创建远程目录 try: sftp.stat(remote_dir) except IOError: sftp.mkdir(remote_dir) # 递归上传文件 for item in os.listdir(local_dir): local_path = os.path.join(local_dir, item) remote_path = remote_dir + '/' + item if os.path.isfile(local_path): self.log_deploy(f" → {item}") sftp.put(local_path, remote_path) elif os.path.isdir(local_path): self._upload_directory(sftp, local_path, remote_path) if __name__ == "__main__": app = QApplication(sys.argv) window = AdminWindow() window.show() sys.exit(app.exec())