Initial commit - DesignerCEP Project with Caddy deployment

This commit is contained in:
zuowei1216
2025-12-19 21:27:17 +08:00
commit 8ea58fe480
170 changed files with 47469 additions and 0 deletions

View 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.zipCEP 扩展下载)...")
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()