diff --git a/.env.example b/.env.example index c0b29702..5bc5eb04 100644 --- a/.env.example +++ b/.env.example @@ -21,3 +21,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..0671dddc --- /dev/null +++ b/README-NODE.md @@ -0,0 +1,10 @@ +# Node.js 原生部署与 Python venv + +设置 `QL_DIR` 为安装目录,`QL_DATA_DIR` 为以 `/data` 结尾的绝对持久化数据路径。 +如需隔离 Python,请在启动进程的环境中显式设置 `PYTHON_VENV_DIR`(绝对路径或相对 `QL_DIR` 的路径),例如 `export PYTHON_VENV_DIR=/srv/qinglong/.venv`。未设置时保留现有 Python prefix 行为。 + +- `qinglong`(`shell/start.sh`)安装系统依赖和 Node 工具,并安装 Python requests,然后调用公共启动脚本。 +- `npm run start:simple`(`shell/start-simplify.sh`)使用已准备的 Node、pm2、nginx、jq 等依赖,不安装系统包、全局 npm 包或 requests;显式指定的 venv 不存在时仍会创建。请先在 venv 中安装 requests 和任务所需依赖。 +- 两种启动方式均保留已有 `.env`;使用 venv 时清除继承的 `PYTHONPATH`/`PYTHONUSERBASE`,依赖查询、安装和卸载使用该 venv 的 Python。 + +该模式需具备现有启动流程所要求的 nginx 配置和 pm2 服务权限。生产环境设置变量应通过 systemd 的 `Environment` 或相应进程配置完成。已有系统 Python 依赖不自动迁移至 venv,需在 venv 中重新安装。 diff --git a/back/config/const.ts b/back/config/const.ts index 3179af5e..459cf0fd 100644 --- a/back/config/const.ts +++ b/back/config/const.ts @@ -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', diff --git a/back/config/util.ts b/back/config/util.ts index 88d591c7..08b440d8 100644 --- a/back/config/util.ts +++ b/back/config/util.ts @@ -1,19 +1,25 @@ import * as fs from 'fs/promises'; +import { statSync } from 'fs'; import * as path from 'path'; import { exec, execSync } from 'child_process'; 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'; import { FormData } from 'undici'; import os from 'os'; import { maybeSudo, isInContainer } from './container'; +import { shellQuote } from '../shared/shellQuote'; import { resolveFileAccess } from '../shared/fileAccess'; -import { mirrorWriteCommand, normalizeMirror, rewriteMirrorSources } from '../shared/linuxMirror'; +import { + mirrorWriteCommand, + normalizeMirror, + rewriteMirrorSources, +} from '../shared/linuxMirror'; export * from './share'; @@ -671,7 +677,9 @@ export async function setSystemTimezone(timezone: string): Promise { throw new Error('Invalid timezone'); } - await promiseExec(maybeSudo(`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`)); + await promiseExec( + maybeSudo(`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`), + ); await promiseExec(`echo "${timezone}" | ${maybeSudo('tee /etc/timezone')}`); return true; @@ -681,61 +689,150 @@ export async function setSystemTimezone(timezone: string): Promise { } } +// Helper function to check if a name is a GitHub URL +function getPythonExecutable(): string { + return PYTHON_VENV_DIR + ? shellQuote(path.resolve(config.rootPath, PYTHON_VENV_DIR, 'bin/python3')) + : 'python3'; +} + +function getPythonPip(): string { + return PYTHON_VENV_DIR ? `${getPythonExecutable()} -m pip` : 'pip3'; +} + +function isGitHubUrl(name: string): boolean { + // Support git+https://, git+http://, https://, and http:// URLs + // This covers GitHub URLs and other git-compatible repositories + return !!name.match(/^(git\+https?:\/\/|https?:\/\/)/i); +} + +// Helper function to check if a name is a requirements file +function isRequirementsFile(name: string): boolean { + return !!name.match(/requirements.*\.(txt|in)$/i); +} + +// Helper function to check if a name is a pyproject.toml file +function isPyprojectToml(name: string): boolean { + return name.endsWith('pyproject.toml'); +} + export function getGetCommand(type: DependenceTypes, name: string): string { + const trimmedName = name.trim(); + + // For Python dependencies installed from GitHub or requirements files, + // we can't reliably check if they're installed, so skip the check + if (type === DependenceTypes.python3) { + if ( + isGitHubUrl(trimmedName) || + isRequirementsFile(trimmedName) || + isPyprojectToml(trimmedName) + ) { + // Return a command that will always indicate not installed + // This ensures GitHub URLs and requirements files are always installed + return 'echo ""'; + } + } + const baseCommands = { - [DependenceTypes.nodejs]: `pnpm ls -g | grep "${name}" | head -1`, - [DependenceTypes.python3]: ` - python3 -c "exec(''' -name='${name}' -try: - from importlib.metadata import version - print(version(name)) -except: - import importlib.util as u - import importlib.metadata as m - spec=u.find_spec(name) - print(name if spec else '') -''')"`, - [DependenceTypes.linux]: getOsTypeSync() === 'Alpine' - ? `apk info -es ${name}` - : maybeSudo(`dpkg-query -s ${name}`), + [DependenceTypes.nodejs]: `pnpm ls -g | grep "${trimmedName}" | head -1`, + [DependenceTypes.python3]: `${getPythonExecutable()} -c ${shellQuote( + `from importlib.metadata import version\nimport importlib.util as u\nname=${JSON.stringify( + trimmedName, + )}\ntry:\n print(version(name))\nexcept Exception:\n print(name if u.find_spec(name) else '')`, + )}`, + [DependenceTypes.linux]: + getOsTypeSync() === 'Alpine' + ? `apk info -es ${trimmedName}` + : maybeSudo(`dpkg-query -s ${trimmedName}`), }; return baseCommands[type]; } export function getInstallCommand(type: DependenceTypes, name: string): string { + if (type === DependenceTypes.python3) { + const prefix = + PYTHON_INSTALL_DIR && !PYTHON_VENV_DIR + ? ` --prefix=${shellQuote(PYTHON_INSTALL_DIR)}` + : ''; + const pip = `${getPythonPip()} install --disable-pip-version-check${ + PYTHON_VENV_DIR ? '' : ' --root-user-action=ignore' + }${prefix}`; + name = name.trim(); + if (isRequirementsFile(name) || isPyprojectToml(name)) { + const file = [config.scriptPath, config.repoPath] + .filter(Boolean) + .map((root) => resolveFileAccess(root, [name])) + .find((file) => { + if (!file) return false; + try { + return statSync(file).isFile(); + } catch { + return false; + } + }); + if (!file) + throw new Error( + 'Python dependency file must be inside scripts or repo', + ); + return isRequirementsFile(name) + ? `${pip} -r ${shellQuote(file)}` + : `${pip} ${shellQuote(path.dirname(file))}`; + } + return `${pip} -- ${shellQuote(name)}`; + } const baseCommands = { [DependenceTypes.nodejs]: 'pnpm add -g', - [DependenceTypes.python3]: - 'pip3 install --disable-pip-version-check --root-user-action=ignore', - [DependenceTypes.linux]: getOsTypeSync() === 'Alpine' - ? 'apk add --no-check-certificate' - : maybeSudo('apt-get install -y'), + [DependenceTypes.linux]: + getOsTypeSync() === 'Alpine' + ? 'apk add --no-check-certificate' + : maybeSudo('apt-get install -y'), }; - - let command = baseCommands[type]; - - if (type === DependenceTypes.python3 && PYTHON_INSTALL_DIR) { - command = `${command} --prefix=${PYTHON_INSTALL_DIR}`; - } - - return `${command} ${name.trim()}`; + return `${baseCommands[type]} ${name.trim()}`; } export function getUninstallCommand( type: DependenceTypes, name: string, ): string { + if (type === DependenceTypes.python3) { + name = name.trim(); + if (isRequirementsFile(name)) { + const file = [config.scriptPath, config.repoPath] + .filter(Boolean) + .map((root) => resolveFileAccess(root, [name])) + .find((file) => { + if (!file) return false; + try { + return statSync(file).isFile(); + } catch { + return false; + } + }); + if (!file) + throw new Error( + 'Python dependency file must be inside scripts or repo', + ); + return `${getPythonPip()} uninstall --disable-pip-version-check${ + PYTHON_VENV_DIR ? '' : ' --root-user-action=ignore' + } -y -r ${shellQuote(file)}`; + } + if (isPyprojectToml(name) || isGitHubUrl(name)) { + // A source URL/project can install several distributions and has no safe + // inverse. Removing its panel entry leaves installed packages intact. + return `printf '%s\\n' ${shellQuote( + 'Remove installed Python distributions separately by package name.', + )}`; + } + return `${getPythonPip()} uninstall --disable-pip-version-check${ + PYTHON_VENV_DIR ? '' : ' --root-user-action=ignore' + } -y -- ${shellQuote(name)}`; + } const baseCommands = { [DependenceTypes.nodejs]: 'pnpm remove -g', - [DependenceTypes.python3]: - 'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y', - [DependenceTypes.linux]: getOsTypeSync() === 'Alpine' - ? 'apk del' - : maybeSudo('apt-get remove -y'), + [DependenceTypes.linux]: + getOsTypeSync() === 'Alpine' ? 'apk del' : maybeSudo('apt-get remove -y'), }; - return `${baseCommands[type]} ${name.trim()}`; } diff --git a/back/services/dependence.ts b/back/services/dependence.ts index c33ff43f..5b756cd1 100644 --- a/back/services/dependence.ts +++ b/back/services/dependence.ts @@ -32,10 +32,11 @@ export default class DependenceService { constructor( @Inject('logger') private logger: winston.Logger, private sockService: SockService, - ) { } + ) {} public async create(payloads: Dependence[]): Promise { const tabs = payloads.map((x) => { + getInstallCommand(x.type, x.name); const tab = new Dependence({ ...x, status: DependenceStatus.queued }); return tab; }); @@ -59,6 +60,7 @@ export default class DependenceService { ...other, status: DependenceStatus.queued, }); + getInstallCommand(tab.type, tab.name); const newDoc = await this.updateDb(tab); this.installDependenceOneByOne([newDoc]); return newDoc; @@ -296,6 +298,23 @@ export default class DependenceService { force: boolean = false, ) { return taskLimit.runDependeny(dependency, () => { + let validatedCommand: string; + try { + validatedCommand = isInstall + ? getInstallCommand(dependency.type, dependency.name.trim()) + : getUninstallCommand(dependency.type, dependency.name.trim()); + } catch (error) { + taskLimit.removeQueuedDependency(dependency); + return DependenceModel.update( + { + status: isInstall + ? DependenceStatus.installFailed + : DependenceStatus.removeFailed, + log: [String(error)], + }, + { where: { id: dependency.id } }, + ).then(() => null); + } return new Promise(async (resolve) => { if (taskLimit.firstDependencyId !== dependency.id) { return resolve(null); @@ -345,9 +364,7 @@ export default class DependenceService { : DependenceStatus.removing; await DependenceModel.update({ status }, { where: { id: depIds } }); - let command = isInstall - ? getInstallCommand(dependency.type, depName) - : getUninstallCommand(dependency.type, depName); + let command = validatedCommand; if (isLinuxDependence) { command = isInstall ? `${linuxCommand.install} ${depName.trim()}` @@ -470,15 +487,20 @@ export default class DependenceService { const message = '\n' + - tf('依赖%s%s,结束时间 %s,耗时 %s 秒', - actionText, - resultText, - endTime.format('YYYY-MM-DD HH:mm:ss'), - String(endTime.diff(startTime, 'second')), - ); + tf( + '依赖%s%s,结束时间 %s,耗时 %s 秒', + actionText, + resultText, + endTime.format('YYYY-MM-DD HH:mm:ss'), + String(endTime.diff(startTime, 'second')), + ); const exitStatus = isSucceed - ? (isInstall ? DependenceStatus.installed : DependenceStatus.removed) - : (isInstall ? DependenceStatus.installFailed : DependenceStatus.removeFailed); + ? isInstall + ? DependenceStatus.installed + : DependenceStatus.removed + : isInstall + ? DependenceStatus.installFailed + : DependenceStatus.removeFailed; this.sockService.sendMessage({ type: socketMessageType, message, diff --git a/docs/development/python-dependency-sources.md b/docs/development/python-dependency-sources.md new file mode 100644 index 00000000..79530bed --- /dev/null +++ b/docs/development/python-dependency-sources.md @@ -0,0 +1,7 @@ +# Python 依赖来源 + +依赖管理中可手动添加 Python 包名、`git+https://` 等 pip 支持的源 URL,或脚本/订阅仓库目录内的 requirements 文件、pyproject.toml 路径。文件路径支持目录空格,必须位于持久化 scripts 或 repo 根目录内;越界与指向根目录外的符号链接会被拒绝。 + +requirements 使用 `pip install -r`,pyproject 使用完整项目目录安装,保留相对依赖文件和项目源码。拉取订阅不会自动安装仓库声明的依赖。 + +删除 requirements 条目会调用 `pip uninstall -r`。源 URL 或 pyproject 条目无法可靠逆向推断其安装的所有分发包,因此删除该条目只移除面板记录;需另外按包名卸载已安装的包,日志会明确提示。 diff --git a/package.json b/package.json index 3f5c3ffa..e36e566e 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "panel": "npm run build:back && node static/build/app.js", "gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true,snakeToCamel=false", "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", - "postinstall": "max setup 2>/dev/null || true" + "postinstall": "max setup 2>/dev/null || true", + "start:simple": "bash shell/start-simplify.sh" }, "gitHooks": { "pre-commit": "lint-staged" @@ -76,7 +77,9 @@ "@codemirror/state": "6.5.4", "@codemirror/view": "6.39.16", "proxy-addr": "2.0.8", - "websocket-driver": "0.7.5" + "websocket-driver": "0.7.5", + "brace-expansion@1": "1.1.21", + "brace-expansion@2": "2.1.7" } }, "dependencies": { @@ -171,7 +174,7 @@ "concurrently": "^7.0.0", "file-saver": "2.0.2", "lint-staged": "^13.0.3", - "moment": "2.30.1", + "moment": "2.31.0", "monaco-editor": "0.33.0", "nodemon": "^3.0.1", "prettier": "^2.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2028fca..863bffc4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,8 @@ overrides: '@codemirror/view': 6.39.16 proxy-addr: 2.0.8 websocket-driver: 0.7.5 + brace-expansion@1: 1.1.21 + brace-expansion@2: 2.1.7 dependencies: '@ant-design/plots': @@ -278,8 +280,8 @@ devDependencies: specifier: ^13.0.3 version: 13.3.0 moment: - specifier: 2.30.1 - version: 2.30.1 + specifier: 2.31.0 + version: 2.31.0 monaco-editor: specifier: 0.33.0 version: 0.33.0 @@ -876,7 +878,7 @@ packages: '@babel/runtime': 7.26.0 antd: 4.24.16(react-dom@18.3.1)(react@18.3.1) classnames: 2.5.1 - moment: 2.30.1 + moment: 2.31.0 rc-util: 5.43.0(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -5369,7 +5371,7 @@ packages: fast-deep-equal: 3.1.3 intl: 1.2.5 lodash: 4.17.21 - moment: 2.30.1 + moment: 2.31.0 qiankun: 2.10.16 react-intl: 3.12.1(react@18.3.1) react-redux: 8.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1)(redux@4.2.1) @@ -5866,7 +5868,7 @@ packages: classnames: 2.5.1 copy-to-clipboard: 3.3.3 lodash: 4.17.21 - moment: 2.30.1 + moment: 2.31.0 rc-cascader: 3.7.3(react-dom@18.3.1)(react@18.3.1) rc-checkbox: 3.0.1(react-dom@18.3.1)(react@18.3.1) rc-collapse: 3.4.2(react-dom@18.3.1)(react@18.3.1) @@ -6357,18 +6359,6 @@ packages: big-integer: 1.6.52 dev: true - /brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - /brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - dependencies: - balanced-match: 1.0.2 - dev: true - /braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -8662,7 +8652,7 @@ packages: /file-stream-rotator@0.6.1: resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} dependencies: - moment: 2.30.1 + moment: 2.31.0 dev: false /fill-range@7.1.1: @@ -10733,11 +10723,12 @@ packages: /moment-timezone@0.5.46: resolution: {integrity: sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==} dependencies: - moment: 2.30.1 + moment: 2.31.0 dev: false - /moment@2.30.1: - resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + /moment@2.31.0: + resolution: + integrity: sha512-0acOTfMiWOheYS4eoWb80yYMb/JLvVv9SHbs2PehaDzfUG0Bw855SKyk0IKTnPGa5+U2bmi3W68l1+sGLX/pvw== /monaco-editor@0.33.0: resolution: {integrity: sha512-VcRWPSLIUEgQJQIE0pVT8FcGBIgFoxz7jtqctE+IiCxWugD0DwgyQBcZBhdSrdMC84eumoqMZsGl2GTreOzwqw==} @@ -12724,7 +12715,7 @@ packages: classnames: 2.5.1 date-fns: 2.30.0 dayjs: 1.11.13 - moment: 2.30.1 + moment: 2.31.0 rc-trigger: 5.3.4(react-dom@18.3.1)(react@18.3.1) rc-util: 5.43.0(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 @@ -13951,7 +13942,7 @@ packages: dottie: 2.0.6 inflection: 1.13.4 lodash: 4.17.21 - moment: 2.30.1 + moment: 2.31.0 moment-timezone: 0.5.46 pg-connection-string: 2.7.0 retry-as-promised: 7.0.4 diff --git a/shell/start-simplify.sh b/shell/start-simplify.sh new file mode 100644 index 00000000..f016ada5 --- /dev/null +++ b/shell/start-simplify.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -e + +: "${QL_DIR:?Please set QL_DIR}" +: "${QL_DATA_DIR:?Please set QL_DATA_DIR}" +[[ "$QL_DIR" = /* && "$QL_DATA_DIR" = /*/data ]] || { echo "QL_DIR and QL_DATA_DIR must be absolute paths (data directory ends in /data)" >&2; exit 1; } +command="$1" + +export PNPM_HOME="${PNPM_HOME:-${QL_DATA_DIR}/dep_cache/node}" +if [[ -n "${PYTHON_VENV_DIR:-}" ]]; then + [[ "$PYTHON_VENV_DIR" = /* ]] || PYTHON_VENV_DIR="${QL_DIR}/${PYTHON_VENV_DIR}" + export PYTHON_VENV_DIR + if [[ ! -x "$PYTHON_VENV_DIR/bin/python3" ]]; then + python3 -m venv "$PYTHON_VENV_DIR" + fi + [[ -x "$PYTHON_VENV_DIR/bin/python3" ]] || { echo "Invalid Python venv" >&2; exit 1; } + unset PYTHON_HOME PYTHONUSERBASE PYTHONPATH + export PATH="${PYTHON_VENV_DIR}/bin:${PATH}:${PNPM_HOME}" + export PIP_CACHE_DIR="${PYTHON_VENV_DIR}/pip" +else + export PYTHON_SHORT_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') + export PYTHON_HOME="${PYTHON_HOME:-${QL_DATA_DIR}/dep_cache/python3}" + export PYTHONUSERBASE="$PYTHON_HOME" + export PATH="${PATH}:${PNPM_HOME}:${PYTHON_HOME}/bin" + 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" +fi +export NODE_PATH="${NODE_PATH:-/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules}" +mkdir -p "$PIP_CACHE_DIR" + +cd "${QL_DIR}" +if [[ ! -e .env ]]; then + # Preserve user configuration, including a file created concurrently. + (umask 077; set -C; cat .env.example > .env) +fi +chmod u+x "${QL_DIR}"/shell/*.sh + +. "${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 657f7aff..5553c359 100644 --- a/shell/start.sh +++ b/shell/start.sh @@ -77,65 +77,13 @@ if [[ $command != "reload" ]]; then npm install -g pnpm@8.3.1 pm2 ts-node typescript@5 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 - -export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin -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 - -if [[ $command != "reload" ]]; then - pip3 install --prefix ${PYTHON_HOME} requests -fi - -cd "${QL_DIR}" -if [[ ! -e .env ]]; then - # Preserve user configuration, including a file created concurrently. - (umask 077; set -C; cat .env.example > .env) -fi -chmod 777 ${QL_DIR}/shell/*.sh - -. ${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 & +if [[ "$command" != "reload" ]]; then + if [[ -n "${PYTHON_VENV_DIR:-}" ]]; then + [[ "$PYTHON_VENV_DIR" = /* ]] || export PYTHON_VENV_DIR="${QL_DIR}/${PYTHON_VENV_DIR}" + [[ -x "$PYTHON_VENV_DIR/bin/python3" ]] || python3 -m venv "$PYTHON_VENV_DIR" + "$PYTHON_VENV_DIR/bin/python3" -m pip install requests + else + pip3 install --prefix "${PYTHON_HOME:-${QL_DATA_DIR}/dep_cache/python3}" requests 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" "🎉 启动成功!" +exec /bin/bash "${QL_DIR}/shell/start-simplify.sh" "$@" diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 862561c3..9b2382a1 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -705,5 +705,7 @@ "发送状态": "Delivery status", "通知来源": "Notification source", "脚本通知": "Script notification", - "系统通知": "System notification" + "系统通知": "System notification", + "Python支持多种安装方式:\n1. 包名(如:requests)\n2. GitHub链接(如:git+https://github.com/user/repo.git)\n3. requirements文件路径(如:path/to/requirements.txt)\n4. pyproject.toml文件路径": "Python supports package names, Git URLs, and existing requirements.txt or pyproject.toml files under the scripts or repository directory.", + "支持包名、GitHub链接、requirements.txt或pyproject.toml路径": "Package name, Git URL, requirements.txt or pyproject.toml path" } diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index e97138fa..ae20332b 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -705,5 +705,7 @@ "发送状态": "发送状态", "通知来源": "通知来源", "脚本通知": "脚本通知", - "系统通知": "系统通知" + "系统通知": "系统通知", + "Python支持多种安装方式:\n1. 包名(如:requests)\n2. GitHub链接(如:git+https://github.com/user/repo.git)\n3. requirements文件路径(如:path/to/requirements.txt)\n4. pyproject.toml文件路径": "Python支持多种安装方式:\n1. 包名(如:requests)\n2. GitHub链接(如:git+https://github.com/user/repo.git)\n3. requirements文件路径(如:path/to/requirements.txt)\n4. pyproject.toml文件路径", + "支持包名、GitHub链接、requirements.txt或pyproject.toml路径": "支持包名、GitHub链接、requirements.txt或pyproject.toml路径" } diff --git a/src/pages/dependence/modal.tsx b/src/pages/dependence/modal.tsx index 91891faf..6fbf0fc6 100644 --- a/src/pages/dependence/modal.tsx +++ b/src/pages/dependence/modal.tsx @@ -22,6 +22,9 @@ const DependenceModal = ({ }) => { const [form] = Form.useForm(); const [loading, setLoading] = useState(false); + const [selectedType, setSelectedType] = useState( + DependenceTypes[defaultType as any], + ); const handleOk = async (values: any) => { setLoading(true); @@ -90,7 +93,7 @@ const DependenceModal = ({ label={intl.get('依赖类型')} initialValue={DependenceTypes[defaultType as any]} > - setSelectedType(value)}> {config.dependenceTypes.map((x, i) => (