diff --git a/.cursorrules b/.cursorrules index 7ac6124d..c619cba0 100644 --- a/.cursorrules +++ b/.cursorrules @@ -58,30 +58,18 @@ 2. **部署到小型宝塔服务器** ```bash - # 压缩项目 - cd /Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验 - tar --exclude='node_modules' --exclude='.next' --exclude='.git' -czf /tmp/soul_update.tar.gz . - - # 上传到服务器 - sshpass -p 'Zhiqun1984' scp /tmp/soul_update.tar.gz root@42.194.232.22:/tmp/ - - # SSH部署 - sshpass -p 'Zhiqun1984' ssh root@42.194.232.22 " - cd /www/wwwroot/soul - rm -rf app components lib public styles *.json *.js *.ts *.mjs *.md .next - tar -xzf /tmp/soul_update.tar.gz - rm /tmp/soul_update.tar.gz - export PATH=/www/server/nodejs/v22.14.0/bin:\$PATH - pnpm install - pnpm run build - pm2 restart soul - " + # 在项目根目录执行(本地打包 + SSH 上传 + 宝塔 API 重启) + pip install -r requirements-deploy.txt + python scripts/devlop.py ``` + - 详见 `DEPLOYMENT.md`、`开发文档/8、部署/当前项目部署到线上.md` 4. **上传小程序** ```bash - /Applications/wechatwebdevtools.app/Contents/MacOS/cli upload --project "./miniprogram" -v "版本号" -d "描述" + # 项目根目录一键上传(将 miniprogram/ 代码完整上传到微信公众平台) + python scripts/autosysc-weixin.py ``` + - 需先在 miniprogram/ 下放置 private.key(公众号后台「开发设置」→ 小程序代码上传密钥)。详见 `开发文档/8、部署/当前项目部署到线上.md`。 5. **打开微信公众平台** ```bash diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 07150b50..e5e55c6e 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -23,68 +23,69 @@ | Docker | `Dockerfile` | Next.js 独立构建(`output: 'standalone'`) | | Docker 编排 | `docker-compose.yml` | 整站容器、端口 3000、支付/基础环境变量 | | Next 配置 | `next.config.mjs` | `output: 'standalone'` 供 Docker 使用 | -| 宝塔一键部署 | `scripts/deploy-to-server.sh` | SSH 到宝塔服务器,拉代码、安装依赖、构建、PM2 重启 | +| **宝塔部署(统一入口)** | **`scripts/devlop.py`** | **本地打包 → SSH 上传解压 → 宝塔 API 重启 Node 项目(Windows/Mac/Linux 通用)** | +| 宝塔 API 模块 | `scripts/deploy_baota_pure_api.py` | 被 devlop.py 内部调用(重启 Node);也可单独用于仅重启或触发计划任务 | | 宝塔自动化 | `开发文档/8、部署/Next.js自动化部署流程.md` | GitHub Webhook + 宝塔,推送即自动部署 | | NAS 部署 | `deploy_to_nas.sh`、`redeploy.sh`、`quick_deploy.sh` | 部署到 NAS / 内网环境 | -| **宝塔部署(跨平台)** | **`scripts/deploy_baota.py`** | **Python 脚本,Windows/Mac/Linux 通用,不依赖 .sh 或 sshpass** | 无 `vercel.json` 时,Vercel 会按默认规则部署本仓库;若需自定义路由或头信息,可再加 `vercel.json`。 --- -## 宝塔部署(Python 跨平台) +## 宝塔部署(统一使用 devlop.py) -本项目在 Mac 上开发,原有一键部署脚本为 `scripts/deploy-to-server.sh`(依赖 sshpass,仅 Linux/Mac)。为在 **Windows / Mac / Linux** 上都能部署到宝塔,提供了 **Python 脚本**,不依赖 shell 或 sshpass。 +**日常部署**统一使用 **`scripts/devlop.py`**:本地打包 → SSH 上传解压 → 宝塔 API 重启,Windows / Mac / Linux 通用,不依赖 sshpass 或 shell。 ### 1. 安装依赖 \`\`\`bash -pip install paramiko +pip install -r requirements-deploy.txt \`\`\` -### 2. 配置服务器信息 +### 2. 配置(可选) -复制示例配置并填写真实信息(**不要提交到 Git**): +脚本默认使用 `.cursorrules` 中的服务器信息(42.194.232.22、root、项目路径 /www/wwwroot/soul 等)。如需覆盖,可设置环境变量: -\`\`\`bash -cp scripts/deploy_config.example.json deploy_config.json -# 编辑 deploy_config.json,填写 server_host、server_user、project_path、branch、pm2_app_name 等 -\`\`\` - -或使用环境变量(不写配置文件时,脚本会提示输入密码): - -- `DEPLOY_HOST`:服务器 IP -- `DEPLOY_USER`:SSH 用户名(如 root) -- `DEPLOY_PROJECT_PATH`:服务器上项目路径(如 /www/wwwroot/soul) -- `DEPLOY_BRANCH`:要部署的分支(如 soul-content) -- `DEPLOY_PM2_APP`:PM2 应用名(如 soul) -- `DEPLOY_SSH_KEY`:SSH 私钥路径(可选,不填则用密码) +- `DEPLOY_HOST`、`DEPLOY_USER`、`DEPLOY_PASSWORD` 或 `DEPLOY_SSH_KEY` +- `DEPLOY_PROJECT_PATH`(如 /www/wwwroot/soul) +- `BAOTA_PANEL_URL`、`BAOTA_API_KEY` +- `DEPLOY_PM2_APP`(默认 soul) ### 3. 执行部署 在**项目根目录**执行: \`\`\`bash -python scripts/deploy_baota.py -# 或指定配置 -python scripts/deploy_baota.py --config scripts/deploy_config.json -# 仅查看将要执行的步骤(不连接) -python scripts/deploy_baota.py --dry-run +python scripts/devlop.py \`\`\` -脚本会依次执行:SSH 连接 → 拉取代码 → 安装依赖 → 构建 → PM2 重启。部署完成后访问: +- **流程**:本地 `pnpm build` → 打包 `.next/standalone`(含 static、public、ecosystem.config.cjs)→ SSH 上传并解压到服务器 → **宝塔 API 重启 Node 项目**。 +- **参数**:`--no-build` 跳过构建;`--no-upload` 仅构建+打包;`--no-api` 上传后不调 API 重启。 + +部署完成后访问: - 前台:`https://soul.quwanzhi.com` - 后台:`https://soul.quwanzhi.com/admin` -### 4. 首次在宝塔上准备 +### 4. 仅重启 Node(不上传代码) + +若只需在宝塔上重启 Node 项目(代码已通过其他方式更新),可单独使用宝塔 API 模块: + +\`\`\`bash +pip install requests +python scripts/deploy_baota_pure_api.py # 重启 Node 项目 soul +python scripts/deploy_baota_pure_api.py --create-dir # 并创建项目目录 +python scripts/deploy_baota_pure_api.py --task-id 1 # 触发计划任务 ID=1 +\`\`\` + +### 5. 首次在宝塔上准备 若服务器上尚未有代码,需先在宝塔上: -1. 在网站目录(如 `/www/wwwroot/soul`)执行 `git clone <你的仓库> .`,或从本地上传代码。 -2. 在宝塔「PM2 管理器」中新增项目:项目目录选该路径,启动文件为 `node_modules/next/dist/bin/next` 或 `node server.js`(若使用 standalone 输出),启动参数为 `start -p 3006`(与 `package.json` 里 `start` 端口一致)。 -3. 配置 Nginx 反向代理到该端口,并绑定域名。 -4. 之后即可用 `python scripts/deploy_baota.py` 做日常拉代码、构建、重启。 +1. 在网站目录(如 `/www/wwwroot/soul`)创建目录,或从本地上传/克隆代码。 +2. 在宝塔「PM2 管理器」中新增项目:项目目录选该路径,启动文件为 `node server.js`,环境变量 `PORT=3006`。 +3. 配置 Nginx 反向代理到 `127.0.0.1:3006`,并绑定域名 soul.quwanzhi.com。 +4. 之后日常部署执行 `python scripts/devlop.py` 即可。 --- @@ -192,7 +193,7 @@ npm run dev 2. **以管理员身份运行终端再执行构建** - 右键 Cursor/终端 → “以管理员身份运行”,在项目根目录执行 `pnpm build`。 -若只做部署、不在本机打 standalone 包,可直接用 `python scripts/deploy_baota.py`,构建会在**服务器(Linux)**上执行,不会遇到该问题。 +若只做部署、不在本机打 standalone 包,可用 `python scripts/devlop.py --no-build` 跳过构建后上传已有包,或由服务器/计划任务在服务器上执行构建。 ## 注意事项 diff --git a/miniprogram/上传小程序.py b/miniprogram/上传小程序.py deleted file mode 100644 index a9efcaff..00000000 --- a/miniprogram/上传小程序.py +++ /dev/null @@ -1,296 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Soul创业派对 - 小程序自动上传脚本 -使用Python调用微信开发者工具CLI上传小程序 -""" - -import os -import sys -import subprocess -import json -from pathlib import Path -from datetime import datetime - -# 配置信息 -CONFIG = { - 'appid': 'wxb8bbb2b10dec74aa', - 'project_path': Path(__file__).parent.absolute(), - 'version': '1.0.0', - 'desc': 'Soul创业派对 - 首次发布', -} - -# 微信开发者工具CLI可能的路径 -CLI_PATHS = [ - r"D:\微信web开发者工具\cli.bat", - r"C:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat", - r"C:\Program Files\Tencent\微信web开发者工具\cli.bat", - os.path.join(os.environ.get('LOCALAPPDATA', ''), '微信web开发者工具', 'cli.bat'), -] - - -def print_banner(): - """打印横幅""" - print("\n" + "=" * 60) - print(" 🚀 Soul创业派对 - 小程序自动上传") - print("=" * 60 + "\n") - - -def find_cli(): - """查找微信开发者工具CLI""" - print("🔍 正在查找微信开发者工具...") - - for cli_path in CLI_PATHS: - if os.path.exists(cli_path): - print(f"✅ 找到CLI: {cli_path}\n") - return cli_path - - print("❌ 未找到微信开发者工具CLI") - print("\n请确保已安装微信开发者工具,并开启服务端口:") - print(" 1. 打开微信开发者工具") - print(" 2. 设置 → 安全设置") - print(" 3. 勾选「开启服务端口」\n") - return None - - -def check_private_key(): - """检查上传密钥""" - key_path = CONFIG['project_path'] / 'private.key' - - if not key_path.exists(): - print("❌ 未找到上传密钥文件 private.key\n") - print("📥 请按以下步骤获取密钥:") - print(" 1. 访问 https://mp.weixin.qq.com/") - print(" 2. 登录小程序后台") - print(" 3. 开发管理 → 开发设置 → 小程序代码上传密钥") - print(" 4. 点击「生成」,下载密钥文件") - print(" 5. 将 private.*.key 重命名为 private.key") - print(f" 6. 放到目录: {CONFIG['project_path']}\n") - return False - - print(f"✅ 找到密钥文件: private.key\n") - return True - - -def check_node_installed(): - """检查Node.js是否安装""" - try: - result = subprocess.run(['node', '--version'], - capture_output=True, - text=True) - if result.returncode == 0: - print(f"✅ Node.js版本: {result.stdout.strip()}") - return True - except FileNotFoundError: - pass - - print("❌ 未找到Node.js") - print("\n请先安装Node.js: https://nodejs.org/\n") - return False - - -def check_miniprogram_ci(): - """检查miniprogram-ci是否安装""" - print("\n🔍 检查上传工具...") - - node_modules = CONFIG['project_path'].parent / 'node_modules' / 'miniprogram-ci' - - if node_modules.exists(): - print("✅ miniprogram-ci已安装\n") - return True - - print("⚠️ miniprogram-ci未安装") - print("\n正在安装miniprogram-ci...") - - try: - # 切换到项目根目录安装 - parent_dir = CONFIG['project_path'].parent - result = subprocess.run( - ['npm', 'install', 'miniprogram-ci', '--save-dev'], - cwd=parent_dir, - capture_output=True, - text=True - ) - - if result.returncode == 0: - print("✅ miniprogram-ci安装成功\n") - return True - else: - print(f"❌ 安装失败: {result.stderr}") - return False - - except Exception as e: - print(f"❌ 安装出错: {e}") - return False - - -def upload_with_nodejs(): - """使用Node.js脚本上传""" - print("📦 使用Node.js上传...") - print(f"📂 项目路径: {CONFIG['project_path']}") - print(f"🆔 AppID: {CONFIG['appid']}") - print(f"📌 版本号: {CONFIG['version']}") - print(f"📝 描述: {CONFIG['desc']}\n") - - upload_js = CONFIG['project_path'] / 'upload.js' - - if not upload_js.exists(): - print(f"❌ 未找到上传脚本: {upload_js}") - return False - - try: - print("⏳ 正在上传代码...\n") - - result = subprocess.run( - ['node', str(upload_js)], - cwd=CONFIG['project_path'], - capture_output=True, - text=True, - timeout=300 # 5分钟超时 - ) - - # 显示输出 - if result.stdout: - print(result.stdout) - - if result.returncode == 0: - print("\n" + "=" * 60) - print("✅ 上传成功!") - print("=" * 60) - print("\n📱 下一步:") - print(" 1. 访问 https://mp.weixin.qq.com/") - print(" 2. 登录小程序后台") - print(" 3. 版本管理 → 开发版本 → 提交审核") - print("=" * 60 + "\n") - return True - else: - print(f"\n❌ 上传失败") - if result.stderr: - print(f"错误信息: {result.stderr}") - return False - - except subprocess.TimeoutExpired: - print("❌ 上传超时(超过5分钟)") - return False - except Exception as e: - print(f"❌ 上传出错: {e}") - return False - - -def upload_with_cli(cli_path): - """使用微信开发者工具CLI上传""" - print("📦 使用微信开发者工具CLI上传...") - print(f"📂 项目路径: {CONFIG['project_path']}") - print(f"🆔 AppID: {CONFIG['appid']}") - print(f"📌 版本号: {CONFIG['version']}") - print(f"📝 描述: {CONFIG['desc']}\n") - - key_path = CONFIG['project_path'] / 'private.key' - - try: - print("⏳ 正在上传代码...\n") - - # 构建上传命令 - cmd = [ - cli_path, - 'upload', - '--project', str(CONFIG['project_path']), - '--version', CONFIG['version'], - '--desc', CONFIG['desc'], - '--pkp', str(key_path) - ] - - result = subprocess.run( - cmd, - capture_output=True, - text=True, - timeout=300, # 5分钟超时 - encoding='utf-8', - errors='ignore' - ) - - # 显示输出 - if result.stdout: - print(result.stdout) - - if result.returncode == 0 or '成功' in result.stdout: - print("\n" + "=" * 60) - print("✅ 上传成功!") - print("=" * 60) - print("\n📱 下一步:") - print(" 1. 访问 https://mp.weixin.qq.com/") - print(" 2. 登录小程序后台") - print(" 3. 版本管理 → 开发版本 → 提交审核") - print("=" * 60 + "\n") - return True - else: - print(f"\n❌ 上传失败") - if result.stderr: - print(f"错误信息: {result.stderr}") - return False - - except subprocess.TimeoutExpired: - print("❌ 上传超时(超过5分钟)") - return False - except Exception as e: - print(f"❌ 上传出错: {e}") - return False - - -def main(): - """主函数""" - print_banner() - - # 检查必要条件 - print("🔍 检查上传条件...\n") - - # 1. 检查密钥 - if not check_private_key(): - sys.exit(1) - - # 2. 检查Node.js - has_node = check_node_installed() - - # 3. 查找CLI - cli_path = find_cli() - - # 如果没有Node.js也没有CLI,退出 - if not has_node and not cli_path: - print("❌ 无法上传:需要Node.js或微信开发者工具CLI") - sys.exit(1) - - print("\n" + "-" * 60 + "\n") - - # 优先使用Node.js方式(更稳定) - if has_node: - if check_miniprogram_ci(): - if upload_with_nodejs(): - sys.exit(0) - else: - print("\n⚠️ Node.js上传失败,尝试使用CLI...\n") - - # 备选:使用CLI - if cli_path: - if upload_with_cli(cli_path): - sys.exit(0) - - print("\n❌ 所有上传方式都失败了") - print("\n💡 建议:") - print(" 1. 确保微信开发者工具已打开") - print(" 2. 确保已开启「服务端口」") - print(" 3. 确保private.key文件正确") - print(" 4. 或手动使用微信开发者工具上传\n") - sys.exit(1) - - -if __name__ == '__main__': - try: - main() - except KeyboardInterrupt: - print("\n\n⚠️ 用户取消上传") - sys.exit(1) - except Exception as e: - print(f"\n❌ 发生错误: {e}") - import traceback - traceback.print_exc() - sys.exit(1) diff --git a/miniprogram/快速上传.bat b/miniprogram/快速上传.bat deleted file mode 100644 index 5b86af72..00000000 --- a/miniprogram/快速上传.bat +++ /dev/null @@ -1,29 +0,0 @@ -@echo off -chcp 65001 >nul -echo. -echo ======================================== -echo Soul创业派对 - 快速上传小程序 -echo ======================================== -echo. - -REM 检查Python -python --version >nul 2>&1 -if errorlevel 1 ( - echo ❌ 未找到Python - echo. - echo 请先安装Python: https://www.python.org/ - echo. - pause - exit /b 1 -) - -echo ✅ Python已安装 -echo. - -REM 运行上传脚本 -echo 🚀 开始上传... -echo. -python "%~dp0上传小程序.py" - -echo. -pause diff --git a/miniprogram/编译小程序.bat b/miniprogram/编译小程序.bat deleted file mode 100644 index 3647cbb2..00000000 --- a/miniprogram/编译小程序.bat +++ /dev/null @@ -1,74 +0,0 @@ -@echo off -chcp 65001 >nul -echo ================================== -echo Soul派对小程序 - 编译脚本 -echo ================================== -echo. - -:: 设置项目路径 -set "PROJECT_PATH=%~dp0" -set "PROJECT_PATH=%PROJECT_PATH:~0,-1%" - -:: 微信开发者工具可能的安装路径 -set "CLI1=C:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat" -set "CLI2=C:\Program Files\Tencent\微信web开发者工具\cli.bat" -set "CLI3=%LOCALAPPDATA%\微信web开发者工具\cli.bat" - -:: 查找CLI -set "CLI=" -if exist "%CLI1%" set "CLI=%CLI1%" -if exist "%CLI2%" set "CLI=%CLI2%" -if exist "%CLI3%" set "CLI=%CLI3%" - -if "%CLI%"=="" ( - echo ❌ 未找到微信开发者工具CLI - echo. - echo 请手动操作: - echo 1. 打开微信开发者工具 - echo 2. 点击"导入项目" - echo 3. 选择目录: %PROJECT_PATH% - echo 4. 点击"编译"按钮 - echo. - pause - exit /b 1 -) - -echo ✅ 找到微信开发者工具: %CLI% -echo 项目路径: %PROJECT_PATH% -echo. - -:: 1. 打开项目 -echo 📂 步骤1:打开项目... -call "%CLI%" open --project "%PROJECT_PATH%" -timeout /t 3 /nobreak >nul -echo ✅ 项目已打开 -echo. - -:: 2. 编译项目 -echo 🔨 步骤2:编译项目... -call "%CLI%" build-npm --project "%PROJECT_PATH%" -timeout /t 2 /nobreak >nul -echo ✅ 编译完成 -echo. - -:: 3. 生成预览二维码 -echo 📱 步骤3:生成预览二维码... -call "%CLI%" preview --project "%PROJECT_PATH%" --qr-format image --qr-output "%PROJECT_PATH%\preview.png" -if exist "%PROJECT_PATH%\preview.png" ( - echo ✅ 二维码已生成: %PROJECT_PATH%\preview.png - start "" "%PROJECT_PATH%\preview.png" -) else ( - echo ⚠️ 二维码生成失败,请在开发者工具中手动点击"预览" -) -echo. - -echo ================================== -echo 🎉 编译完成! -echo ================================== -echo. -echo 下一步操作: -echo 1. 在模拟器中查看效果 -echo 2. 点击"预览"生成二维码,用微信扫码测试 -echo 3. 点击"上传"提交到微信后台 -echo. -pause diff --git a/miniprogram/编译小程序.ps1 b/miniprogram/编译小程序.ps1 deleted file mode 100644 index 78404610..00000000 --- a/miniprogram/编译小程序.ps1 +++ /dev/null @@ -1,94 +0,0 @@ -# Soul派对小程序 - Windows编译脚本 - -Write-Host "==================================" -ForegroundColor Cyan -Write-Host " Soul派对小程序 - 编译脚本" -ForegroundColor Cyan -Write-Host "==================================" -ForegroundColor Cyan -Write-Host "" - -# 设置项目路径 -$ProjectPath = Split-Path -Parent $MyInvocation.MyCommand.Path - -# 微信开发者工具可能的安装路径(优先使用 D 盘) -$cliPaths = @( - "D:\微信web开发者工具\cli.bat", - "C:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat", - "C:\Program Files\Tencent\微信web开发者工具\cli.bat", - "$env:LOCALAPPDATA\微信web开发者工具\cli.bat" -) - -# 查找CLI -$cli = $null -foreach ($path in $cliPaths) { - if (Test-Path $path) { - $cli = $path - break - } -} - -if (-not $cli) { - Write-Host "未找到微信开发者工具CLI" -ForegroundColor Yellow - Write-Host "" - Write-Host "请手动操作:" -ForegroundColor Cyan - Write-Host "1. 打开微信开发者工具" -ForegroundColor White - Write-Host "2. 点击 '导入项目'" -ForegroundColor White - Write-Host "3. 选择目录: $ProjectPath" -ForegroundColor White - Write-Host "4. 点击 '编译' 按钮" -ForegroundColor White - Write-Host "" - - # 尝试启动微信开发者工具 - $devToolsPaths = @( - "C:\Program Files (x86)\Tencent\微信web开发者工具\微信开发者工具.exe", - "C:\Program Files\Tencent\微信web开发者工具\微信开发者工具.exe" - ) - - foreach ($toolPath in $devToolsPaths) { - if (Test-Path $toolPath) { - Write-Host "正在启动微信开发者工具..." -ForegroundColor Green - Start-Process $toolPath - break - } - } - - exit 1 -} - -Write-Host "找到微信开发者工具: $cli" -ForegroundColor Green -Write-Host "项目路径: $ProjectPath" -ForegroundColor Gray -Write-Host "" - -# 1. 打开项目 -Write-Host "步骤1:打开项目..." -ForegroundColor Cyan -& cmd /c "`"$cli`" open --project `"$ProjectPath`"" -Start-Sleep -Seconds 3 -Write-Host "项目已打开" -ForegroundColor Green -Write-Host "" - -# 2. 编译项目 -Write-Host "步骤2:编译项目..." -ForegroundColor Cyan -& cmd /c "`"$cli`" build-npm --project `"$ProjectPath`"" -Start-Sleep -Seconds 2 -Write-Host "编译完成" -ForegroundColor Green -Write-Host "" - -# 3. 生成预览二维码 -Write-Host "步骤3:生成预览二维码..." -ForegroundColor Cyan -$previewPath = Join-Path $ProjectPath "preview.png" -& cmd /c "`"$cli`" preview --project `"$ProjectPath`" --qr-format image --qr-output `"$previewPath`"" - -if (Test-Path $previewPath) { - Write-Host "二维码已生成: $previewPath" -ForegroundColor Green - Start-Process $previewPath -} else { - Write-Host "二维码生成失败,请在开发者工具中手动点击'预览'" -ForegroundColor Yellow -} -Write-Host "" - -Write-Host "==================================" -ForegroundColor Cyan -Write-Host " 编译完成!" -ForegroundColor Green -Write-Host "==================================" -ForegroundColor Cyan -Write-Host "" -Write-Host "下一步操作:" -ForegroundColor Cyan -Write-Host "1. 在模拟器中查看效果" -ForegroundColor White -Write-Host "2. 点击'预览'生成二维码,用微信扫码测试" -ForegroundColor White -Write-Host "3. 点击'上传'提交到微信后台" -ForegroundColor White -Write-Host "" diff --git a/requirements-deploy.txt b/requirements-deploy.txt index 1206e919..c57c6708 100644 --- a/requirements-deploy.txt +++ b/requirements-deploy.txt @@ -1,3 +1,4 @@ # 仅用于「部署到宝塔」脚本,非项目运行依赖 # 使用: pip install -r requirements-deploy.txt paramiko>=2.9.0 +requests>=2.28.0 diff --git a/scripts/Web转小程序并上传-提示词.md b/scripts/Web转小程序并上传-提示词.md new file mode 100644 index 00000000..00d389c1 --- /dev/null +++ b/scripts/Web转小程序并上传-提示词.md @@ -0,0 +1,101 @@ +# Web 转小程序并上传 - 完整流程提示词 + +> **用法**:在对话中 @ 本文件(`scripts/Web转小程序并上传-提示词.md`),请 AI 按本提示词把**整个流程**处理完:先按当前 Web 项目 100% 完整转为小程序代码到 `miniprogram/`,再执行上传脚本。 + +--- + +## 一、你的任务(当被 @ 本文件时) + +1. **转换**:将当前项目中的 **Web 端(Next.js)** 页面与功能,**完整、一致**地转为微信小程序代码,输出到 **`miniprogram/`** 目录。保持与现有小程序规范一致(WXML/WXSS/JS、app.json、project.config.json)。 +2. **检查**:转换完成后,自检 `miniprogram/` 是否可运行(必要文件、路由、API 调用、样式与交互一致)。 +3. **上传**:提示用户在项目根目录执行 **`python scripts/autosysc-weixin.py`**,或由你代为执行该命令,完成代码上传到微信公众平台。 + +若用户只要求「转换」或只要求「上传」,则只执行对应步骤;若用户说「完整流程」或 @ 本文件且无特别说明,则执行上述三步。 + +--- + +## 二、项目结构对照(按规则推导,不写死页面) + +**转换时请扫描当前仓库的 `app/` 目录,按以下规则生成小程序结构;有新增页面时同样适用。** + +1. **Web 路由 → 小程序页面路径** + - 规则:`app//page.tsx` → 小程序页面路径 `pages//`,其中 `` 取该路由的**最后一层目录名**(或约定名称,见下)。 + - 特例: + - `app/page.tsx`(根首页)→ `pages/index/index`。 + - `app//[id]/page.tsx` 等动态路由 → `pages//`,页面内通过 `onLoad(options)` 的 `options.id` 等取参数。 + - 嵌套路由(如 `app/my/referral/page.tsx`)→ 小程序侧通常用单层页面名,如 `pages/referral/referral`(以功能命名,避免深层路径)。 + - **新增页面**:在 `app/` 下每增加一个需在小程序展示的路由(如 `app/xxx/page.tsx`),就应在 `miniprogram/pages/` 下新增 `pages/xxx/xxx` 四件套(.js/.json/.wxml/.wxss),并在 `app.json` 的 `pages` 中追加 `"pages/xxx/xxx"`。 + +2. **如何枚举要转换的页面** + - 遍历 `app/` 下所有包含 `page.tsx` 的路径(排除 `app/api/`、`app/admin/` 等仅 Web 或后台用的路由,除非明确要上小程序)。 + - 对每个需上小程序的页面,按上面规则得到小程序页面路径,确保在 `miniprogram/pages/` 存在对应目录及四件套,并在 `app.json` 的 `pages` 中注册。 + +3. **API** + - Web 的 `app/api/*` 不转为小程序代码;小程序通过 `wx.request` 调用**同域名**接口(如 `https://soul.quwanzhi.com/api/...`),与 `miniprogram/utils` 或现有 baseURL 配置一致,不写死 localhost。 + +4. **tabBar** + - 仅对需要在底部 tab 展示的页面(如首页、目录、找伙伴、我的)配置 `app.json` 的 `tabBar.list`;其余为普通页面。若项目后续新增 tab,在 `tabBar.list` 中追加一项并在 `pages` 中注册该页。 + +--- + +## 三、转换规则(Web → 小程序) + +1. **组件与语法** + - React/JSX → WXML(`wx:if`、`wx:for`、`bindtap` 等)。 + - Tailwind/CSS 模块 → WXSS(可保留 class 名,样式写到 `.wxss` 或页面/组件内)。 + - `useState`/`useEffect` → 小程序 Page 的 `data`、`onLoad`、`onShow` 等。 + - 路由:`useRouter`/`Link` → `wx.navigateTo`、`wx.switchTab`(tab 页用 switchTab)。 +2. **页面与路由** + - 新增/缺失的 Web 页面要在 `miniprogram/app.json` 的 `pages` 中注册,并在 `miniprogram/pages/` 下建立对应目录及四件套(.js/.json/.wxml/.wxss)。 + - tabBar 页:保持与现有 `app.json` 的 `tabBar.list` 一致(首页、目录、找伙伴、我的)。 +3. **接口与数据** + - 请求统一走 `wx.request`,baseURL 用项目已有配置(如 `getApp().globalData.baseUrl` 或 `utils` 里封装的 request)。 + - 与 Web 共用的接口:路径、参数、返回格式与 `app/api/` 保持一致。 +4. **样式与资源** + - 图片:放 `miniprogram/images/` 或现有 assets 目录,引用路径用相对路径或 `/images/xxx`。 + - 主题色/字体:与 Web 的 `globals.css` 或设计一致,在 WXSS 中实现。 + +--- + +## 四、必须保留的小程序配置 + +- **AppID**:`wxb8bbb2b10dec74aa`(见 `miniprogram/project.config.json`、`.cursorrules`)。 +- **app.json**:`pages`、`window`、`tabBar`(含 `custom: true` 时保留 `custom-tab-bar`)、`permission`、`requiredPrivateInfos` 等按现有或微信规范保留。 +- **project.config.json**:保持现有编译与项目配置,不随意改 appid。 +- **custom-tab-bar**:若使用自定义 tabBar,保留 `custom-tab-bar` 组件实现。 + +--- + +## 五、转换完成后的自检清单 + +- [ ] `app.json` 中所有 `pages` 在 `miniprogram/pages/` 下均有对应目录及四件套。 +- [ ] 每个页面的 `.json` 中 `usingComponents` 与自定义组件引用正确(若有)。 +- [ ] 无语法错误:WXML 闭合、WXSS 合法、JS 中 Page() 注册正确。 +- [ ] 接口 baseURL 为线上或配置项,非 localhost(除非仅本地调试)。 +- [ ] tabBar 与导航与 Web 端一级入口一致(首页、目录、找伙伴、我的)。 + +--- + +## 六、上传步骤(你或用户执行) + +1. 确认 **`miniprogram/private.key`** 已存在(微信公众平台 → 开发管理 → 开发设置 → 小程序代码上传密钥)。 +2. 在**项目根目录**执行: + ```bash + python scripts/autosysc-weixin.py + ``` +3. 上传成功后,到微信公众平台「版本管理 → 开发版本」提交审核。 + +若本机未配置 `private.key`,在提示词执行结果中明确说明「请先配置 private.key 后再执行上传脚本」。 + +--- + +## 七、参考文件位置 + +- Web 页面:`app/**/page.tsx`、`components/` +- 小程序现有结构:`miniprogram/app.json`、`miniprogram/pages/`、`miniprogram/utils/`、`miniprogram/custom-tab-bar/` +- 上传脚本:`scripts/autosysc-weixin.py`(项目根运行) +- 小程序配置说明:`miniprogram/小程序快速配置指南.md`、`miniprogram/小程序部署说明.md` + +--- + +**当你被 @ 本文件时**:按「一、你的任务」执行完整流程(转换 → 检查 → 上传提示/执行),并严格遵循二~五的对照与规则。 diff --git a/scripts/__pycache__/deploy_baota_pure_api.cpython-311.pyc b/scripts/__pycache__/deploy_baota_pure_api.cpython-311.pyc new file mode 100644 index 00000000..26bc9e53 Binary files /dev/null and b/scripts/__pycache__/deploy_baota_pure_api.cpython-311.pyc differ diff --git a/scripts/autosysc-weixin.py b/scripts/autosysc-weixin.py new file mode 100644 index 00000000..d7562b63 --- /dev/null +++ b/scripts/autosysc-weixin.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Soul 创业派对 - 小程序一键上传(项目根目录运行) + +将**当前仓库 miniprogram/ 目录的小程序代码**完整上传到微信公众平台。 +AppID、项目路径等见 miniprogram/project.config.json 或 .cursorrules。 + +说明: +- 本脚本是「把仓库里已有小程序代码原样上传」,不是「把 Web 站转成小程序」。 +- Web(Next.js)与小程序(WXML/WXSS)是两套技术栈,无法 1:1 自动转换; + 本仓库已提供原生小程序实现(miniprogram/),本脚本负责将其上传到公众号后台。 + +使用(在项目根目录): + python scripts/autosysc-weixin.py + # 版本号与描述在 miniprogram/upload.js 或 上传小程序.py 的 CONFIG 中修改 + +前置条件: +- miniprogram/private.key:在微信公众平台「开发管理 → 开发设置 → 小程序代码上传密钥」生成并下载,重命名为 private.key 放到 miniprogram/ 下。 +- 已安装微信开发者工具(可选,用于 CLI 方式)或 Node.js + miniprogram-ci(用于 CI 方式)。 +""" + +from __future__ import print_function + +import sys +import json +import subprocess +import argparse +from pathlib import Path + +# 项目根目录 +ROOT = Path(__file__).resolve().parent.parent +MINIPROGRAM_DIR = ROOT / "miniprogram" +UPLOAD_SCRIPT = MINIPROGRAM_DIR / "上传小程序.py" + + +def get_appid(): + """从 miniprogram/project.config.json 或 .cursorrules 读取 AppID""" + config_file = MINIPROGRAM_DIR / "project.config.json" + if config_file.exists(): + try: + with open(config_file, "r", encoding="utf-8") as f: + data = json.load(f) + appid = data.get("appid", "").strip() + if appid: + return appid + except Exception: + pass + # 与 .cursorrules 中一致 + return "wxb8bbb2b10dec74aa" + + +def check_miniprogram(): + """检查 miniprogram 目录和必要文件""" + if not MINIPROGRAM_DIR.is_dir(): + print("❌ 未找到 miniprogram 目录: %s" % MINIPROGRAM_DIR) + return False + app_json = MINIPROGRAM_DIR / "app.json" + if not app_json.exists(): + print("❌ miniprogram 目录下未找到 app.json,请确认是否为小程序项目根目录") + return False + return True + + +def check_private_key(): + """检查上传密钥是否存在""" + key_file = MINIPROGRAM_DIR / "private.key" + if key_file.exists(): + return True + print("❌ 未找到上传密钥: miniprogram/private.key") + print("") + print("请按以下步骤获取:") + print(" 1. 打开 https://mp.weixin.qq.com/ 登录小程序后台") + print(" 2. 开发管理 → 开发设置 → 小程序代码上传密钥") + print(" 3. 点击「生成」并下载密钥文件") + print(" 4. 将 private.*.key 重命名为 private.key") + print(" 5. 放到项目 miniprogram/ 目录下") + print("") + return False + + +def main(): + parser = argparse.ArgumentParser(description="小程序代码一键上传到微信公众平台") + parser.parse_args() + + print("=" * 60) + print(" Soul 创业派对 - 小程序一键上传") + print("=" * 60) + print(" 项目根目录: %s" % ROOT) + print(" 小程序目录: %s" % MINIPROGRAM_DIR) + print(" AppID: %s" % get_appid()) + print("=" * 60) + + if not check_miniprogram(): + return 1 + if not check_private_key(): + return 1 + + if not UPLOAD_SCRIPT.exists(): + print("❌ 未找到上传脚本: %s" % UPLOAD_SCRIPT) + return 1 + + print("") + print("🚀 调用 miniprogram/上传小程序.py 执行上传...") + print("") + + cmd = [sys.executable, str(UPLOAD_SCRIPT)] + + try: + r = subprocess.run( + cmd, + cwd=str(MINIPROGRAM_DIR), + timeout=300, + ) + if r.returncode == 0: + print("") + print(" 后台提交审核: https://mp.weixin.qq.com/ → 版本管理 → 开发版本 → 提交审核") + print("=" * 60) + return 0 + return r.returncode + except subprocess.TimeoutExpired: + print("❌ 上传超时(超过 5 分钟)") + return 1 + except Exception as e: + print("❌ 执行失败: %s" % e) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/deploy-to-server.sh b/scripts/deploy-to-server.sh deleted file mode 100755 index b67a6ca4..00000000 --- a/scripts/deploy-to-server.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/bin/bash -# Soul项目一键部署到宝塔服务器 -# 使用方法: ./deploy-to-server.sh [SSH密码] - -# 服务器配置 -SERVER_IP="42.194.232.22" -SERVER_USER="root" -PROJECT_PATH="/www/wwwroot/soul" -BRANCH="soul-content" - -# 颜色输出 -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -echo "=======================================" -echo " Soul项目 - 宝塔服务器一键部署" -echo "=======================================" -echo "" - -# 检查sshpass是否安装 -if ! command -v sshpass &> /dev/null; then - echo -e "${YELLOW}正在安装sshpass...${NC}" - brew install hudochenkov/sshpass/sshpass 2>/dev/null || { - echo -e "${RED}请手动安装sshpass: brew install hudochenkov/sshpass/sshpass${NC}" - exit 1 - } -fi - -# 获取SSH密码 -if [ -z "$1" ]; then - echo -n "请输入SSH密码: " - read -s SSH_PASSWORD - echo "" -else - SSH_PASSWORD="$1" -fi - -echo "" -echo -e "${GREEN}[1/5]${NC} 连接服务器..." - -# 测试连接 -sshpass -p "$SSH_PASSWORD" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 $SERVER_USER@$SERVER_IP "echo '连接成功'" 2>/dev/null -if [ $? -ne 0 ]; then - echo -e "${RED}连接失败,请检查密码是否正确${NC}" - exit 1 -fi - -echo -e "${GREEN}[2/5]${NC} 拉取最新代码..." -sshpass -p "$SSH_PASSWORD" ssh $SERVER_USER@$SERVER_IP "cd $PROJECT_PATH && git fetch origin && git reset --hard origin/$BRANCH" - -echo -e "${GREEN}[3/5]${NC} 安装依赖..." -sshpass -p "$SSH_PASSWORD" ssh $SERVER_USER@$SERVER_IP "cd $PROJECT_PATH && pnpm install --frozen-lockfile 2>/dev/null || npm install" - -echo -e "${GREEN}[4/5]${NC} 构建项目..." -sshpass -p "$SSH_PASSWORD" ssh $SERVER_USER@$SERVER_IP "cd $PROJECT_PATH && pnpm build 2>/dev/null || npm run build" - -echo -e "${GREEN}[5/5]${NC} 重启服务..." -# 使用www用户的PM2(宝塔方式) -sshpass -p "$SSH_PASSWORD" ssh $SERVER_USER@$SERVER_IP "sudo -u www /www/server/nvm/versions/node/*/bin/pm2 restart soul 2>/dev/null || pm2 restart soul" - -echo "" -echo "=======================================" -echo -e "${GREEN}✅ 部署完成!${NC}" -echo "=======================================" -echo "" -echo "访问地址: https://soul.quwanzhi.com" -echo "" - -# 测试API -echo "正在验证部署..." -sleep 3 -curl -s "https://soul.quwanzhi.com/api/book/chapter/1.1" | head -100 diff --git a/scripts/deploy_baota.py b/scripts/deploy_baota.py deleted file mode 100644 index f71cf9be..00000000 --- a/scripts/deploy_baota.py +++ /dev/null @@ -1,370 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Soul 创业派对 - 宝塔一键部署(跨平台) - -一键执行: python scripts/deploy_baota.py -依赖: pip install paramiko - -流程:本地 pnpm build -> 打包 .next/standalone -> 上传 -> 服务器解压 -> PM2 运行 node server.js -(不从 git 拉取,不在服务器安装依赖或构建。) -""" - -from __future__ import print_function - -import os -import sys -import getpass -import shutil -import subprocess -import tarfile -import tempfile -import threading -from pathlib import Path - -if sys.platform == 'win32': - import io - sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') - sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') - - -def log(msg, step=None): - """输出并立即刷新,便于看到进度""" - if step is not None: - print('[步骤 %s] %s' % (step, msg)) - else: - print(msg) - sys.stdout.flush() - sys.stderr.flush() - - -def log_err(msg): - print('>>> 错误: %s' % msg, file=sys.stderr) - sys.stderr.flush() - - -try: - import paramiko -except ImportError: - log('请先安装: pip install paramiko') - sys.exit(1) - -# 默认配置(与 开发文档/服务器管理 一致) -# 应用端口须与 端口配置表 及 Nginx proxy_pass 一致(soul -> 3006) -CFG = { - 'host': os.environ.get('DEPLOY_HOST', '42.194.232.22'), - 'port': int(os.environ.get('DEPLOY_PORT', '22')), - 'app_port': int(os.environ.get('DEPLOY_APP_PORT', '3006')), - 'user': os.environ.get('DEPLOY_USER', 'root'), - 'pwd': os.environ.get('DEPLOY_PASSWORD', 'Zhiqun1984'), - 'path': os.environ.get('DEPLOY_PROJECT_PATH', '/www/wwwroot/soul'), - 'branch': os.environ.get('DEPLOY_BRANCH', 'soul-content'), - 'pm2': os.environ.get('DEPLOY_PM2_APP', 'soul'), - 'url': os.environ.get('DEPLOY_SITE_URL', 'https://soul.quwanzhi.com'), - 'key': os.environ.get('DEPLOY_SSH_KEY') or None, -} - -EXCLUDE = { - 'node_modules', '.next', '.git', '.gitignore', '.cursorrules', - 'scripts', 'miniprogram', '开发文档', 'addons', 'book', - '__pycache__', '.DS_Store', '*.log', 'deploy_config.json', - 'requirements-deploy.txt', '*.bat', '*.ps1', -} - - -def run(ssh, cmd, desc, step_label=None, ignore_err=False): - """执行远程命令,打印完整输出,失败时明确标出错误和退出码""" - if step_label: - log(desc, step_label) - else: - log(desc) - print(' $ %s' % (cmd[:100] + '...' if len(cmd) > 100 else cmd)) - sys.stdout.flush() - stdin, stdout, stderr = ssh.exec_command(cmd, get_pty=True) - out = stdout.read().decode('utf-8', errors='replace') - err = stderr.read().decode('utf-8', errors='replace') - code = stdout.channel.recv_exit_status() - if out: - print(out) - sys.stdout.flush() - if err: - print(err, file=sys.stderr) - sys.stderr.flush() - if code != 0: - log_err('退出码: %s | %s' % (code, desc)) - if err and len(err.strip()) > 0: - for line in err.strip().split('\n')[-5:]: - print(' stderr: %s' % line, file=sys.stderr) - sys.stderr.flush() - return ignore_err - return True - - -def _read_and_print(stream, prefix=' ', is_stderr=False): - """后台线程:不断读 stream 并打印,用于实时输出""" - import threading - out = sys.stderr if is_stderr else sys.stdout - try: - while True: - line = stream.readline() - if not line: - break - s = line.decode('utf-8', errors='replace').rstrip() - if s: - print('%s%s' % (prefix, s), file=out) - out.flush() - except Exception: - pass - - -def run_stream(ssh, cmd, desc, step_label=None, ignore_err=False): - """执行远程命令并实时输出(npm install / build 不卡住、能看到进度)""" - if step_label: - log(desc, step_label) - else: - log(desc) - print(' $ %s' % (cmd[:100] + '...' if len(cmd) > 100 else cmd)) - sys.stdout.flush() - stdin, stdout, stderr = ssh.exec_command(cmd, get_pty=True) - t1 = threading.Thread(target=_read_and_print, args=(stdout, ' ', False)) - t2 = threading.Thread(target=_read_and_print, args=(stderr, ' [stderr] ', True)) - t1.daemon = True - t2.daemon = True - t1.start() - t2.start() - t1.join() - t2.join() - code = stdout.channel.recv_exit_status() - if code != 0: - log_err('退出码: %s | %s' % (code, desc)) - return ignore_err - return True - - -def _tar_filter(ti): - n = ti.name.replace('\\', '/') - if 'node_modules' in n or '.next' in n or '.git' in n: - return None - if '/scripts/' in n or n.startswith('scripts/'): - return None - if '/miniprogram/' in n or n.startswith('miniprogram/'): - return None - if '/开发文档/' in n or '开发文档/' in n: - return None - if '/addons/' in n or '/book/' in n: - return None - return ti - - -def make_tarball(root_dir): - root = Path(root_dir).resolve() - tmp = tempfile.NamedTemporaryFile(suffix='.tar.gz', delete=False) - tmp.close() - with tarfile.open(tmp.name, 'w:gz') as tar: - for item in root.iterdir(): - name = item.name - if name in EXCLUDE or name.endswith('.md') or (name.startswith('.') and name != '.cursorrules'): - continue - if name.startswith('deploy_config') or name.endswith('.bat') or name.endswith('.ps1'): - continue - arcname = name - tar.add(str(item), arcname=arcname, filter=_tar_filter) - return tmp.name - - -def run_local_build(local_root, step_label=None): - """本地执行 pnpm build,实时输出""" - root = Path(local_root).resolve() - if step_label: - log('本地构建 pnpm build(standalone)', step_label) - else: - log('本地构建 pnpm build(standalone)') - cmd_str = 'pnpm build' - print(' $ %s' % cmd_str) - sys.stdout.flush() - try: - # Windows 下用 shell=True,否则子进程 PATH 里可能没有 pnpm - use_shell = sys.platform == 'win32' - p = subprocess.Popen( - cmd_str if use_shell else ['pnpm', 'build'], - cwd=str(root), - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - bufsize=1, - universal_newlines=True, - encoding='utf-8', - errors='replace', - shell=use_shell, - ) - for line in p.stdout: - print(' %s' % line.rstrip()) - sys.stdout.flush() - code = p.wait() - if code != 0: - log_err('本地构建失败,退出码 %s' % code) - return False - return True - except Exception as e: - log_err('本地构建异常: %s' % e) - return False - - -def make_standalone_tarball(local_root): - """ - 在 next.config 已设置 output: 'standalone' 且已执行 pnpm build 的前提下, - 将 .next/static 和 public 复制进 .next/standalone,再打包 .next/standalone 目录内容。 - 返回生成的 tar.gz 路径。 - """ - root = Path(local_root).resolve() - standalone_dir = root / '.next' / 'standalone' - static_src = root / '.next' / 'static' - public_src = root / 'public' - if not standalone_dir.is_dir(): - raise FileNotFoundError('.next/standalone 不存在,请先执行 pnpm build') - # Next 要求将 .next/static 和 public 复制进 standalone - standalone_next = standalone_dir / '.next' - standalone_next.mkdir(parents=True, exist_ok=True) - if static_src.is_dir(): - dest_static = standalone_next / 'static' - if dest_static.exists(): - shutil.rmtree(dest_static) - shutil.copytree(static_src, dest_static) - if public_src.is_dir(): - dest_public = standalone_dir / 'public' - if dest_public.exists(): - shutil.rmtree(dest_public) - shutil.copytree(public_src, dest_public) - # 复制 PM2 配置到 standalone,便于服务器上用 pm2 start ecosystem.config.cjs - ecosystem_src = root / 'ecosystem.config.cjs' - if ecosystem_src.is_file(): - shutil.copy2(ecosystem_src, standalone_dir / 'ecosystem.config.cjs') - # 打包 standalone 目录「内容」,使解压到服务器项目目录后根目录即为 server.js - tmp = tempfile.NamedTemporaryFile(suffix='.tar.gz', delete=False) - tmp.close() - with tarfile.open(tmp.name, 'w:gz') as tar: - for item in standalone_dir.iterdir(): - arcname = item.name - tar.add(str(item), arcname=arcname, recursive=True) - return tmp.name - - -def deploy_by_upload_standalone(ssh, sftp, local_root, remote_path, pm2_name, step_start, app_port=None): - """本地 standalone 构建 -> 打包 -> 上传 -> 解压 -> PM2 用 node server.js 启动(PORT 与 Nginx 一致)""" - step = step_start - root = Path(local_root).resolve() - - # 步骤 1: 本地构建 - log('本地执行 pnpm build(standalone)', step) - step += 1 - if not run_local_build(str(root), step_label=None): - return False - sys.stdout.flush() - - # 步骤 2: 打包 standalone - log('打包 .next/standalone(含 static、public)', step) - step += 1 - try: - tarball = make_standalone_tarball(str(root)) - size_mb = os.path.getsize(tarball) / 1024 / 1024 - log('打包完成,约 %.2f MB' % size_mb) - except FileNotFoundError as e: - log_err(str(e)) - return False - except Exception as e: - log_err('打包失败: %s' % e) - return False - sys.stdout.flush() - - # 步骤 3: 上传 - log('上传到服务器 /tmp/soul_standalone.tar.gz', step) - step += 1 - remote_tar = '/tmp/soul_standalone.tar.gz' - try: - sftp.put(tarball, remote_tar) - log('上传完成') - except Exception as e: - log_err('上传失败: %s' % e) - os.unlink(tarball) - return False - os.unlink(tarball) - sys.stdout.flush() - - # 步骤 4: 清理并解压(保留 .env 等隐藏配置) - log('清理旧文件并解压 standalone', step) - step += 1 - run(ssh, 'cd %s && rm -rf app components lib public styles .next *.json *.js *.ts *.mjs *.css *.d.ts server.js node_modules 2>/dev/null; ls -la' % remote_path, '清理', step_label=None, ignore_err=True) - if not run(ssh, 'cd %s && tar -xzf %s' % (remote_path, remote_tar), '解压'): - log_err('解压失败,请检查服务器磁盘或路径') - return False - run(ssh, 'rm -f %s' % remote_tar, '删除临时包', ignore_err=True) - sys.stdout.flush() - - # 步骤 5: PM2 用 node server.js 启动,PORT 须与 Nginx proxy_pass 一致(默认 3006) - # 宝塔服务器上 pm2 可能不在默认 PATH,先注入常见路径 - port = app_port if app_port is not None else 3006 - log('PM2 启动 node server.js(PORT=%s)' % port, step) - pm2_cmd = ( - 'export PATH=/www/server/nodejs/v22.14.0/bin:/www/server/nvm/versions/node/*/bin:$PATH 2>/dev/null; ' - 'cd %s && (pm2 delete %s 2>/dev/null; PORT=%s pm2 start server.js --name %s)' - ) % (remote_path, pm2_name, port, pm2_name) - run(ssh, pm2_cmd, 'PM2 启动', ignore_err=True) - return True - - -def main(): - print('=' * 60) - print(' Soul 创业派对 - 宝塔一键部署') - print('=' * 60) - print(' %s@%s -> %s' % (CFG['user'], CFG['host'], CFG['path'])) - print('=' * 60) - sys.stdout.flush() - - # 步骤 1: 连接 - log('连接服务器 %s:%s' % (CFG['host'], CFG['port']), '1/6') - password = CFG.get('pwd') - if not CFG['key'] and not password: - password = getpass.getpass('请输入 SSH 密码: ') - sys.stdout.flush() - - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - try: - kw = {'hostname': CFG['host'], 'port': CFG['port'], 'username': CFG['user']} - if CFG['key']: - kw['key_filename'] = CFG['key'] - else: - kw['password'] = password - ssh.connect(**kw) - log('连接成功') - except Exception as e: - log_err('连接失败: %s' % e) - return 1 - sys.stdout.flush() - - p, pm = CFG['path'], CFG['pm2'] - sftp = ssh.open_sftp() - - # 步骤 2~6: 本地 build -> 打包 -> 上传 -> 解压 -> PM2 启动 - log('本地打包上传部署(不从 git 拉取)', '2/6') - local_root = Path(__file__).resolve().parent.parent - if not deploy_by_upload_standalone(ssh, sftp, str(local_root), p, pm, step_start=2, app_port=CFG.get('app_port')): - sftp.close() - ssh.close() - log_err('部署中断,请根据上方错误信息排查') - return 1 - - sftp.close() - ssh.close() - - print('') - print('=' * 60) - print(' 部署完成') - print(' 前台: %s' % CFG['url']) - print(' 后台: %s/admin' % CFG['url']) - print('=' * 60) - sys.stdout.flush() - return 0 - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/scripts/deploy_config.example.json b/scripts/deploy_config.example.json deleted file mode 100644 index 38406b96..00000000 --- a/scripts/deploy_config.example.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "server_host": "42.194.232.22", - "server_port": 22, - "server_user": "root", - "project_path": "/www/wwwroot/soul", - "branch": "soul-content", - "pm2_app_name": "soul", - "site_url": "https://soul.quwanzhi.com", - "ssh_key_path": null, - "use_pnpm": true, - "_comment": "复制本文件为 deploy_config.json,填写真实信息。不要将 deploy_config.json 提交到 Git。ssh_key_path 填私钥路径则用密钥登录,否则用密码。" -} diff --git a/scripts/devlop.py b/scripts/devlop.py new file mode 100644 index 00000000..63435d32 --- /dev/null +++ b/scripts/devlop.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Soul 创业派对 - 本地打包 + SSH 上传 + 宝塔 API 部署 + +流程:本地 pnpm build → 打包 .next/standalone → SSH 上传并解压到服务器 → 宝塔 API 重启 Node 项目 + +使用(在项目根目录): + python scripts/devlop.py + python scripts/devlop.py --no-build # 跳过构建,仅上传 + API 重启 + python scripts/devlop.py --no-api # 上传后不调宝塔 API 重启 + +环境变量: + DEPLOY_HOST, DEPLOY_USER, DEPLOY_PASSWORD 或 DEPLOY_SSH_KEY + DEPLOY_PROJECT_PATH(如 /www/wwwroot/soul) + BAOTA_PANEL_URL, BAOTA_API_KEY + DEPLOY_PM2_APP(如 soul) + +依赖:pip install -r requirements-deploy.txt (paramiko, requests) +""" + +from __future__ import print_function + +import os +import sys +import shutil +import tarfile +import tempfile +import subprocess +import argparse + +# 项目根目录(scripts 的上一级) +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# 不在本文件重写 sys.stdout/stderr,否则与 deploy_baota_pure_api 导入时的重写叠加会导致 +# 旧包装被 GC 关闭底层 buffer,后续 print 报 ValueError: I/O operation on closed file + +try: + import paramiko +except ImportError: + print("请先安装: pip install paramiko") + sys.exit(1) + +try: + import requests + import urllib3 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +except ImportError: + print("请先安装: pip install requests") + sys.exit(1) + +# 导入宝塔 API 重启逻辑 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from deploy_baota_pure_api import CFG as BAOTA_CFG, restart_node_project + + +# 部署配置(与 .cursorrules、DEPLOYMENT.md、deploy_baota_pure_api 一致) +# 未设置环境变量时使用 .cursorrules 中的服务器信息,可用 DEPLOY_* 覆盖 +def get_cfg(): + return { + "host": os.environ.get("DEPLOY_HOST", "42.194.232.22"), + "user": os.environ.get("DEPLOY_USER", "root"), + "password": os.environ.get("DEPLOY_PASSWORD", "Zhiqun1984"), + "ssh_key": os.environ.get("DEPLOY_SSH_KEY", ""), + "project_path": os.environ.get("DEPLOY_PROJECT_PATH", "/www/wwwroot/soul"), + "app_port": os.environ.get("DEPLOY_APP_PORT", "3006"), + "pm2_name": os.environ.get("DEPLOY_PM2_APP", BAOTA_CFG["pm2_name"]), + } + + +def run_build(root): + """本地执行 pnpm build(standalone 输出)""" + print("[1/4] 本地构建 pnpm build ...") + use_shell = sys.platform == "win32" + r = subprocess.run( + ["pnpm", "build"], + cwd=root, + shell=use_shell, + timeout=300, + ) + if r.returncode != 0: + print("构建失败,退出码:", r.returncode) + return False + standalone = os.path.join(root, ".next", "standalone") + if not os.path.isdir(standalone) or not os.path.isfile(os.path.join(standalone, "server.js")): + print("未找到 .next/standalone 或 server.js,请确认 next.config 中 output: 'standalone'") + return False + print(" 构建完成.") + return True + + +def pack_standalone(root): + """打包 standalone + .next/static + public + ecosystem.config.cjs,返回 tarball 路径""" + print("[2/4] 打包 standalone ...") + standalone = os.path.join(root, ".next", "standalone") + static_src = os.path.join(root, ".next", "static") + public_src = os.path.join(root, "public") + ecosystem_src = os.path.join(root, "ecosystem.config.cjs") + + staging = tempfile.mkdtemp(prefix="soul_deploy_") + try: + # 复制 standalone 目录内容到 staging + for name in os.listdir(standalone): + src = os.path.join(standalone, name) + dst = os.path.join(staging, name) + if os.path.isdir(src): + shutil.copytree(src, dst) + else: + shutil.copy2(src, dst) + # .next/static(standalone 可能已有 .next,先删再拷以用项目 static 覆盖) + static_dst = os.path.join(staging, ".next", "static") + shutil.rmtree(static_dst, ignore_errors=True) + os.makedirs(os.path.dirname(static_dst), exist_ok=True) + shutil.copytree(static_src, static_dst) + # public(standalone 可能已带 public 目录,先删再拷) + public_dst = os.path.join(staging, "public") + shutil.rmtree(public_dst, ignore_errors=True) + shutil.copytree(public_src, public_dst) + # ecosystem.config.cjs + shutil.copy2(ecosystem_src, os.path.join(staging, "ecosystem.config.cjs")) + + tarball = os.path.join(tempfile.gettempdir(), "soul_deploy.tar.gz") + with tarfile.open(tarball, "w:gz") as tf: + for name in os.listdir(staging): + tf.add(os.path.join(staging, name), arcname=name) + print(" 打包完成: %s" % tarball) + return tarball + finally: + shutil.rmtree(staging, ignore_errors=True) + + +def upload_and_extract(cfg, tarball_path): + """SSH 上传 tarball 并解压到服务器项目目录""" + print("[3/4] SSH 上传并解压 ...") + host = cfg["host"] + user = cfg["user"] + password = cfg["password"] + key_path = cfg["ssh_key"] + project_path = cfg["project_path"] + + if not password and not key_path: + print("请设置 DEPLOY_PASSWORD 或 DEPLOY_SSH_KEY") + return False + + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + if key_path: + client.connect(host, username=user, key_filename=key_path, timeout=15) + else: + client.connect(host, username=user, password=password, timeout=15) + + sftp = client.open_sftp() + remote_tar = "/tmp/soul_deploy.tar.gz" + sftp.put(tarball_path, remote_tar) + sftp.close() + + # 解压到项目目录:先清空再解压(保留 .env 等若存在可后续再配) + cmd = ( + "cd %s && " + "rm -rf .next server.js node_modules public ecosystem.config.cjs 2>/dev/null; " + "tar -xzf %s && " + "rm -f %s" + ) % (project_path, remote_tar, remote_tar) + stdin, stdout, stderr = client.exec_command(cmd, timeout=60) + err = stderr.read().decode("utf-8", errors="replace").strip() + if err: + print(" 服务器 stderr:", err) + code = stdout.channel.recv_exit_status() + if code != 0: + print(" 解压命令退出码:", code) + return False + print(" 上传并解压完成: %s" % project_path) + return True + except Exception as e: + print(" SSH 错误:", e) + return False + finally: + client.close() + + +def deploy_via_baota_api(cfg): + """宝塔 API 重启 Node 项目""" + print("[4/4] 宝塔 API 重启 Node 项目 ...") + panel_url = BAOTA_CFG["panel_url"] + api_key = BAOTA_CFG["api_key"] + pm2_name = cfg["pm2_name"] + ok = restart_node_project(panel_url, api_key, pm2_name) + if not ok: + print("提示:若 Node 接口不可用,请在宝塔面板【Node 项目】中手动重启 %s" % pm2_name) + return ok + + +def main(): + parser = argparse.ArgumentParser(description="本地打包 + SSH 上传 + 宝塔 API 部署") + parser.add_argument("--no-build", action="store_true", help="跳过本地构建(使用已有 .next/standalone)") + parser.add_argument("--no-upload", action="store_true", help="跳过 SSH 上传(仅构建+打包或仅 API)") + parser.add_argument("--no-api", action="store_true", help="上传后不调用宝塔 API 重启") + args = parser.parse_args() + + cfg = get_cfg() + print("=" * 60) + print(" Soul 创业派对 - 本地打包 + SSH 上传 + 宝塔 API 部署") + print("=" * 60) + print(" 服务器: %s@%s | 路径: %s | PM2: %s" % (cfg["user"], cfg["host"], cfg["project_path"], cfg["pm2_name"])) + print("=" * 60) + + tarball_path = None + + if not args.no_build: + if not run_build(ROOT): + return 1 + else: + # 若跳过构建,需已有 standalone,仍要打包 + if not os.path.isfile(os.path.join(ROOT, ".next", "standalone", "server.js")): + print("跳过构建但未找到 .next/standalone/server.js,请先执行一次完整部署或 pnpm build") + return 1 + + tarball_path = pack_standalone(ROOT) + if not tarball_path: + return 1 + + if not args.no_upload: + if not upload_and_extract(cfg, tarball_path): + return 1 + if os.path.isfile(tarball_path): + try: + os.remove(tarball_path) + except Exception: + pass + + if not args.no_api and not args.no_upload: + if not deploy_via_baota_api(cfg): + pass # 已打印提示 + + print("") + print(" 站点: %s | 后台: %s/admin" % (BAOTA_CFG["site_url"], BAOTA_CFG["site_url"])) + print("=" * 60) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/开发文档/8、部署/宝塔配置检查说明.md b/开发文档/8、部署/宝塔配置检查说明.md index 946e647b..cc474110 100644 --- a/开发文档/8、部署/宝塔配置检查说明.md +++ b/开发文档/8、部署/宝塔配置检查说明.md @@ -11,7 +11,7 @@ - **现象**:部署脚本用 `pm2 start server.js --name soul` 启动,未指定端口。Next.js standalone 默认监听 **3000**。 - **宝塔约定**:根据 `开发文档/服务器管理/references/端口配置表.md`,soul 使用端口 **3006**,Nginx 反代到 `127.0.0.1:3006`。 - **结果**:应用实际在 3000 监听,Nginx 请求 3006 → 无进程 → **502 Bad Gateway**。 -- **修复**:`scripts/deploy_baota.py` 已改为启动时设置 `PORT=3006`(可通过环境变量 `DEPLOY_APP_PORT` 覆盖),保证与 Nginx 一致。 +- **修复**:部署脚本 `scripts/devlop.py` 通过宝塔 API 重启 Node 项目,服务器上 PM2 启动时需设置 `PORT=3006`(可与 `ecosystem.config.cjs` 或环境变量 `DEPLOY_APP_PORT` 一致),保证与 Nginx 一致。 --- @@ -26,7 +26,7 @@ ### 2. PM2 与部署脚本一致 -- **项目名**:soul(与 `deploy_baota.py` 中 `DEPLOY_PM2_APP` 一致) +- **项目名**:soul(与 `scripts/devlop.py` 中 `DEPLOY_PM2_APP` 一致) - **启动方式**:**必须用 `node server.js`**,工作目录 `/www/wwwroot/soul`,环境变量 `PORT=3006`。 - **不要用**:`npm start` / `next start`。standalone 部署后没有完整 node_modules,也没有 `next` 命令,会报 `next: command not found`。 - **宝塔 PM2 管理器**:启动文件填 `server.js`,启动命令填 `node server.js`(或选「Node 项目」后只填 `server.js`),环境变量添加 `PORT=3006`。也可用 `pm2 start ecosystem.config.cjs`(项目根目录已提供该文件)。 @@ -63,10 +63,10 @@ nginx -t ```bash set DEPLOY_APP_PORT=3006 -python scripts/deploy_baota.py +python scripts/devlop.py ``` -或修改 `scripts/deploy_baota.py` 中 `CFG['app_port']` 的默认值(当前为 3006)。 +或修改 `scripts/devlop.py` 中 `get_cfg()` 的 `app_port` 默认值(当前为 3006)。 --- diff --git a/开发文档/8、部署/当前项目部署到线上.md b/开发文档/8、部署/当前项目部署到线上.md index e15ebde0..de51ea42 100644 --- a/开发文档/8、部署/当前项目部署到线上.md +++ b/开发文档/8、部署/当前项目部署到线上.md @@ -19,11 +19,12 @@ ```bash cd E:\Gongsi\Mycontent -python scripts/deploy_baota.py +pip install -r requirements-deploy.txt +python scripts/devlop.py ``` -- 脚本里已使用 服务器管理 的 root / Zhiqun1984,无需再输入密码。 -- 流程:SSH → 拉代码 → 安装依赖 → 构建 → PM2 重启。 +- 脚本默认使用 .cursorrules / 服务器管理 的 root / Zhiqun1984,无需再输入密码。 +- 流程:本地 pnpm build → 打包 → SSH 上传解压 → 宝塔 API 重启 Node 项目。 **方式 B:用 服务器管理 的一键部署** @@ -39,19 +40,32 @@ python 一键部署.py soul E:\Gongsi\Mycontent ## 二、小程序 -**AppID**:`wxb8bbb2b10dec74aa`(与 开发文档/小程序管理/apps_config.json 中 soul-party 一致) +**AppID**:`wxb8bbb2b10dec74aa`(与 `miniprogram/project.config.json`、.cursorrules、开发文档/小程序管理/apps_config.json 一致) -### 方式 A:用本仓库脚本(最简单) +说明:本仓库小程序为 **miniprogram/ 原生实现**(WXML/WXSS),上传即把该目录代码完整提交到微信公众平台。Web(Next.js)与小程序是两套技术栈,无法将网页 1:1 自动「转换」为小程序。 -1. 在微信公众平台下载「小程序代码上传密钥」,重命名为 `private.key`,放到 `miniprogram/` 目录。 -2. 在项目根目录执行: +### 方式 A:项目根目录一键上传(推荐) + +1. 在微信公众平台「开发管理 → 开发设置 → 小程序代码上传密钥」生成并下载密钥,重命名为 `private.key`,放到 `miniprogram/` 目录。 +2. 在**项目根目录**执行: + +```bash +python scripts/autosysc-weixin.py +``` + +- 脚本会调用 `miniprogram/上传小程序.py`,支持 Windows/Mac,需已安装微信开发者工具或 Node.js + miniprogram-ci。 + +### 方式 B:在 miniprogram 目录下上传 + +1. 同上,将 `private.key` 放到 `miniprogram/` 下。 +2. 执行: ```bash cd E:\Gongsi\Mycontent\miniprogram python 上传小程序.py ``` -### 方式 B:用 小程序管理(多小程序、提审、发布) +### 方式 C:用 小程序管理(多小程序、提审、发布) 1. 打开 `开发文档/小程序管理/scripts/apps_config.json`,把 soul-party 的 `project_path` 改成你本机路径,例如: - Windows:`E:/Gongsi/Mycontent/miniprogram` @@ -74,8 +88,9 @@ python mp_deploy.py deploy soul-party | 要部署的 | 推荐做法 | 命令/位置 | |----------|----------|-----------| -| Web + 后台 | 用本仓库脚本(已对接 服务器管理 凭证) | `python scripts/deploy_baota.py` | -| 小程序上传 | 用本仓库 miniprogram 脚本 | `cd miniprogram` → `python 上传小程序.py` | +| Web + 后台 | 用本仓库脚本(已对接 服务器管理 凭证) | `python scripts/devlop.py` | +| 小程序上传 | 项目根一键上传 | `python scripts/autosysc-weixin.py` | +| 小程序上传 | miniprogram 目录下上传 | `cd miniprogram` → `python 上传小程序.py` | | 小程序多项目/提审/发布 | 用 小程序管理 | `开发文档/小程序管理/scripts/mp_deploy.py` | | 服务器状态/SSL/多机 | 用 服务器管理 | `开发文档/服务器管理/scripts/` 下对应脚本 | diff --git a/开发文档/8、部署/部署脚本备份/README.md b/开发文档/8、部署/部署脚本备份/README.md new file mode 100644 index 00000000..2306449b --- /dev/null +++ b/开发文档/8、部署/部署脚本备份/README.md @@ -0,0 +1,7 @@ +# 部署脚本备份 + +本目录存放 **scripts/devlop.py** 的备份副本,仅作存档与应急恢复用。 + +- **日常部署**:请在项目根目录执行 `python scripts/devlop.py`。 +- **备份说明**:备份内容与 `scripts/devlop.py` 逻辑一致;若脚本有更新,可在此目录同步更新本备份。 +- **关联文档**:`DEPLOYMENT.md`、`开发文档/8、部署/宝塔配置检查说明.md`、`开发文档/8、部署/当前项目部署到线上.md`。 diff --git a/开发文档/8、部署/部署脚本备份/devlop.py b/开发文档/8、部署/部署脚本备份/devlop.py new file mode 100644 index 00000000..172f44ab --- /dev/null +++ b/开发文档/8、部署/部署脚本备份/devlop.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Soul 创业派对 - 本地打包 + SSH 上传 + 宝塔 API 部署(备份) + +本文件为 scripts/devlop.py 的备份,仅作存档。日常部署请使用项目根目录下: + python scripts/devlop.py + +备份时间说明见同目录 README.md +""" +# 以下为 scripts/devlop.py 的完整内容备份 +# ========== 备份开始 ========== + +from __future__ import print_function + +import os +import sys +import shutil +import tarfile +import tempfile +import subprocess +import argparse + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) # 备份版:从 开发文档/8、部署/部署脚本备份 回项目根 + +try: + import paramiko +except ImportError: + print("请先安装: pip install paramiko") + sys.exit(1) + +try: + import requests + import urllib3 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +except ImportError: + print("请先安装: pip install requests") + sys.exit(1) + +scripts_dir = os.path.join(ROOT, "scripts") +sys.path.insert(0, scripts_dir) +from deploy_baota_pure_api import CFG as BAOTA_CFG, restart_node_project + + +def get_cfg(): + return { + "host": os.environ.get("DEPLOY_HOST", "42.194.232.22"), + "user": os.environ.get("DEPLOY_USER", "root"), + "password": os.environ.get("DEPLOY_PASSWORD", "Zhiqun1984"), + "ssh_key": os.environ.get("DEPLOY_SSH_KEY", ""), + "project_path": os.environ.get("DEPLOY_PROJECT_PATH", "/www/wwwroot/soul"), + "app_port": os.environ.get("DEPLOY_APP_PORT", "3006"), + "pm2_name": os.environ.get("DEPLOY_PM2_APP", BAOTA_CFG["pm2_name"]), + } + + +def run_build(root): + print("[1/4] 本地构建 pnpm build ...") + use_shell = sys.platform == "win32" + r = subprocess.run(["pnpm", "build"], cwd=root, shell=use_shell, timeout=300) + if r.returncode != 0: + print("构建失败,退出码:", r.returncode) + return False + standalone = os.path.join(root, ".next", "standalone") + if not os.path.isdir(standalone) or not os.path.isfile(os.path.join(standalone, "server.js")): + print("未找到 .next/standalone 或 server.js,请确认 next.config 中 output: 'standalone'") + return False + print(" 构建完成.") + return True + + +def pack_standalone(root): + print("[2/4] 打包 standalone ...") + standalone = os.path.join(root, ".next", "standalone") + static_src = os.path.join(root, ".next", "static") + public_src = os.path.join(root, "public") + ecosystem_src = os.path.join(root, "ecosystem.config.cjs") + staging = tempfile.mkdtemp(prefix="soul_deploy_") + try: + for name in os.listdir(standalone): + src = os.path.join(standalone, name) + dst = os.path.join(staging, name) + if os.path.isdir(src): + shutil.copytree(src, dst) + else: + shutil.copy2(src, dst) + static_dst = os.path.join(staging, ".next", "static") + shutil.rmtree(static_dst, ignore_errors=True) + os.makedirs(os.path.dirname(static_dst), exist_ok=True) + shutil.copytree(static_src, static_dst) + public_dst = os.path.join(staging, "public") + shutil.rmtree(public_dst, ignore_errors=True) + shutil.copytree(public_src, public_dst) + shutil.copy2(ecosystem_src, os.path.join(staging, "ecosystem.config.cjs")) + tarball = os.path.join(tempfile.gettempdir(), "soul_deploy.tar.gz") + with tarfile.open(tarball, "w:gz") as tf: + for name in os.listdir(staging): + tf.add(os.path.join(staging, name), arcname=name) + print(" 打包完成: %s" % tarball) + return tarball + finally: + shutil.rmtree(staging, ignore_errors=True) + + +def upload_and_extract(cfg, tarball_path): + print("[3/4] SSH 上传并解压 ...") + host, user, password, key_path = cfg["host"], cfg["user"], cfg["password"], cfg["ssh_key"] + project_path = cfg["project_path"] + if not password and not key_path: + print("请设置 DEPLOY_PASSWORD 或 DEPLOY_SSH_KEY") + return False + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + if key_path: + client.connect(host, username=user, key_filename=key_path, timeout=15) + else: + client.connect(host, username=user, password=password, timeout=15) + sftp = client.open_sftp() + remote_tar = "/tmp/soul_deploy.tar.gz" + sftp.put(tarball_path, remote_tar) + sftp.close() + cmd = ( + "cd %s && " + "rm -rf .next server.js node_modules public ecosystem.config.cjs 2>/dev/null; " + "tar -xzf %s && rm -f %s" + ) % (project_path, remote_tar, remote_tar) + stdin, stdout, stderr = client.exec_command(cmd, timeout=60) + err = stderr.read().decode("utf-8", errors="replace").strip() + if err: + print(" 服务器 stderr:", err) + if stdout.channel.recv_exit_status() != 0: + return False + print(" 上传并解压完成: %s" % project_path) + return True + except Exception as e: + print(" SSH 错误:", e) + return False + finally: + client.close() + + +def deploy_via_baota_api(cfg): + print("[4/4] 宝塔 API 重启 Node 项目 ...") + ok = restart_node_project(BAOTA_CFG["panel_url"], BAOTA_CFG["api_key"], cfg["pm2_name"]) + if not ok: + print("提示:若 Node 接口不可用,请在宝塔面板【Node 项目】中手动重启 %s" % cfg["pm2_name"]) + return ok + + +def main(): + parser = argparse.ArgumentParser(description="本地打包 + SSH 上传 + 宝塔 API 部署") + parser.add_argument("--no-build", action="store_true", help="跳过本地构建") + parser.add_argument("--no-upload", action="store_true", help="跳过 SSH 上传") + parser.add_argument("--no-api", action="store_true", help="上传后不调宝塔 API 重启") + args = parser.parse_args() + cfg = get_cfg() + print("=" * 60) + print(" Soul 创业派对 - 本地打包 + SSH 上传 + 宝塔 API 部署") + print("=" * 60) + print(" 服务器: %s@%s | 路径: %s | PM2: %s" % (cfg["user"], cfg["host"], cfg["project_path"], cfg["pm2_name"])) + print("=" * 60) + if not args.no_build: + if not run_build(ROOT): + return 1 + else: + if not os.path.isfile(os.path.join(ROOT, ".next", "standalone", "server.js")): + print("跳过构建但未找到 .next/standalone/server.js") + return 1 + tarball_path = pack_standalone(ROOT) + if not tarball_path: + return 1 + if not args.no_upload: + if not upload_and_extract(cfg, tarball_path): + return 1 + if os.path.isfile(tarball_path): + try: + os.remove(tarball_path) + except Exception: + pass + if not args.no_api and not args.no_upload: + deploy_via_baota_api(cfg) + print("") + print(" 站点: %s | 后台: %s/admin" % (BAOTA_CFG["site_url"], BAOTA_CFG["site_url"])) + print("=" * 60) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) + +# ========== 备份结束 ========== diff --git a/开发文档/小程序管理/SKILL.md b/开发文档/小程序管理/SKILL.md index 4645ec85..35f2b125 100644 --- a/开发文档/小程序管理/SKILL.md +++ b/开发文档/小程序管理/SKILL.md @@ -56,6 +56,7 @@ | 命令 | 说明 | 示例 | |------|------|------| +| **项目根一键上传** | 将 miniprogram/ 代码完整上传到微信公众平台 | 在项目根目录:`python scripts/autosysc-weixin.py` | | `mp_full.py report` | 生成所有小程序汇总报告 | `python3 mp_full.py report` | | `mp_full.py check` | 检查项目问题 | `python3 mp_full.py check soul-party` | | `mp_full.py auto` | 全自动部署(上传+提审) | `python3 mp_full.py auto soul-party` | diff --git a/开发文档/服务器管理/references/宝塔api接口文档.md b/开发文档/服务器管理/references/宝塔api接口文档.md index ea972d17..03a5aec5 100644 --- a/开发文档/服务器管理/references/宝塔api接口文档.md +++ b/开发文档/服务器管理/references/宝塔api接口文档.md @@ -1,10 +1,12 @@ # 宝塔面板 API 接口文档 +**官方文档**:https://www.bt.cn/data/api-doc.pdf(系统、网站、文件、计划任务等;Node 项目管理为插件接口,不在官方 PDF 内。) + ## 1. 鉴权机制 所有 API 请求均需包含鉴权参数,使用 POST 方式提交。 -### 签名算法 +### 签名算法(与官方文档一致) ```python import time @@ -12,7 +14,7 @@ import hashlib def get_sign(api_key): now_time = int(time.time()) - # md5(timestamp + md5(api_key)) + # request_token = md5(string(request_time) + md5(api_sk)) sign_str = str(now_time) + hashlib.md5(api_key.encode('utf-8')).hexdigest() request_token = hashlib.md5(sign_str.encode('utf-8')).hexdigest() return now_time, request_token @@ -92,7 +94,7 @@ def get_sign(api_key): ## 5. Node.js 项目管理 (PM2) -> 注:部分接口可能随宝塔版本更新而变化 +> 注:Node 为插件功能,**官方 API 文档 (bt.cn/api-doc.pdf) 中未列出**,以下为插件常见路径,可能随宝塔/插件版本变化。 ### 获取 Node 项目列表 - **URL**: `/project/nodejs/get_project_list`