Initial commit - DesignerCEP Project with Caddy deployment
This commit is contained in:
330
AdminTool/README.md
Normal file
330
AdminTool/README.md
Normal file
@@ -0,0 +1,330 @@
|
||||
# DesignerCEP AdminTool 使用说明
|
||||
|
||||
## 📦 安装依赖
|
||||
|
||||
```bash
|
||||
cd AdminTool
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 一、自动化部署脚本 (auto_deploy_core.py)
|
||||
|
||||
### 功能说明
|
||||
|
||||
自动化完成以下所有步骤:
|
||||
1. ✅ 构建前端(Shell + Core)
|
||||
2. ✅ 打包 Shell.zip(供 CEP 扩展下载)
|
||||
3. ✅ 上传到服务器(SSH/SFTP)
|
||||
4. ✅ 更新 MySQL 数据库版本号
|
||||
5. ✅ 清除本地缓存(测试用)
|
||||
|
||||
### 使用方法
|
||||
|
||||
#### 1. 首次使用 - 配置服务器
|
||||
|
||||
```bash
|
||||
python auto_deploy_core.py --version 1.0.6 --setup
|
||||
```
|
||||
|
||||
会提示你输入:
|
||||
- 服务器地址
|
||||
- SSH 端口、用户名、密码
|
||||
- 远程路径
|
||||
- MySQL 地址、端口、用户名、密码、数据库名
|
||||
|
||||
配置会保存到 `deploy_config.json`,下次直接使用。
|
||||
|
||||
#### 2. 仅构建(不部署)
|
||||
|
||||
```bash
|
||||
python auto_deploy_core.py --version 1.0.6
|
||||
```
|
||||
|
||||
只在本地构建 Shell 和 Core,不上传到服务器。
|
||||
|
||||
#### 3. 构建并部署
|
||||
|
||||
```bash
|
||||
python auto_deploy_core.py --version 1.0.6 --deploy
|
||||
```
|
||||
|
||||
构建并自动上传到服务器:
|
||||
- Shell 在线登录页 → `/static/shell/`
|
||||
- Core 核心应用 → `/static/core/1.0.6/`
|
||||
- Shell.zip 下载包 → `/static/downloads/shell-1.0.6.zip`
|
||||
|
||||
#### 4. 构建、部署并更新数据库
|
||||
|
||||
```bash
|
||||
python auto_deploy_core.py --version 1.0.6 --deploy --update-db
|
||||
```
|
||||
|
||||
完整流程:构建 → 上传 → 更新 MySQL 数据库版本号。
|
||||
|
||||
**数据库更新 SQL:**
|
||||
```sql
|
||||
UPDATE plugin_groups SET current_version = '1.0.6' WHERE id = 1;
|
||||
```
|
||||
|
||||
#### 5. 其他选项
|
||||
|
||||
```bash
|
||||
# 跳过清除缓存
|
||||
python auto_deploy_core.py --version 1.0.6 --deploy --skip-clean
|
||||
|
||||
# 重新配置服务器
|
||||
python auto_deploy_core.py --version 1.0.6 --setup
|
||||
```
|
||||
|
||||
### 配置文件示例
|
||||
|
||||
`deploy_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"host": "your-server.com",
|
||||
"port": "22",
|
||||
"username": "root",
|
||||
"password": "your-password",
|
||||
"remote_path": "/var/www/DesignerCEP/Server/static",
|
||||
"mysql": {
|
||||
"host": "localhost",
|
||||
"port": "3306",
|
||||
"username": "root",
|
||||
"password": "your-mysql-password",
|
||||
"database": "designer_cep",
|
||||
"table": "plugin_groups"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 二、图形化管理工具 (admin_gui.py)
|
||||
|
||||
### 功能说明
|
||||
|
||||
提供可视化界面管理:
|
||||
- 用户组管理
|
||||
- 用户权限管理
|
||||
- 版本上传和分配
|
||||
- **自动化部署**(新增)
|
||||
|
||||
### 使用方法
|
||||
|
||||
```bash
|
||||
python admin_gui.py
|
||||
```
|
||||
|
||||
### 主要功能
|
||||
|
||||
#### 1. 组与用户管理
|
||||
|
||||
- 创建用户组
|
||||
- 修改组备注
|
||||
- 查看组内用户
|
||||
- 移动用户到其他组
|
||||
- 修改用户权限
|
||||
|
||||
#### 2. 发布与上传
|
||||
|
||||
- 上传 ZIP 版本文件
|
||||
- 查看历史版本
|
||||
- 分配版本给用户组
|
||||
|
||||
#### 3. 自动化部署 ⭐ (新增)
|
||||
|
||||
完全图形化的部署流程:
|
||||
|
||||
**3.1 服务器配置**
|
||||
- 服务器地址、SSH 端口
|
||||
- 用户名、密码
|
||||
- 远程路径
|
||||
- 保存/加载配置
|
||||
- 测试 SSH 连接
|
||||
|
||||
**3.2 本地构建配置**
|
||||
- 项目根目录(自动检测)
|
||||
- 版本号
|
||||
|
||||
**3.3 部署选项**
|
||||
- ☑️ 部署 Shell(在线登录页)
|
||||
- ☑️ 部署 Core(核心应用)
|
||||
- ☑️ 打包 Shell 为 .zip
|
||||
- ☑️ 构建前端(npm run build)
|
||||
|
||||
**3.4 一键部署**
|
||||
- 点击 "🚀 开始部署" 按钮
|
||||
- 实时查看部署日志
|
||||
- 进度条显示当前进度
|
||||
- 部署完成后显示访问地址
|
||||
|
||||
---
|
||||
|
||||
## 📖 三、部署流程说明
|
||||
|
||||
### 完整部署流程
|
||||
|
||||
```
|
||||
1. 构建前端
|
||||
└─> npm run build (Designer/)
|
||||
└─> 生成 dist/Shell/ 和 dist/Designer/
|
||||
|
||||
2. 打包 Shell.zip
|
||||
└─> 压缩 dist/Shell/ → shell-{version}.zip
|
||||
|
||||
3. 连接服务器 (SSH)
|
||||
└─> 使用配置的服务器信息
|
||||
|
||||
4. 创建远程目录
|
||||
└─> /static/shell/
|
||||
└─> /static/core/{version}/
|
||||
└─> /static/downloads/
|
||||
|
||||
5. 上传文件
|
||||
└─> Shell → /static/shell/ (在线登录页)
|
||||
└─> Core → /static/core/{version}/ (核心应用)
|
||||
└─> shell-{version}.zip → /static/downloads/ (CEP 扩展下载)
|
||||
|
||||
6. 更新数据库 (MySQL)
|
||||
└─> UPDATE plugin_groups SET current_version = '{version}'
|
||||
|
||||
7. 完成!
|
||||
└─> 显示访问地址
|
||||
```
|
||||
|
||||
### 服务器目录结构
|
||||
|
||||
```
|
||||
/var/www/DesignerCEP/Server/static/
|
||||
├── shell/ # Shell 在线登录页
|
||||
│ ├── index.html
|
||||
│ └── assets/
|
||||
├── downloads/ # 下载文件
|
||||
│ └── shell-1.0.6.zip
|
||||
└── core/ # Core 核心应用
|
||||
├── 1.0.5/
|
||||
└── 1.0.6/
|
||||
├── index.html
|
||||
└── assets/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 四、常见问题
|
||||
|
||||
### Q1: SSH 连接失败
|
||||
|
||||
**A**: 检查以下几点:
|
||||
1. 服务器地址和端口是否正确
|
||||
2. 用户名密码是否正确
|
||||
3. 服务器防火墙是否开放 SSH 端口
|
||||
4. 本地网络是否正常
|
||||
|
||||
### Q2: MySQL 连接失败
|
||||
|
||||
**A**: 检查以下几点:
|
||||
1. MySQL 地址和端口是否正确
|
||||
2. 用户名密码是否正确
|
||||
3. 数据库是否存在
|
||||
4. MySQL 是否允许远程连接
|
||||
|
||||
### Q3: 构建失败
|
||||
|
||||
**A**:
|
||||
1. 检查是否安装了 Node.js 和 npm
|
||||
2. 检查项目目录是否正确
|
||||
3. 运行 `npm install` 安装依赖
|
||||
4. 查看错误日志
|
||||
|
||||
### Q4: 上传速度慢
|
||||
|
||||
**A**:
|
||||
1. 检查网络带宽
|
||||
2. 考虑使用国内服务器
|
||||
3. 可以先在本地构建,然后手动上传
|
||||
|
||||
### Q5: 如何回滚版本?
|
||||
|
||||
**A**:
|
||||
1. 在数据库中修改版本号为旧版本
|
||||
2. 或删除 `/static/core/{new_version}/` 目录
|
||||
|
||||
---
|
||||
|
||||
## 📝 五、配置文件说明
|
||||
|
||||
### deploy_config.json
|
||||
|
||||
| 字段 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `host` | 服务器地址 | `your-server.com` |
|
||||
| `port` | SSH 端口 | `22` |
|
||||
| `username` | SSH 用户名 | `root` |
|
||||
| `password` | SSH 密码 | `your-password` |
|
||||
| `remote_path` | 远程静态文件路径 | `/var/www/DesignerCEP/Server/static` |
|
||||
| `mysql.host` | MySQL 地址 | `localhost` |
|
||||
| `mysql.port` | MySQL 端口 | `3306` |
|
||||
| `mysql.username` | MySQL 用户名 | `root` |
|
||||
| `mysql.password` | MySQL 密码 | `your-mysql-password` |
|
||||
| `mysql.database` | 数据库名 | `designer_cep` |
|
||||
| `mysql.table` | 表名 | `plugin_groups` |
|
||||
|
||||
### deploy_config.txt (admin_gui.py 使用)
|
||||
|
||||
纯文本格式,一行一个配置:
|
||||
```
|
||||
host=your-server.com
|
||||
port=22
|
||||
user=root
|
||||
password=your-password
|
||||
remote_path=/var/www/DesignerCEP/Server/static
|
||||
project_root=D:\main\DesignerCEP
|
||||
version=1.0.6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎉 六、快速开始
|
||||
|
||||
### 方法一:命令行部署(推荐)
|
||||
|
||||
```bash
|
||||
# 1. 首次配置
|
||||
python auto_deploy_core.py --version 1.0.6 --setup
|
||||
|
||||
# 2. 部署到服务器
|
||||
python auto_deploy_core.py --version 1.0.6 --deploy --update-db
|
||||
|
||||
# 完成!
|
||||
```
|
||||
|
||||
### 方法二:图形化部署
|
||||
|
||||
```bash
|
||||
# 1. 启动图形界面
|
||||
python admin_gui.py
|
||||
|
||||
# 2. 切换到"自动化部署"标签
|
||||
# 3. 配置服务器信息
|
||||
# 4. 点击"开始部署"按钮
|
||||
|
||||
# 完成!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如遇到问题,请检查:
|
||||
1. 部署日志输出
|
||||
2. 服务器 SSH 日志
|
||||
3. MySQL 连接状态
|
||||
4. 网络连接情况
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2024-12-17
|
||||
|
||||
994
AdminTool/admin_gui.py
Normal file
994
AdminTool/admin_gui.py
Normal file
@@ -0,0 +1,994 @@
|
||||
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("<b>用户组列表</b>"))
|
||||
|
||||
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("<b>组内用户</b>"))
|
||||
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())
|
||||
540
AdminTool/auto_deploy_core.py
Normal file
540
AdminTool/auto_deploy_core.py
Normal file
@@ -0,0 +1,540 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Core 应用自动发布脚本
|
||||
完全自动化构建、打包、上传服务器、更新数据库
|
||||
|
||||
使用方法:
|
||||
python auto_deploy_core.py --version v1.0.6
|
||||
python auto_deploy_core.py --version v1.0.6 --deploy # 部署到服务器
|
||||
python auto_deploy_core.py --version v1.0.6 --deploy --update-db # 部署并更新数据库
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
import argparse
|
||||
import zipfile
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import paramiko
|
||||
import pymysql
|
||||
|
||||
# ==================== 配置区域 ====================
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.absolute() # 上一级目录
|
||||
DESIGNER_DIR = PROJECT_ROOT / "Designer"
|
||||
DIST_CORE_DIR = DESIGNER_DIR / "dist" / "Designer" # Core 构建输出目录
|
||||
DIST_SHELL_DIR = DESIGNER_DIR / "dist" / "Shell" # Shell 构建输出目录
|
||||
SERVER_DIR = PROJECT_ROOT / "Server"
|
||||
ARCHIVES_DIR = SERVER_DIR / "archives"
|
||||
CONFIG_FILE = Path(__file__).parent / "deploy_config.json"
|
||||
|
||||
# 颜色输出(Windows 兼容)
|
||||
try:
|
||||
import colorama
|
||||
colorama.init()
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[93m'
|
||||
RED = '\033[91m'
|
||||
BLUE = '\033[94m'
|
||||
RESET = '\033[0m'
|
||||
except ImportError:
|
||||
GREEN = YELLOW = RED = BLUE = RESET = ''
|
||||
|
||||
# ==================== 工具函数 ====================
|
||||
|
||||
def print_step(step_num, total_steps, message):
|
||||
"""打印步骤信息"""
|
||||
print(f"\n{BLUE}{'='*60}{RESET}")
|
||||
print(f"{GREEN}[步骤 {step_num}/{total_steps}] {message}{RESET}")
|
||||
print(f"{BLUE}{'='*60}{RESET}\n")
|
||||
|
||||
def print_success(message):
|
||||
"""打印成功信息"""
|
||||
print(f"{GREEN}✓ {message}{RESET}")
|
||||
|
||||
def print_warning(message):
|
||||
"""打印警告信息"""
|
||||
print(f"{YELLOW}⚠ {message}{RESET}")
|
||||
|
||||
def print_error(message):
|
||||
"""打印错误信息"""
|
||||
print(f"{RED}✗ {message}{RESET}")
|
||||
|
||||
def run_command(command, cwd=None, shell=True, check=True):
|
||||
"""运行命令并返回结果"""
|
||||
print(f" 执行: {command}")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=cwd,
|
||||
shell=shell,
|
||||
check=check,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
errors='replace'
|
||||
)
|
||||
if result.stdout:
|
||||
print(f" 输出: {result.stdout.strip()}")
|
||||
return result
|
||||
except subprocess.CalledProcessError as e:
|
||||
print_error(f"命令执行失败: {e}")
|
||||
if e.stderr:
|
||||
print(f" 错误: {e.stderr}")
|
||||
if check:
|
||||
sys.exit(1)
|
||||
return None
|
||||
|
||||
# ==================== 发布步骤 ====================
|
||||
|
||||
def load_config():
|
||||
"""加载部署配置"""
|
||||
if not CONFIG_FILE.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print_warning(f"加载配置文件失败: {e}")
|
||||
return None
|
||||
|
||||
def save_config(config):
|
||||
"""保存部署配置"""
|
||||
try:
|
||||
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
print_success(f"配置已保存: {CONFIG_FILE}")
|
||||
except Exception as e:
|
||||
print_error(f"保存配置失败: {e}")
|
||||
|
||||
def step1_build_frontend():
|
||||
"""步骤 1: 构建前端(Shell + Core)"""
|
||||
print_step(1, 8, "构建前端(Shell + Core)")
|
||||
|
||||
# 检查 package.json
|
||||
package_json = DESIGNER_DIR / "package.json"
|
||||
if not package_json.exists():
|
||||
print_error(f"未找到 package.json: {package_json}")
|
||||
sys.exit(1)
|
||||
|
||||
# 运行构建命令
|
||||
os.chdir(DESIGNER_DIR)
|
||||
print(" 正在构建 Core...")
|
||||
run_command("npm run build:core", cwd=DESIGNER_DIR)
|
||||
print(" 正在构建 Shell...")
|
||||
run_command("npm run build", cwd=DESIGNER_DIR)
|
||||
|
||||
# 验证输出目录
|
||||
if not DIST_CORE_DIR.exists():
|
||||
print_error(f"Core 构建输出目录不存在: {DIST_CORE_DIR}")
|
||||
sys.exit(1)
|
||||
|
||||
if not DIST_SHELL_DIR.exists():
|
||||
print_error(f"Shell 构建输出目录不存在: {DIST_SHELL_DIR}")
|
||||
sys.exit(1)
|
||||
|
||||
print_success(f"构建完成")
|
||||
print(f" Core: {DIST_CORE_DIR}")
|
||||
print(f" Shell: {DIST_SHELL_DIR}")
|
||||
|
||||
def step2_package_shell_zip(version):
|
||||
"""步骤 2: 打包 Shell 为 ZIP(供 CEP 扩展下载)"""
|
||||
print_step(2, 8, "打包 Shell 为 ZIP")
|
||||
|
||||
# 生成 ZIP 文件名
|
||||
zip_filename = f"shell-{version}.zip"
|
||||
zip_path = DESIGNER_DIR / "dist" / zip_filename
|
||||
|
||||
# 如果文件已存在,先删除
|
||||
if zip_path.exists():
|
||||
print_warning(f"ZIP 文件已存在,删除: {zip_path}")
|
||||
zip_path.unlink()
|
||||
|
||||
# 创建 ZIP
|
||||
print(f" 创建 ZIP: {zip_path}")
|
||||
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for root, dirs, files in os.walk(DIST_SHELL_DIR):
|
||||
for file in files:
|
||||
file_path = Path(root) / file
|
||||
arcname = file_path.relative_to(DIST_SHELL_DIR)
|
||||
zipf.write(file_path, arcname)
|
||||
|
||||
# 显示文件大小
|
||||
size_mb = zip_path.stat().st_size / (1024 * 1024)
|
||||
print_success(f"Shell 打包完成: {zip_path}")
|
||||
print(f" 文件大小: {size_mb:.2f} MB")
|
||||
|
||||
return zip_path
|
||||
|
||||
def step3_upload_to_server(version, config):
|
||||
"""步骤 3: 上传到服务器"""
|
||||
print_step(3, 8, "上传到服务器")
|
||||
|
||||
if not config:
|
||||
print_warning("未配置服务器信息,跳过上传")
|
||||
return
|
||||
|
||||
# 连接 SSH
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
try:
|
||||
print(f" 连接服务器: {config['host']}")
|
||||
ssh.connect(
|
||||
hostname=config['host'],
|
||||
port=int(config.get('port', 22)),
|
||||
username=config['username'],
|
||||
password=config.get('password', ''),
|
||||
timeout=30
|
||||
)
|
||||
print_success("SSH 连接成功")
|
||||
|
||||
sftp = ssh.open_sftp()
|
||||
remote_path = config['remote_path']
|
||||
|
||||
# 1. 创建远程目录
|
||||
print(" 创建远程目录...")
|
||||
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()
|
||||
print_success("目录创建完成")
|
||||
|
||||
# 2. 上传 Shell(在线登录页)
|
||||
print(" 上传 Shell(在线登录页)...")
|
||||
upload_directory(sftp, DIST_SHELL_DIR, f"{remote_path}/shell")
|
||||
print_success("Shell 上传完成")
|
||||
|
||||
# 3. 上传 Core
|
||||
print(" 上传 Core(核心应用)...")
|
||||
upload_directory(sftp, DIST_CORE_DIR, f"{remote_path}/core/{version}")
|
||||
print_success("Core 上传完成")
|
||||
|
||||
# 4. 上传 Shell.zip
|
||||
print(" 上传 Shell.zip(CEP 扩展下载)...")
|
||||
shell_zip = DESIGNER_DIR / "dist" / f"shell-{version}.zip"
|
||||
if shell_zip.exists():
|
||||
remote_zip = f"{remote_path}/downloads/shell-{version}.zip"
|
||||
sftp.put(str(shell_zip), remote_zip)
|
||||
print_success(f"Shell.zip 上传完成: {remote_zip}")
|
||||
|
||||
sftp.close()
|
||||
ssh.close()
|
||||
|
||||
print_success("所有文件上传完成")
|
||||
print(f"\n访问地址:")
|
||||
print(f" Shell: https://{config['host']}/shell/")
|
||||
print(f" Core: https://{config['host']}/core/{version}/")
|
||||
print(f" 下载: https://{config['host']}/downloads/shell-{version}.zip")
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"上传失败: {e}")
|
||||
raise
|
||||
finally:
|
||||
ssh.close()
|
||||
|
||||
def upload_directory(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):
|
||||
print(f" → {item}")
|
||||
sftp.put(local_path, remote_path)
|
||||
elif os.path.isdir(local_path):
|
||||
upload_directory(sftp, local_path, remote_path)
|
||||
|
||||
def step4_update_mysql(version, config):
|
||||
"""步骤 4: 更新 MySQL 数据库 (通过 SSH 执行 Docker 命令)"""
|
||||
print_step(4, 8, "更新 MySQL 数据库")
|
||||
|
||||
# 注意:我们不再直接连接 MySQL,而是通过 SSH 发送 docker exec 命令
|
||||
# 这样用户不需要暴露 MySQL 端口,更安全
|
||||
|
||||
if not config:
|
||||
print_warning("未配置服务器信息,跳过数据库更新")
|
||||
return
|
||||
|
||||
try:
|
||||
# 连接 SSH
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
print(f" 连接服务器: {config['host']}")
|
||||
ssh.connect(
|
||||
hostname=config['host'],
|
||||
port=int(config.get('port', 22)),
|
||||
username=config['username'],
|
||||
password=config.get('password', ''),
|
||||
timeout=30
|
||||
)
|
||||
print_success("SSH 连接成功")
|
||||
|
||||
# 构造 SQL 语句
|
||||
# 更新 'default' 分组为最新版本 (更健壮,不依赖 ID=1)
|
||||
sql = f"UPDATE plugin_groups SET current_version_file = 'core-v{version}.zip' WHERE name = 'default';"
|
||||
|
||||
# 构造 Docker 命令
|
||||
# 假设容器名为 designercep_db,用户 designer_user,密码 DesignerPass123!,库名 designer_db
|
||||
# 这些应该与 docker-compose.yml 保持一致
|
||||
db_user = "designer_user"
|
||||
db_pass = "DesignerPass123!"
|
||||
db_name = "designer_db"
|
||||
container_name = "designercep_db"
|
||||
|
||||
print(f" 执行远程 Docker SQL 命令...")
|
||||
print(f" SQL: {sql}")
|
||||
|
||||
docker_cmd = f'docker exec -i {container_name} mysql -u{db_user} -p{db_pass} {db_name} -e "{sql}"'
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command(docker_cmd)
|
||||
exit_status = stdout.channel.recv_exit_status()
|
||||
|
||||
if exit_status == 0:
|
||||
print_success("数据库更新命令执行成功")
|
||||
print(f" 输出: {stdout.read().decode().strip()}")
|
||||
else:
|
||||
print_error(f"数据库更新失败 (Exit code: {exit_status})")
|
||||
print(f" 错误: {stderr.read().decode().strip()}")
|
||||
raise Exception("Docker exec failed")
|
||||
|
||||
ssh.close()
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"数据库更新失败: {e}")
|
||||
print_warning("您需要手动执行 SQL (进入容器执行):")
|
||||
print(f" {sql}")
|
||||
# 不抛出异常,以免打断流程,因为这步失败通常不影响文件上传
|
||||
|
||||
|
||||
def step5_clean_cache():
|
||||
"""步骤 5: 清除客户端缓存(测试用)"""
|
||||
print_step(5, 8, "清除客户端缓存(可选)")
|
||||
|
||||
cache_dir = Path.home() / "AppData" / "Roaming" / "DesignerCache"
|
||||
|
||||
if not cache_dir.exists():
|
||||
print_warning(f"缓存目录不存在: {cache_dir}")
|
||||
return
|
||||
|
||||
try:
|
||||
shutil.rmtree(cache_dir)
|
||||
print_success(f"缓存已清除: {cache_dir}")
|
||||
except Exception as e:
|
||||
print_warning(f"清除缓存失败: {e}")
|
||||
print(" 您可以手动删除缓存目录")
|
||||
|
||||
def step6_setup_config():
|
||||
"""步骤 6: 配置服务器信息(首次运行)"""
|
||||
print_step(6, 8, "配置服务器信息")
|
||||
|
||||
config = load_config()
|
||||
if config:
|
||||
print_warning("配置文件已存在,跳过配置")
|
||||
print(f" 配置文件: {CONFIG_FILE}")
|
||||
return config
|
||||
|
||||
print("请输入服务器配置信息:")
|
||||
print()
|
||||
|
||||
config = {}
|
||||
|
||||
# SSH 配置
|
||||
config['host'] = input(" 服务器地址: ").strip()
|
||||
config['port'] = input(" SSH 端口 [22]: ").strip() or "22"
|
||||
config['username'] = input(" SSH 用户名: ").strip()
|
||||
config['password'] = input(" SSH 密码: ").strip()
|
||||
config['remote_path'] = input(" 远程路径 [/var/www/DesignerCEP/Server/static]: ").strip() \
|
||||
or "/var/www/DesignerCEP/Server/static"
|
||||
|
||||
print()
|
||||
|
||||
# MySQL 配置
|
||||
use_mysql = input(" 是否配置 MySQL? (y/n) [y]: ").strip().lower()
|
||||
if use_mysql != 'n':
|
||||
config['mysql'] = {}
|
||||
config['mysql']['host'] = input(" MySQL 地址: ").strip()
|
||||
config['mysql']['port'] = input(" MySQL 端口 [3306]: ").strip() or "3306"
|
||||
config['mysql']['username'] = input(" MySQL 用户名: ").strip()
|
||||
config['mysql']['password'] = input(" MySQL 密码: ").strip()
|
||||
config['mysql']['database'] = input(" 数据库名: ").strip()
|
||||
config['mysql']['table'] = input(" 表名 [plugin_groups]: ").strip() or "plugin_groups"
|
||||
|
||||
# 保存配置
|
||||
save_config(config)
|
||||
|
||||
return config
|
||||
|
||||
def step7_summary(version, deployed):
|
||||
"""步骤 7: 发布总结"""
|
||||
print_step(7, 8, "发布总结")
|
||||
|
||||
print(f"""
|
||||
{GREEN}{'='*60}
|
||||
DesignerCEP 发布完成!
|
||||
{'='*60}{RESET}
|
||||
|
||||
[发布信息]
|
||||
版本号: {version}
|
||||
构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
部署状态: {'已部署到服务器' if deployed else '仅本地构建'}
|
||||
|
||||
[构建产物]
|
||||
Core: {DIST_CORE_DIR}
|
||||
Shell: {DIST_SHELL_DIR}
|
||||
Shell.zip: {DESIGNER_DIR / 'dist' / f'shell-{version}.zip'}
|
||||
|
||||
[后续步骤]
|
||||
1. {'✓' if deployed else '○'} 验证服务器文件
|
||||
2. {'✓' if deployed else '○'} 确认数据库版本
|
||||
3. ○ 测试客户端更新功能
|
||||
4. ○ 通知用户更新
|
||||
|
||||
[测试方法]
|
||||
1. 删除客户端缓存(已自动执行)
|
||||
2. 重启 Photoshop 插件
|
||||
3. 登录账号,检查是否下载新版本
|
||||
4. 验证功能是否正常
|
||||
|
||||
{GREEN}{'='*60}{RESET}
|
||||
""")
|
||||
|
||||
# ==================== 主函数 ====================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='DesignerCEP 自动发布脚本(Shell + Core)',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
示例:
|
||||
# 仅构建(不部署)
|
||||
python auto_deploy_core.py --version 1.0.6
|
||||
|
||||
# 构建并部署到服务器
|
||||
python auto_deploy_core.py --version 1.0.6 --deploy
|
||||
|
||||
# 构建、部署并更新数据库
|
||||
python auto_deploy_core.py --version 1.0.6 --deploy --update-db
|
||||
|
||||
# 首次运行,配置服务器信息
|
||||
python auto_deploy_core.py --version 1.0.6 --setup
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--version', '-v',
|
||||
required=True,
|
||||
help='版本号,例如: 1.0.6(不要加 v)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--deploy', '-d',
|
||||
action='store_true',
|
||||
help='部署到服务器'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--update-db',
|
||||
action='store_true',
|
||||
help='自动更新 MySQL 数据库'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--setup',
|
||||
action='store_true',
|
||||
help='配置服务器信息(首次运行)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--skip-clean',
|
||||
action='store_true',
|
||||
help='跳过清除缓存步骤'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
version = args.version
|
||||
|
||||
print(f"""
|
||||
{BLUE}{'='*60}
|
||||
DesignerCEP 自动发布脚本
|
||||
{'='*60}{RESET}
|
||||
|
||||
[配置信息]
|
||||
版本号: {version}
|
||||
项目根目录: {PROJECT_ROOT}
|
||||
Designer: {DESIGNER_DIR}
|
||||
部署到服务器: {'是' if args.deploy else '否'}
|
||||
更新数据库: {'是' if args.update_db else '否'}
|
||||
清除缓存: {'否' if args.skip_clean else '是'}
|
||||
|
||||
{BLUE}{'='*60}{RESET}
|
||||
""")
|
||||
|
||||
try:
|
||||
# 加载配置
|
||||
config = None
|
||||
if args.deploy or args.setup:
|
||||
config = load_config()
|
||||
if not config or args.setup:
|
||||
config = step6_setup_config()
|
||||
|
||||
# 执行发布步骤
|
||||
step1_build_frontend()
|
||||
step2_package_shell_zip(version)
|
||||
|
||||
if args.deploy:
|
||||
if not config:
|
||||
print_error("未找到配置文件,请先运行 --setup 配置服务器")
|
||||
sys.exit(1)
|
||||
|
||||
step3_upload_to_server(version, config)
|
||||
|
||||
if args.update_db:
|
||||
step4_update_mysql(version, config)
|
||||
else:
|
||||
print_step(4, 8, "更新数据库(已跳过)")
|
||||
print_warning("数据库未自动更新,请手动执行:")
|
||||
print(f" UPDATE plugin_groups SET current_version = '{version}';")
|
||||
else:
|
||||
print_step(3, 8, "上传到服务器(已跳过)")
|
||||
print_step(4, 8, "更新数据库(已跳过)")
|
||||
|
||||
if not args.skip_clean:
|
||||
step5_clean_cache()
|
||||
else:
|
||||
print_step(5, 8, "清除缓存(已跳过)")
|
||||
|
||||
step7_summary(version, args.deploy)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print_error("\n用户中断")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print_error(f"发布失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
243
AdminTool/deploy_core_only.py
Normal file
243
AdminTool/deploy_core_only.py
Normal file
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Core 快速发布脚本
|
||||
仅执行:构建 Core -> 上传 Core -> 更新数据库
|
||||
跳过 Shell 的构建和上传
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
import argparse
|
||||
import zipfile
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import paramiko
|
||||
|
||||
# ==================== 配置区域 ====================
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.absolute()
|
||||
DESIGNER_DIR = PROJECT_ROOT / "Designer"
|
||||
DIST_CORE_DIR = DESIGNER_DIR / "dist_core" # Core 构建输出目录
|
||||
CONFIG_FILE = Path(__file__).parent / "deploy_config.json"
|
||||
|
||||
# 颜色输出
|
||||
try:
|
||||
import colorama
|
||||
colorama.init()
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[93m'
|
||||
RED = '\033[91m'
|
||||
BLUE = '\033[94m'
|
||||
RESET = '\033[0m'
|
||||
except ImportError:
|
||||
GREEN = YELLOW = RED = BLUE = RESET = ''
|
||||
|
||||
# ==================== 工具函数 ====================
|
||||
|
||||
def print_step(message):
|
||||
print(f"\n{BLUE}{'='*60}{RESET}")
|
||||
print(f"{GREEN}{message}{RESET}")
|
||||
print(f"{BLUE}{'='*60}{RESET}\n")
|
||||
|
||||
def print_success(message):
|
||||
print(f"{GREEN}✓ {message}{RESET}")
|
||||
|
||||
def print_error(message):
|
||||
print(f"{RED}✗ {message}{RESET}")
|
||||
|
||||
def run_command(command, cwd=None):
|
||||
print(f" 执行: {command}")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=cwd,
|
||||
shell=True,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
errors='replace'
|
||||
)
|
||||
if result.stdout:
|
||||
print(f" 输出: {result.stdout.strip()[:200]}...") # 只打印前200字符
|
||||
return result
|
||||
except subprocess.CalledProcessError as e:
|
||||
print_error(f"命令执行失败: {e}")
|
||||
if e.stderr:
|
||||
print(f" 错误: {e.stderr}")
|
||||
sys.exit(1)
|
||||
|
||||
def load_config():
|
||||
if not CONFIG_FILE.exists():
|
||||
print_error("未找到配置文件 deploy_config.json")
|
||||
sys.exit(1)
|
||||
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
# ==================== 核心步骤 ====================
|
||||
|
||||
def step1_build_core():
|
||||
print_step("步骤 1: 构建 Core")
|
||||
|
||||
# 检查目录
|
||||
if not DESIGNER_DIR.exists():
|
||||
print_error(f"Designer 目录不存在: {DESIGNER_DIR}")
|
||||
sys.exit(1)
|
||||
|
||||
# 执行构建
|
||||
print(" 正在构建 Core (npm run build:core)...")
|
||||
os.chdir(DESIGNER_DIR)
|
||||
run_command("npm run build:core", cwd=DESIGNER_DIR)
|
||||
|
||||
# 验证
|
||||
if not DIST_CORE_DIR.exists():
|
||||
print_error(f"Core 构建失败,输出目录不存在: {DIST_CORE_DIR}")
|
||||
sys.exit(1)
|
||||
|
||||
print_success("Core 构建完成")
|
||||
|
||||
def step2_upload_core(version, config):
|
||||
print_step("步骤 2: 上传 Core 到服务器")
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
try:
|
||||
print(f" 连接服务器: {config['host']}")
|
||||
ssh.connect(
|
||||
hostname=config['host'],
|
||||
port=int(config.get('port', 22)),
|
||||
username=config['username'],
|
||||
password=config.get('password', ''),
|
||||
timeout=30
|
||||
)
|
||||
print_success("SSH 连接成功")
|
||||
|
||||
sftp = ssh.open_sftp()
|
||||
remote_base = config['remote_path']
|
||||
remote_core_dir = f"{remote_base}/core/{version}"
|
||||
|
||||
# 创建远程目录
|
||||
print(f" 创建远程目录: {remote_core_dir}")
|
||||
ssh.exec_command(f"mkdir -p {remote_core_dir}")
|
||||
|
||||
# 递归上传
|
||||
print(" 开始上传文件...")
|
||||
upload_count = 0
|
||||
for root, dirs, files in os.walk(DIST_CORE_DIR):
|
||||
relative_root = Path(root).relative_to(DIST_CORE_DIR)
|
||||
remote_root = f"{remote_core_dir}/{relative_root}".replace("\\", "/").rstrip("/")
|
||||
if str(relative_root) == ".":
|
||||
remote_root = remote_core_dir
|
||||
|
||||
# 确保子目录存在
|
||||
try:
|
||||
sftp.stat(remote_root)
|
||||
except IOError:
|
||||
sftp.mkdir(remote_root)
|
||||
|
||||
for file in files:
|
||||
local_file = Path(root) / file
|
||||
remote_file = f"{remote_root}/{file}"
|
||||
sftp.put(str(local_file), remote_file)
|
||||
upload_count += 1
|
||||
if upload_count % 10 == 0:
|
||||
print(f" 已上传 {upload_count} 个文件...", end='\r')
|
||||
|
||||
print(f"\n 共上传 {upload_count} 个文件")
|
||||
print_success("Core 上传完成")
|
||||
|
||||
sftp.close()
|
||||
ssh.close()
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"上传失败: {e}")
|
||||
raise
|
||||
|
||||
def step3_update_db(version, config):
|
||||
print_step("步骤 3: 更新数据库")
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
try:
|
||||
ssh.connect(
|
||||
hostname=config['host'],
|
||||
port=int(config.get('port', 22)),
|
||||
username=config['username'],
|
||||
password=config.get('password', ''),
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# 构造 SQL
|
||||
sql = f"UPDATE plugin_groups SET current_version_file = 'core-v{version}.zip' WHERE name = 'default';"
|
||||
|
||||
# 数据库配置
|
||||
db_conf = config.get('mysql', {})
|
||||
db_user = db_conf.get('username', 'designer_user')
|
||||
db_pass = db_conf.get('password', 'DesignerPass123!')
|
||||
db_name = db_conf.get('database', 'designer_db')
|
||||
container_name = "designercep_db"
|
||||
|
||||
print(f" 更新 'default' 分组版本为: core-v{version}.zip")
|
||||
docker_cmd = f'docker exec -i {container_name} mysql -u{db_user} -p{db_pass} {db_name} -e "{sql}"'
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command(docker_cmd)
|
||||
exit_status = stdout.channel.recv_exit_status()
|
||||
|
||||
if exit_status == 0:
|
||||
print_success("数据库更新成功")
|
||||
else:
|
||||
print_error(f"数据库更新失败: {stderr.read().decode().strip()}")
|
||||
raise Exception("DB Update Failed")
|
||||
|
||||
ssh.close()
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"数据库操作失败: {e}")
|
||||
raise
|
||||
|
||||
def step4_clean_cache():
|
||||
print_step("步骤 4: 清除本地缓存")
|
||||
cache_dir = Path.home() / "AppData" / "Roaming" / "DesignerCache"
|
||||
if cache_dir.exists():
|
||||
try:
|
||||
shutil.rmtree(cache_dir)
|
||||
print_success(f"已清除: {cache_dir}")
|
||||
except Exception as e:
|
||||
print_error(f"清除失败: {e}")
|
||||
else:
|
||||
print(" 无需清除")
|
||||
|
||||
# ==================== 主入口 ====================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Core 快速发布脚本')
|
||||
parser.add_argument('--version', '-v', required=True, help='版本号 (如 1.0.7)')
|
||||
args = parser.parse_args()
|
||||
|
||||
version = args.version
|
||||
|
||||
print(f"{BLUE}开始发布 Core - 版本: {version}{RESET}")
|
||||
|
||||
try:
|
||||
config = load_config()
|
||||
|
||||
step1_build_core()
|
||||
step2_upload_core(version, config)
|
||||
step3_update_db(version, config)
|
||||
step4_clean_cache()
|
||||
|
||||
print_step("🎉 发布全部完成!")
|
||||
print(f" 版本: {version}")
|
||||
print(f" 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"\n发布过程中止: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
69
AdminTool/fix_default_group.py
Normal file
69
AdminTool/fix_default_group.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import requests
|
||||
import sys
|
||||
|
||||
# Configuration from admin_gui.py
|
||||
API_URL = "https://backend.aidg168.uk/api/v1"
|
||||
TOKEN = "admin-secret-token"
|
||||
HEADERS = {"x-admin-token": TOKEN}
|
||||
|
||||
def fix_group():
|
||||
print(f"Connecting to {API_URL}...")
|
||||
|
||||
# 1. List groups
|
||||
try:
|
||||
resp = requests.get(f"{API_URL}/admin/groups", headers=HEADERS)
|
||||
resp.raise_for_status()
|
||||
groups = resp.json()
|
||||
except Exception as e:
|
||||
print(f"Error listing groups: {e}")
|
||||
print("Please check your network connection or server status.")
|
||||
return
|
||||
|
||||
print(f"Found {len(groups)} groups.")
|
||||
|
||||
target_group = None
|
||||
default_exists = False
|
||||
|
||||
for g in groups:
|
||||
print(f" - ID: {g['id']}, Name: {g['name']}, Comment: {g.get('comment')}")
|
||||
if g['name'] == '默认':
|
||||
target_group = g
|
||||
if g['name'] == 'default':
|
||||
default_exists = True
|
||||
|
||||
if default_exists:
|
||||
print("\n[OK] Group 'default' already exists.")
|
||||
if target_group:
|
||||
print("Warning: Group '默认' also exists. Please verify which one you want to keep.")
|
||||
else:
|
||||
print("Everything looks good.")
|
||||
return
|
||||
|
||||
if target_group:
|
||||
print(f"\nFound group '默认' (ID: {target_group['id']}). Renaming to 'default'...")
|
||||
try:
|
||||
url = f"{API_URL}/admin/groups/{target_group['id']}"
|
||||
# The API expects JSON body with fields to update
|
||||
resp = requests.put(url, json={"name": "default"}, headers=HEADERS)
|
||||
resp.raise_for_status()
|
||||
print("Success! Group renamed to 'default'.")
|
||||
except Exception as e:
|
||||
print(f"Error renaming group: {e}")
|
||||
if hasattr(e, 'response') and e.response:
|
||||
print(f"Server response: {e.response.text}")
|
||||
else:
|
||||
print("\nGroup '默认' not found and 'default' does not exist.")
|
||||
print("Creating 'default' group...")
|
||||
try:
|
||||
url = f"{API_URL}/admin/groups"
|
||||
payload = {"name": "default", "comment": "Default User Group"}
|
||||
resp = requests.post(url, json=payload, headers=HEADERS)
|
||||
resp.raise_for_status()
|
||||
print("Success! Group 'default' created.")
|
||||
except Exception as e:
|
||||
print(f"Error creating group: {e}")
|
||||
if hasattr(e, 'response') and e.response:
|
||||
print(f"Server response: {e.response.text}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
fix_group()
|
||||
5
AdminTool/requirements.txt
Normal file
5
AdminTool/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
PyQt5
|
||||
requests
|
||||
paramiko
|
||||
pymysql
|
||||
colorama
|
||||
Reference in New Issue
Block a user