重构部署流程,统一使用 scripts/devlop.py 进行宝塔服务器部署,简化小程序上传步骤,更新相关文档以反映新流程和依赖项。删除不再使用的脚本和配置文件,提升项目可维护性。
This commit is contained in:
26
.cursorrules
26
.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
|
||||
|
||||
@@ -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` 跳过构建后上传已有包,或由服务器/计划任务在服务器上执行构建。
|
||||
|
||||
## 注意事项
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ""
|
||||
@@ -1,3 +1,4 @@
|
||||
# 仅用于「部署到宝塔」脚本,非项目运行依赖
|
||||
# 使用: pip install -r requirements-deploy.txt
|
||||
paramiko>=2.9.0
|
||||
requests>=2.28.0
|
||||
|
||||
101
scripts/Web转小程序并上传-提示词.md
Normal file
101
scripts/Web转小程序并上传-提示词.md
Normal file
@@ -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/<path>/page.tsx` → 小程序页面路径 `pages/<name>/<name>`,其中 `<name>` 取该路由的**最后一层目录名**(或约定名称,见下)。
|
||||
- 特例:
|
||||
- `app/page.tsx`(根首页)→ `pages/index/index`。
|
||||
- `app/<a>/[id]/page.tsx` 等动态路由 → `pages/<a>/<a>`,页面内通过 `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`
|
||||
|
||||
---
|
||||
|
||||
**当你被 @ 本文件时**:按「一、你的任务」执行完整流程(转换 → 检查 → 上传提示/执行),并严格遵循二~五的对照与规则。
|
||||
BIN
scripts/__pycache__/deploy_baota_pure_api.cpython-311.pyc
Normal file
BIN
scripts/__pycache__/deploy_baota_pure_api.cpython-311.pyc
Normal file
Binary file not shown.
130
scripts/autosysc-weixin.py
Normal file
130
scripts/autosysc-weixin.py
Normal file
@@ -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())
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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 填私钥路径则用密钥登录,否则用密码。"
|
||||
}
|
||||
243
scripts/devlop.py
Normal file
243
scripts/devlop.py
Normal file
@@ -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())
|
||||
@@ -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)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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/` 下对应脚本 |
|
||||
|
||||
|
||||
7
开发文档/8、部署/部署脚本备份/README.md
Normal file
7
开发文档/8、部署/部署脚本备份/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 部署脚本备份
|
||||
|
||||
本目录存放 **scripts/devlop.py** 的备份副本,仅作存档与应急恢复用。
|
||||
|
||||
- **日常部署**:请在项目根目录执行 `python scripts/devlop.py`。
|
||||
- **备份说明**:备份内容与 `scripts/devlop.py` 逻辑一致;若脚本有更新,可在此目录同步更新本备份。
|
||||
- **关联文档**:`DEPLOYMENT.md`、`开发文档/8、部署/宝塔配置检查说明.md`、`开发文档/8、部署/当前项目部署到线上.md`。
|
||||
192
开发文档/8、部署/部署脚本备份/devlop.py
Normal file
192
开发文档/8、部署/部署脚本备份/devlop.py
Normal file
@@ -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())
|
||||
|
||||
# ========== 备份结束 ==========
|
||||
@@ -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` |
|
||||
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user