This commit is contained in:
jiandanc 2026-06-26 16:53:57 +08:00 committed by GitHub
commit bb3c7b822d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 452 additions and 12 deletions

View File

@ -14,3 +14,6 @@ JWT_EXPIRES_IN=
QINIU_AK=
QINIU_SK=
QINIU_SCOPE=
# Python 虚拟环境路径,留空则使用系统 Python
# PYTHON_VENV_DIR=./.venv

296
README-NODE.md Normal file
View File

@ -0,0 +1,296 @@
# QingLong - Node.js 原生部署指南
本文档说明如何在 Linux 服务器上以 Node.js 原生方式编译、部署和运行青龙面板,使用 Python venv 隔离环境。
## 一、环境要求
### 本地构建环境macOS / Linux
- Node.js 20.x
- pnpm 8.3.1
- Python 3.x
### 服务器运行环境
- Node.js 16+
- Python 3 + python3-venv
- nginx
- jq、curl、git
- pm2全局安装
## 二、本地编译
```bash
# 克隆项目
git clone https://github.com/whyour/qinglong.git
cd qinglong
# 安装依赖
npm i -g pnpm@8.3.1
pnpm install
# 创建本地 venv可选仅本地开发需要
python3 -m venv .venv
# 构建前端
pnpm build:front
# 构建后端
pnpm build:back
```
构建产物:
```
static/
├── build/ ← 后端编译产物tsc
│ └── app.js ← pm2 入口
└── dist/ ← 前端构建产物umi
├── index.html
└── ...
```
## 三、打包上传
### 打包(不含 node_modules约 12MB
```bash
COPYFILE_DISABLE=1 tar czf qinglong-deploy.tar.gz \
--exclude='node_modules' \
static/ shell/ sample/ back/protos/ \
package.json pnpm-lock.yaml ecosystem.config.js .env.example version.yaml
```
### 上传到服务器
```bash
scp qinglong-deploy.tar.gz user@server:~/
```
## 四、服务器部署
### 4.1 解压并安装依赖
```bash
mkdir -p ~/qinglong
tar xzf qinglong-deploy.tar.gz -C ~/qinglong
cd ~/qinglong
# 安装 pnpm如果没有
npm i -g pnpm@8.3.1
# 安装生产依赖
pnpm install --prod
```
### 4.2 首次启动
```bash
export QL_DIR=$(pwd)
export QL_DATA_DIR="${QL_DIR}/data"
bash shell/start-simplify.sh
```
首次启动会自动:
1. 读取 `.env` 配置(不存在则从 `.env.example` 复制)
2. 创建 Python venv`${QL_DIR}/.venv`
3. 安装 `requests` 到 venv
4. 修复 `task` / `ql` 命令软链接
5. 启动 nginx + pm2
访问 `http://服务器IP:5700` 即可。
### 4.3 配置 systemctl 开机自启
```bash
sudo tee /etc/systemd/system/qinglong.service << 'EOF'
[Unit]
Description=QingLong Panel
After=network.target
[Service]
Type=forking
WorkingDirectory=/home/jiandanc/qinglong
Environment="QL_DIR=/home/jiandanc/qinglong"
Environment="QL_DATA_DIR=/home/jiandanc/qinglong/data"
ExecStart=/bin/bash /home/jiandanc/qinglong/shell/start-simplify.sh
StandardOutput=journal
StandardError=journal
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
# 重载配置
sudo systemctl daemon-reload
# 启动服务
sudo systemctl start qinglong
# 设置开机自启
sudo systemctl enable qinglong
# 查看状态
sudo systemctl status qinglong
# 查看日志
sudo journalctl -u qinglong -f
```
> **注意**`WorkingDirectory`、`QL_DIR`、`QL_DATA_DIR` 需要替换为你的实际部署路径。
## 五、目录结构
部署后的完整目录结构:
```
~/qinglong/ ← QL_DIR
├── .venv/ ← Python 虚拟环境(自动创建)
│ ├── bin/
│ │ ├── python3
│ │ └── pip3
│ └── lib/
├── data/ ← QL_DATA_DIR运行数据
│ ├── config/ ← 配置文件
│ │ ├── config.sh ← 用户配置
│ │ └── crontab.list ← 定时任务列表
│ ├── scripts/ ← 用户脚本
│ ├── log/ ← 运行日志
│ ├── db/ ← 数据库
│ ├── deps/ ← 依赖缓存
│ └── dep_cache/
│ └── node/ ← Node 依赖缓存
├── static/
│ ├── build/ ← 后端编译产物
│ └── dist/ ← 前端构建产物
├── shell/
│ ├── start.sh ← 完整启动脚本(含系统依赖安装)
│ ├── start-simplify.sh ← 精简启动脚本(跳过系统依赖安装)
│ ├── task.sh ← 定时任务执行脚本
│ └── update.sh ← 更新脚本ql 命令)
├── sample/ ← 配置模板
├── back/protos/ ← gRPC proto 文件
├── ecosystem.config.js ← pm2 配置
├── package.json
└── .env ← 环境变量Node.js 读取)
```
## 六、Python venv 说明
### 工作原理
启动脚本将 `.venv/bin` 加入 `PATH` 最前面,使所有 `python3` / `pip3` 调用自动指向 venv无需修改 `task.sh` 等原有脚本。
### 环境变量解析优先级
```
1. export PYTHON_VENV_DIR=/path ← 手动 export最高优先级
2. .env 中 PYTHON_VENV_DIR=./.venv ← 启动脚本从 .env 读取
3. ${QL_DIR}/.venv ← 默认值(兜底)
```
### 不设置的变量
以下变量**不应设置**,否则会破坏 venv 的包解析机制:
- `PYTHONHOME` — 会覆盖 Python 的 prefix 解析
- `PYTHONUSERBASE` — venv 不需要
- `PYTHONPATH` — venv 通过 `pyvenv.cfg` 自动管理
### 验证 venv 是否生效
```bash
# 查看 Python 路径
which python3
# 预期: ~/qinglong/.venv/bin/python3
# 验证是否在 venv 中
python3 -c "import sys; print('✅ venv' if sys.prefix != sys.base_prefix else '❌ 系统')"
```
## 七、本地开发模式
```bash
cd qinglong
# 创建 venv
python3 -m venv .venv
# 安装依赖
pnpm install
# 启动(前端 8000 端口,后端 5700 端口)
pnpm start
```
开发模式下 `pnpm start` 会自动 source `shell/dev-env.sh` 设置 venv 环境。
## 八、常用运维命令
```bash
# 查看服务状态
sudo systemctl status qinglong
# 重启服务
sudo systemctl restart qinglong
# 查看日志
sudo journalctl -u qinglong -f
# 查看 pm2 状态
sudo pm2 list
# 查看 pm2 日志
sudo pm2 logs
# 手动运行任务
cd ~/qinglong
QL_DIR=$(pwd) QL_DATA_DIR="$(pwd)/data" bash shell/task.sh <脚本名> now
# 更新 qinglong重新编译后替换 static/ 目录,重启服务)
sudo systemctl restart qinglong
```
## 九、常见问题
### Q: 网页运行任务一直显示"运行中"
检查 `task` 软链接是否指向正确目录:
```bash
ls -la /usr/local/bin/task
# 应该指向 ~/qinglong/shell/task.sh不是 npm 全局路径
```
修复:
```bash
sudo ln -sf ~/qinglong/shell/task.sh /usr/local/bin/task
sudo pm2 restart qinglong
```
### Q: 安装 Python 依赖报权限错误
```bash
sudo chown -R $(whoami) ~/qinglong/.venv
```
### Q: sudo 运行后普通用户无权限
```bash
sudo chown -R $(whoami) ~/qinglong/data
```
### Q: systemctl 日志为空
确保 service 文件中包含:
```ini
StandardOutput=journal
StandardError=journal
```
然后 `sudo systemctl daemon-reload && sudo systemctl restart qinglong`

View File

@ -27,6 +27,7 @@ export const SAMPLE_FILES = [
];
export const PYTHON_INSTALL_DIR = process.env.PYTHON_HOME;
export const PYTHON_VENV_DIR = process.env.PYTHON_VENV_DIR;
export const NotificationModeStringMap = {
0: 'gotify',

View File

@ -5,7 +5,7 @@ import psTreeFun from 'ps-tree';
import { promisify } from 'util';
import { load } from 'js-yaml';
import config from './index';
import { PYTHON_INSTALL_DIR, TASK_COMMAND } from './const';
import { PYTHON_INSTALL_DIR, PYTHON_VENV_DIR, TASK_COMMAND } from './const';
import Logger from '../loaders/logger';
import { writeFileWithLock } from '../shared/utils';
import { DependenceTypes } from '../data/dependence';
@ -603,7 +603,7 @@ export function getInstallCommand(type: DependenceTypes, name: string): string {
let command = baseCommands[type];
if (type === DependenceTypes.python3 && PYTHON_INSTALL_DIR) {
if (type === DependenceTypes.python3 && PYTHON_INSTALL_DIR && !PYTHON_VENV_DIR) {
command = `${command} --prefix=${PYTHON_INSTALL_DIR}`;
}

View File

@ -14,7 +14,7 @@
},
"scripts": {
"start": "concurrently -n w: npm:start:*",
"start:back": "nodemon ./back/app.ts",
"start:back": "source shell/dev-env.sh && nodemon ./back/app.ts",
"start:front": "max dev",
"build:front": "max build",
"build:back": "tsc -p back/tsconfig.json",

12
shell/dev-env.sh Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
# 开发模式下设置 Python venv 环境变量
# 用法: source shell/dev-env.sh需在项目根目录执行
export PYTHON_VENV_DIR="${PYTHON_VENV_DIR:-${PWD}/.venv}"
if [[ -f "${PYTHON_VENV_DIR}/bin/python3" ]]; then
# 仅将 venv 的 bin 加入 PATHPython 的 venv 机制自动处理包路径
export PATH="${PYTHON_VENV_DIR}/bin:${PATH}"
export PIP_CACHE_DIR="${PYTHON_VENV_DIR}/pip"
mkdir -p "${PIP_CACHE_DIR}"
fi

104
shell/start-simplify.sh Normal file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env bash
# 简化版启动脚本:跳过系统依赖和 npm 全局安装
# 前置要求:已安装 nodejs、npm/pnpm、python3、nginx、jq
set -e
set -x
if [[ ! $QL_DIR ]]; then
echo -e "请先设置 export QL_DIR=<qinglong目录>"
exit 1
fi
if [[ ! $QL_DATA_DIR ]]; then
export QL_DATA_DIR="${QL_DIR}/data"
fi
if [[ $QL_DATA_DIR != */data ]]; then
echo -e "QL_DATA_DIR 必须以 /data 结尾,例如 /ql/data"
exit 1
fi
command="$1"
# 从 .env 文件读取 PYTHON_VENV_DIR
if [[ -z "${PYTHON_VENV_DIR:-}" ]] && [[ -f "${QL_DIR}/.env" ]]; then
env_venv_dir=$(grep -E '^PYTHON_VENV_DIR=' "${QL_DIR}/.env" | tail -1 | cut -d'=' -f2 | xargs)
if [[ -n "$env_venv_dir" ]]; then
if [[ "$env_venv_dir" != /* ]]; then
env_venv_dir="${QL_DIR}/${env_venv_dir}"
fi
export PYTHON_VENV_DIR="$env_venv_dir"
fi
fi
export PNPM_HOME=${QL_DIR}/data/dep_cache/node
# ===== Python venv =====
export PYTHON_VENV_DIR="${PYTHON_VENV_DIR:-${QL_DIR}/.venv}"
if [[ ! -f "${PYTHON_VENV_DIR}/bin/python3" ]]; then
echo "正在创建 Python venv: ${PYTHON_VENV_DIR}"
python3 -m venv "${PYTHON_VENV_DIR}"
fi
export PATH=${PYTHON_VENV_DIR}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}
export NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules
export PIP_CACHE_DIR=${PYTHON_VENV_DIR}/pip
mkdir -p "${PIP_CACHE_DIR}"
if [[ $command != "reload" ]]; then
${PYTHON_VENV_DIR}/bin/pip3 install requests
fi
cd ${QL_DIR}
# 仅在 .env 不存在时从 .env.example 复制
if [[ ! -f .env ]] && [[ -f .env.example ]]; then
cp -f .env.example .env
fi
chmod 777 ${QL_DIR}/shell/*.sh
# 确保 task/ql 命令指向当前部署目录(覆盖 npm 全局安装可能指向旧路径的软链接)
ln -sf ${QL_DIR}/shell/task.sh /usr/local/bin/task 2>/dev/null || true
ln -sf ${QL_DIR}/shell/update.sh /usr/local/bin/ql 2>/dev/null || true
. ${QL_DIR}/shell/share.sh
. ${QL_DIR}/shell/env.sh
log_with_style() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
printf "\n[%s] [%7s] %s\n" "${timestamp}" "${level}" "${message}"
}
log_with_style "INFO" "🚀 1. 检测配置文件..."
import_config "$@"
make_dir /etc/nginx/conf.d
make_dir /run/nginx
fix_config
pm2 l &>/dev/null
log_with_style "INFO" "🔄 2. 启动 nginx..."
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
log_with_style "INFO" "⚙️ 3. 启动 pm2 服务..."
reload_pm2
if [[ $command != "reload" ]]; then
if [[ $AutoStartBot == true ]]; then
log_with_style "INFO" "🤖 4. 启动 bot..."
nohup ql bot >$dir_log/bot.log 2>&1 &
fi
if [[ $EnableExtraShell == true ]]; then
log_with_style "INFO" "🛠️ 5. 执行自定义脚本..."
nohup ql extra >$dir_log/extra.log 2>&1 &
fi
pm2 startup
pm2 save
fi
log_with_style "SUCCESS" "🎉 启动成功!"

View File

@ -77,24 +77,48 @@ if [[ $command != "reload" ]]; then
npm install -g pnpm@8.3.1 pm2 ts-node
fi
export PYTHON_SHORT_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
export PNPM_HOME=${QL_DIR}/data/dep_cache/node
export PYTHON_HOME=${QL_DIR}/data/dep_cache/python3
export PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3
# 从 .env 文件读取 PYTHON_VENV_DIR.env 是给 Node.js 用的bash 需要手动读取)
if [[ -z "${PYTHON_VENV_DIR:-}" ]] && [[ -f "${QL_DIR}/.env" ]]; then
env_venv_dir=$(grep -E '^PYTHON_VENV_DIR=' "${QL_DIR}/.env" | tail -1 | cut -d'=' -f2 | xargs)
if [[ -n "$env_venv_dir" ]]; then
# 处理相对路径
if [[ "$env_venv_dir" != /* ]]; then
env_venv_dir="${QL_DIR}/${env_venv_dir}"
fi
export PYTHON_VENV_DIR="$env_venv_dir"
fi
fi
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin
export PNPM_HOME=${QL_DIR}/data/dep_cache/node
# ===== Python venv =====
export PYTHON_VENV_DIR="${PYTHON_VENV_DIR:-${QL_DIR}/.venv}"
if [[ ! -f "${PYTHON_VENV_DIR}/bin/python3" ]]; then
echo "正在创建 Python venv: ${PYTHON_VENV_DIR}"
python3 -m venv "${PYTHON_VENV_DIR}"
fi
# 仅将 venv 的 bin 加入 PATHPython 的 venv 机制自动处理包路径
export PATH=${PYTHON_VENV_DIR}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}
export NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules
export PIP_CACHE_DIR=${PYTHON_HOME}/pip
export PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
export PIP_CACHE_DIR=${PYTHON_VENV_DIR}/pip
mkdir -p "${PIP_CACHE_DIR}"
if [[ $command != "reload" ]]; then
pip3 install --prefix ${PYTHON_HOME} requests
${PYTHON_VENV_DIR}/bin/pip3 install requests
fi
cd ${QL_DIR}
cp -f .env.example .env
# 仅在 .env 不存在时从 .env.example 复制
if [[ ! -f .env ]] && [[ -f .env.example ]]; then
cp -f .env.example .env
fi
chmod 777 ${QL_DIR}/shell/*.sh
# 确保 task/ql 命令指向当前部署目录(覆盖 npm 全局安装可能指向旧路径的软链接)
ln -sf ${QL_DIR}/shell/task.sh /usr/local/bin/task 2>/dev/null || true
ln -sf ${QL_DIR}/shell/update.sh /usr/local/bin/ql 2>/dev/null || true
. ${QL_DIR}/shell/share.sh
. ${QL_DIR}/shell/env.sh