重构部署流程,统一使用 scripts/devlop.py 进行宝塔服务器部署,简化小程序上传步骤,更新相关文档以反映新流程和依赖项。删除不再使用的脚本和配置文件,提升项目可维护性。

This commit is contained in:
乘风
2026-01-31 21:52:15 +08:00
parent 70497d3047
commit 7a3033b4a0
20 changed files with 749 additions and 1017 deletions

View File

@@ -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

View File

@@ -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` 跳过构建后上传已有包,或由服务器/计划任务在服务器上执行构建
## 注意事项

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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 ""

View File

@@ -1,3 +1,4 @@
# 仅用于「部署到宝塔」脚本,非项目运行依赖
# 使用: pip install -r requirements-deploy.txt
paramiko>=2.9.0
requests>=2.28.0

View 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`
---
**当你被 @ 本文件时**:按「一、你的任务」执行完整流程(转换 → 检查 → 上传提示/执行),并严格遵循二~五的对照与规则。

130
scripts/autosysc-weixin.py Normal file
View File

@@ -0,0 +1,130 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Soul 创业派对 - 小程序一键上传(项目根目录运行)
将**当前仓库 miniprogram/ 目录的小程序代码**完整上传到微信公众平台。
AppID、项目路径等见 miniprogram/project.config.json 或 .cursorrules。
说明:
- 本脚本是「把仓库里已有小程序代码原样上传」,不是「把 Web 站转成小程序」。
- WebNext.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())

View File

@@ -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

View File

@@ -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 buildstandalone', step_label)
else:
log('本地构建 pnpm buildstandalone')
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 buildstandalone', 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.jsPORT=%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())

View File

@@ -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
View 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 buildstandalone 输出)"""
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/staticstandalone 可能已有 .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)
# publicstandalone 可能已带 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())

View File

@@ -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
---

View File

@@ -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上传即把该目录代码完整提交到微信公众平台。WebNext.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/` 下对应脚本 |

View File

@@ -0,0 +1,7 @@
# 部署脚本备份
本目录存放 **scripts/devlop.py** 的备份副本,仅作存档与应急恢复用。
- **日常部署**:请在项目根目录执行 `python scripts/devlop.py`
- **备份说明**:备份内容与 `scripts/devlop.py` 逻辑一致;若脚本有更新,可在此目录同步更新本备份。
- **关联文档**`DEPLOYMENT.md``开发文档/8、部署/宝塔配置检查说明.md``开发文档/8、部署/当前项目部署到线上.md`

View 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())
# ========== 备份结束 ==========

View File

@@ -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` |

View File

@@ -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`