From 0af546cd1df3139e5b3703c9e61d9312ce8e349b Mon Sep 17 00:00:00 2001 From: jiandanc Date: Sun, 7 Jun 2026 21:03:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20Python=20venv=20?= =?UTF-8?q?=E9=9A=94=E7=A6=BB=E7=8E=AF=E5=A2=83=EF=BC=8C=E9=80=82=E9=85=8D?= =?UTF-8?q?=20Node.js=20=E5=8E=9F=E7=94=9F=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 背景 原方案使用系统 Python + pip --prefix 安装依赖到 data/dep_cache/python3, 系统 Python 环境被污染,且无法隔离不同项目的依赖。 ## 改动内容 ### 核心:Python venv 支持 - shell/start.sh: 使用 python3 -m venv 创建虚拟环境,替代 --prefix 模式 - 从 .env 文件读取 PYTHON_VENV_DIR 配置 - 首次启动自动创建 .venv,已存在则跳过 - 仅将 .venv/bin 加入 PATH 优先级,不设置 PYTHONHOME/PYTHONPATH(避免破坏 venv 机制) - .env 不再强制覆盖,仅首次从 .env.example 复制 - 启动时自动修复 task/ql 软链接指向当前部署目录 ### 后端适配 - back/config/const.ts: 新增 PYTHON_VENV_DIR 常量 - back/config/util.ts: venv 模式下跳过 pip --prefix,直接使用 venv 的 pip3 ### 开发模式支持 - shell/dev-env.sh: pnpm start 时自动 source,将 .venv/bin 加入 PATH - package.json: start:back 加入 source dev-env.sh ### 新增文件 - shell/start-simplify.sh: 精简版启动脚本(跳过系统依赖安装,适用于已预装环境的服务器) - README-NODE.md: Node.js 原生部署完整文档 ### 配置 - .env.example: 新增 PYTHON_VENV_DIR=./.venv 配置项(默认注释状态) ## 兼容性 - Docker 模式不受影响(使用独立的 docker-entrypoint.sh,走原有 --prefix 逻辑) - 未配置 PYTHON_VENV_DIR 时默认使用系统 Python(向后兼容) --- .env.example | 3 + README-NODE.md | 296 ++++++++++++++++++++++++++++++++++++++++ back/config/const.ts | 1 + back/config/util.ts | 4 +- package.json | 2 +- shell/dev-env.sh | 12 ++ shell/start-simplify.sh | 104 ++++++++++++++ shell/start.sh | 42 ++++-- 8 files changed, 452 insertions(+), 12 deletions(-) create mode 100644 README-NODE.md create mode 100755 shell/dev-env.sh create mode 100644 shell/start-simplify.sh diff --git a/.env.example b/.env.example index f86264af..a149b653 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,6 @@ JWT_EXPIRES_IN= QINIU_AK= QINIU_SK= QINIU_SCOPE= + +# Python 虚拟环境路径,留空则使用系统 Python +# PYTHON_VENV_DIR=./.venv diff --git a/README-NODE.md b/README-NODE.md new file mode 100644 index 00000000..a255479b --- /dev/null +++ b/README-NODE.md @@ -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`。 diff --git a/back/config/const.ts b/back/config/const.ts index 59350a98..a6167c76 100644 --- a/back/config/const.ts +++ b/back/config/const.ts @@ -25,6 +25,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', diff --git a/back/config/util.ts b/back/config/util.ts index ff1aa544..5eb1c339 100644 --- a/back/config/util.ts +++ b/back/config/util.ts @@ -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'; @@ -602,7 +602,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}`; } diff --git a/package.json b/package.json index cda82f41..ef27cc71 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/shell/dev-env.sh b/shell/dev-env.sh new file mode 100755 index 00000000..9e3474a4 --- /dev/null +++ b/shell/dev-env.sh @@ -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 加入 PATH,Python 的 venv 机制自动处理包路径 + export PATH="${PYTHON_VENV_DIR}/bin:${PATH}" + export PIP_CACHE_DIR="${PYTHON_VENV_DIR}/pip" + mkdir -p "${PIP_CACHE_DIR}" +fi diff --git a/shell/start-simplify.sh b/shell/start-simplify.sh new file mode 100644 index 00000000..6e317b57 --- /dev/null +++ b/shell/start-simplify.sh @@ -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=" + 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" "🎉 启动成功!" diff --git a/shell/start.sh b/shell/start.sh index 3f4bb9cd..7705e04d 100644 --- a/shell/start.sh +++ b/shell/start.sh @@ -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 加入 PATH,Python 的 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