fix(python): share safe startup and honor explicit virtual environments

This commit is contained in:
whyour
2026-10-12 00:23:20 +08:00
481 changed files with 52444 additions and 4280 deletions
+108 -95
View File
@@ -10,16 +10,61 @@ create_token() {
}
get_token() {
if [[ -f $file_auth_token ]]; then
__ql_token__=$(cat $file_auth_token | jq -r .value)
local expiration=$(cat $file_auth_token | jq -r .expiration)
local currentTimeStamp=$(date +%s)
if [[ $currentTimeStamp -ge $expiration ]]; then
create_token
local token_data expiration token_value
local currentTimeStamp=${EPOCHSECONDS:-$(date +%s)}
if [[ -f "$file_auth_token" ]] && token_data=$(jq -er \
'select((.expiration | type) == "number" and (.value | type) == "string" and (.value | length) > 0 and (.value | test("[\r\n]") | not)) | .expiration, .value' \
"$file_auth_token" 2>/dev/null); then
expiration=${token_data%%$'\n'*}
token_value=${token_data#*$'\n'}
if [[ "$expiration" =~ ^[1-9][0-9]{0,10}$ && -n "$token_value" && "$token_value" != *$'\n'* && "$token_value" != *$'\r'* ]] && \
(( currentTimeStamp < expiration )); then
__ql_token__=$token_value
return 0
fi
else
create_token
fi
create_token
}
# Check the cached token before every authenticated API request. A rejected
# token is refreshed and retried once; other failures are never retried.
# Arguments are curl options, including the URL, method and optional body.
ql_api_request() {
local api=""
local attempt
get_token || return $?
for attempt in 1 2; do
api=$(curl -s --noproxy "*" \
-H "Authorization: Bearer ${__ql_token__}" \
-H "Content-Type: application/json;charset=UTF-8" \
"$@" \
--compressed)
if [[ "$attempt" == 2 || "$api" == '{"code":200}' ]] ||
[[ "$(jq -r '.code' <<< "$api" 2>/dev/null)" != 401 ]]; then
break
fi
create_token || return $?
done
printf '%s' "$api"
}
# Read the status response once; preserve multiline error messages and the
# legacy code/message variables used by the shell API callers.
ql_parse_status_response() {
# Both task status and statistics endpoints normally return this exact body.
# Match the whole document; all other JSON and malformed responses still
# use jq. Keep its missing-message result for existing callers.
if [[ "$1" == '{"code":200}' ]]; then
code=200
message=null
return 0
fi
local parsed
code=""
message=""
parsed=$(jq -r '.code, .message' <<< "$1") || return $?
code=${parsed%%$'\n'*}
message=${parsed#*$'\n'}
}
add_cron_api() {
@@ -40,19 +85,16 @@ add_cron_api() {
sub_id="null"
fi
local api=$(
curl -s --noproxy "*" "http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
-H "Authorization: Bearer ${__ql_token__}" \
-H "Content-Type: application/json;charset=UTF-8" \
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
--compressed
)
local api=$(ql_api_request \
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
-X POST)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 添加成功"
t '%s -> 添加成功' "$name"
else
echo -e "$name -> 添加失败(${message})"
t '%s -> 添加失败(%s)' "$name" "$message"
fi
}
@@ -70,20 +112,16 @@ update_cron_api() {
local id="$4"
fi
local api=$(
curl -s --noproxy "*" "http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
-X 'PUT' \
-H "Authorization: Bearer ${__ql_token__}" \
-H "Content-Type: application/json;charset=UTF-8" \
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed
)
local api=$(ql_api_request \
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
-X PUT \
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"id\":\"$id\"}")
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 更新成功"
t '%s -> 更新成功' "$name"
else
echo -e "$name -> 更新失败(${message})"
t '%s -> 更新失败(%s)' "$name" "$message"
fi
}
@@ -97,40 +135,32 @@ update_cron_command_api() {
local id="$2"
fi
local api=$(
curl -s --noproxy "*" "http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
-X 'PUT' \
-H "Authorization: Bearer ${__ql_token__}" \
-H "Content-Type: application/json;charset=UTF-8" \
--data-raw "{\"command\":\"${command//\"/\\\"}\",\"id\":\"$id\"}" \
--compressed
)
local api=$(ql_api_request \
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
-X PUT \
--data-raw "{\"command\":\"${command//\"/\\\"}\",\"id\":\"$id\"}")
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$command -> 更新成功"
t '%s -> 更新成功' "$command"
else
echo -e "$command -> 更新失败(${message})"
t '%s -> 更新失败(%s)' "$command" "$message"
fi
}
del_cron_api() {
local ids="$1"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
-X 'DELETE' \
-H "Authorization: Bearer ${__ql_token__}" \
-H "Content-Type: application/json;charset=UTF-8" \
--data-raw "[$ids]" \
--compressed
)
local api=$(ql_api_request \
"http://localhost:${ql_port}/open/crons?t=$currentTimeStamp" \
-X DELETE \
--data-raw "[$ids]")
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "成功"
t '成功'
else
echo -e "失败(${message})"
t '失败(%s)' "$message"
fi
}
@@ -141,17 +171,18 @@ update_cron() {
local logPath="$4"
local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://localhost:${ql_port}/open/crons/status?t=$currentTimeStamp" \
-X 'PUT' \
-H "Authorization: Bearer ${__ql_token__}" \
-H "Content-Type: application/json;charset=UTF-8" \
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
local exitCode="${7:-}"
local currentTimeStamp=${EPOCHSECONDS:-$(date +%s)}
local dataRaw="{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime"
if [[ -n $exitCode ]]; then
dataRaw="${dataRaw},\"exit_code\":$exitCode"
fi
dataRaw="${dataRaw}}"
local api=$(ql_api_request \
"http://localhost:${ql_port}/open/crons/status?t=$currentTimeStamp" \
-X PUT \
--data-raw "$dataRaw")
ql_parse_status_response "$api" || true
if [[ $code != 200 ]]; then
if [[ ! $message ]]; then
message="$api"
@@ -164,32 +195,25 @@ notify_api() {
local title="$1"
local content="$2"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://localhost:${ql_port}/open/system/notify?t=$currentTimeStamp" \
-X 'PUT' \
-H "Authorization: Bearer ${__ql_token__}" \
-H "Content-Type: application/json;charset=UTF-8" \
--data-raw "{\"title\":\"${title//\"/\\\"}\",\"content\":\"${content//\"/\\\"}\"}" \
--compressed
)
local api=$(ql_api_request \
"http://localhost:${ql_port}/open/system/notify?t=$currentTimeStamp" \
-X PUT \
--data-raw "{\"title\":\"${title//\"/\\\"}\",\"content\":\"${content//\"/\\\"}\"}")
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "通知发送成功🎉"
t '通知发送成功🎉'
else
echo -e "通知失败(${message})"
t '通知失败(%s)' "$message"
fi
}
find_cron_api() {
local params="$1"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://localhost:${ql_port}/open/crons/detail?$params&t=$currentTimeStamp" \
-H "Authorization: Bearer ${__ql_token__}" \
-H "Content-Type: application/json;charset=UTF-8" \
--compressed
)
local api=$(ql_api_request \
"http://localhost:${ql_port}/open/crons/detail?$params&t=$currentTimeStamp" \
-X GET)
data=$(echo "$api" | jq -r .data)
if [[ $data == 'null' ]]; then
echo -e ""
@@ -203,20 +227,16 @@ update_auth_config() {
local body="$1"
local tip="$2"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://localhost:${ql_port}/open/system/auth/reset?t=$currentTimeStamp" \
-X 'PUT' \
-H "Authorization: Bearer ${__ql_token__}" \
-H "Content-Type: application/json;charset=UTF-8" \
--data-raw "{$body}" \
--compressed
)
local api=$(ql_api_request \
"http://localhost:${ql_port}/open/system/auth/reset?t=$currentTimeStamp" \
-X PUT \
--data-raw "{$body}")
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "${tip}成功🎉"
t '%s成功🎉' "$tip"
else
echo -e "${tip}失败(${message})"
t '%s失败(%s)' "$tip" "$message"
fi
}
@@ -226,16 +246,11 @@ record_cron_stat() {
local elapsed="${3:-0}"
[[ $ref_id ]] && [[ $ref_id -gt 0 ]] 2>/dev/null || return
local api=$(
curl -s --noproxy "*" "http://localhost:${ql_port:-5700}/open/dashboard/record" \
local api=$(ql_api_request \
"http://localhost:${ql_port:-5700}/open/dashboard/record" \
-X POST \
-H "Authorization: Bearer ${__ql_token__}" \
-H "Content-Type: application/json;charset=UTF-8" \
--data-raw "{\"ref_id\":$ref_id,\"code\":$exit_code,\"elapsed\":$elapsed}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
--data-raw "{\"ref_id\":$ref_id,\"code\":$exit_code,\"elapsed\":$elapsed}")
ql_parse_status_response "$api" || true
if [[ $code != 200 ]]; then
if [[ ! $message ]]; then
message="$api"
@@ -243,5 +258,3 @@ record_cron_stat() {
echo -e "${message}"
fi
}
get_token
+9 -9
View File
@@ -8,7 +8,7 @@ else
repo_path="${dir_repo}/diybot"
fi
echo -e "\n1、安装bot依赖...\n"
t '\n1、安装bot依赖...\n'
os_name="${QL_OS_TYPE:-}"
if [ -z "$os_name" ]; then
os_name=$(source /etc/os-release && echo "$ID")
@@ -28,13 +28,13 @@ case "$os_name" in
$SUDO apt-get install -y gcc python3-dev musl-dev zlib1g-dev libjpeg-dev libfreetype-dev
;;
*)
echo -e "暂不支持此系统 $os_name"
t '暂不支持此系统 %s' "$os_name"
exit 1
;;
esac
echo -e "\nbot依赖安装成功...\n"
t '\nbot依赖安装成功...\n'
echo -e "2、下载bot所需文件...\n"
t '2、下载bot所需文件...\n'
if [[ ! -d ${repo_path}/.git ]]; then
rm -rf ${repo_path}
git_clone_scripts ${url} ${repo_path} "main"
@@ -44,9 +44,9 @@ cp -rf "$repo_path/jbot" $dir_data
if [[ ! -f "$dir_config/bot.json" ]]; then
cp -f "$repo_path/config/bot.json" "$dir_config"
fi
echo -e "\nbot文件下载成功...\n"
t '\nbot文件下载成功...\n'
echo -e "3、安装python3依赖...\n"
t '3、安装python3依赖...\n'
cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
cd $dir_data
@@ -56,11 +56,11 @@ cat requirements.txt | while read LREAD; do
fi
done
echo -e "\npython3依赖安装成功...\n"
t '\npython3依赖安装成功...\n'
echo -e "4、启动bot程序...\n"
t '4、启动bot程序...\n'
make_dir $dir_log/bot
cd $dir_data
ps -eo pid,command | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null
nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 &
echo -e "bot启动成功...\n"
t 'bot启动成功...\n'
+76 -34
View File
@@ -1,70 +1,112 @@
#!/usr/bin/env bash
reset_env() {
echo -e "---> 1. 开始检测配置文件\n"
t '---> 1. 开始检测配置文件\n'
fix_config
echo -e "---> 配置文件检测完成\n"
t '---> 配置文件检测完成\n'
echo -e "---> 2. 开始安装青龙依赖\n"
t '---> 2. 开始安装青龙依赖\n'
npm_install_2 $dir_root
echo -e "---> 青龙依赖安装完成\n"
t '---> 青龙依赖安装完成\n'
echo -e "---> 脚本依赖安装完成\n"
t '---> 脚本依赖安装完成\n'
}
copy_dep() {
echo -e "---> 1. 复制通知文件\n"
echo -e "---> 复制一份 $file_notify_py_sample 为 $file_notify_py\n"
t '---> 1. 复制通知文件\n'
t '---> 复制一份 %s 为 %s\n' "$file_notify_py_sample" "$file_notify_py"
cp -fv $file_notify_py_sample $file_notify_py
echo
echo -e "---> 复制一份 $file_notify_js_sample 为 $file_notify_js\n"
t '---> 复制一份 %s 为 %s\n' "$file_notify_js_sample" "$file_notify_js"
cp -fv $file_notify_js_sample $file_notify_js
echo -e "---> 通知文件复制完成\n"
t '---> 通知文件复制完成\n'
}
pm2_log() {
echo -e "---> pm2日志"
local panelOut="/root/.pm2/logs/qinglong-out.log"
local panelError="/root/.pm2/logs/qinglong-error.log"
tail -n 300 "$panelOut"
tail -n 300 "$panelError"
t '---> 服务诊断信息'
pm2 status || true
local systemLogDir="$dir_data/syslog"
local latestSystemLog
latestSystemLog=$(find "$systemLogDir" -maxdepth 1 -type f -name '*.log' 2>/dev/null | sort | tail -n 1)
if [[ -n "$latestSystemLog" ]]; then
t '---> 最近的系统日志: %s' "$latestSystemLog"
tail -n 300 "$latestSystemLog"
else
t '---> 未找到系统日志: %s' "$systemLogDir"
fi
if [[ "$QL_CONTAINER" == "true" ]]; then
t '---> 容器内 PM2 日志不落盘;早期启动错误请执行 docker logs --tail 300 <容器名>'
return
fi
local pm2Home="${PM2_HOME:-$HOME/.pm2}"
local panelOut="$pm2Home/logs/qinglong-out.log"
local panelError="$pm2Home/logs/qinglong-error.log"
[[ -f "$panelOut" ]] && tail -n 300 "$panelOut"
[[ -f "$panelError" ]] && tail -n 300 "$panelError"
}
check_ql() {
local api=$(curl -s --noproxy "*" "http://localhost:${ql_port}")
echo -e "\n=====> 检测面板\n\n$api\n"
local basePath="${ql_base_url%/}"
local api
local attempt
for ((attempt = 1; attempt <= 10; attempt++)); do
api=$(curl -s --max-time 2 --noproxy "*" "http://localhost:${ql_port}${basePath}/")
[[ $api =~ "<div id=\"root\"></div>" ]] && break
sleep 1
done
t '\n=====> 检测面板'
echo -e "\n\n$api\n"
if [[ $api =~ "<div id=\"root\"></div>" ]]; then
echo -e "=====> 面板服务启动正常\n"
t '=====> 面板服务启动正常\n'
else
t '=====> 面板服务启动异常,请检查上方诊断信息\n'
return 1
fi
}
check_pm2() {
pm2_log
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://localhost:${ql_port}/api/system?t=$currentTimeStamp" \
-H 'Accept: */*' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
-H "Referer: http://localhost:${ql_port}/crontab" \
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
--compressed
)
echo -e "\n=====> 检测后台\n\n$api\n"
if [[ $api =~ "{\"code\"" ]]; then
echo -e "=====> 后台服务启动正常\n"
local basePath="${ql_base_url%/}"
local api
local attempt
for ((attempt = 1; attempt <= 10; attempt++)); do
api=$(
curl -s --max-time 2 --noproxy "*" "http://localhost:${ql_port}${basePath}/api/health?t=$currentTimeStamp" \
-H 'Accept: */*' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
-H "Referer: http://localhost:${ql_port}/crontab" \
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
--compressed
)
[[ $api == *'"code":200'* && $api == *'"status":"ok"'* ]] && break
sleep 1
done
t '\n=====> 检测后台'
echo -e "\n\n$api\n"
if [[ $api == *'"code":200'* && $api == *'"status":"ok"'* ]]; then
t '=====> 后台服务启动正常\n'
else
pm2_log
t '=====> 后台服务启动异常,请检查上方诊断信息\n'
return 1
fi
}
main() {
echo -e "=====> 开始检测"
npm i -g pnpm@8.3.1 pm2 ts-node
t '=====> 开始检测'
npm i -g pnpm@8.3.1 pm2 ts-node typescript@5
reset_env
copy_dep
check_ql
check_pm2
reload_pm2
echo -e "\n=====> 检测结束\n"
local checkStatus=0
check_ql || checkStatus=1
check_pm2 || checkStatus=1
t '\n=====> 检测结束\n'
return $checkStatus
}
main
-12
View File
@@ -1,12 +0,0 @@
#!/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
+111
View File
@@ -0,0 +1,111 @@
# English language pack
declare -gA LANG_MESSAGES=(
['任务随机延迟 %s 秒,将于 %s 开始,配置文件参数 RandomDelay 置空可取消延迟\n']='Task delayed %s seconds, will start at %s. Set RandomDelay to empty to cancel delay\n'
['开始执行...\n']='Starting execution...\n'
['已停止']='Stopped'
['完成']='Completed'
['失败(退出码 %s)']='Failed (exit code %s)'
['安装 %s 依赖包...\n']='Installing %s dependencies...\n'
['开始拉取仓库 %s 到 %s\n']='Cloning repository %s to %s\n'
['添加成功']='Added successfully'
['添加失败(%s)']='Add failed (%s)'
['更新成功']='Updated successfully'
['更新失败(%s)']='Update failed (%s)'
['成功']='Success'
['失败(%s)']='Failed (%s)'
['通知发送成功🎉']='Notification sent successfully 🎉'
['通知失败(%s)']='Notification failed (%s)'
['当前有以下脚本可以运行:']='Available scripts:'
['暂无脚本可以执行']='No scripts available'
['警告:工作目录不存在 %s']='Warning: working directory does not exist: %s'
['\n缺少并发运行的环境变量参数']='\nMissing concurrency environment variable'
['\n缺少单独运行的参数 task xxx.js desi Test']='\nMissing parameter: task xxx.js desi Test'
['暂不支持此系统 %s']='Unsupported system: %s'
['检测到 pm2 服务正在运行']='pm2 service is running'
['npm 模块位置: %s']='npm module location: %s'
['导入数据成功 %s']='Data imported successfully: %s'
['导出数据成功 %s']='Data exported successfully: %s'
['当前版本: %s / 最新版本: %s']='Current: %s / Latest: %s'
['已是最新版本']='Already up to date'
['%s 个定时任务正在运行']='%s scheduled tasks running'
['执行前置命令\n']='Running pre-command\n'
['\n执行前置命令结束\n']='\nPre-command finished\n'
['\n执行后置命令\n']='\nRunning post-command\n'
['\n执行后置命令结束']='\nPost-command finished'
['警告: PM2 启动失败 (退出码: %s),可能是由于硬件不兼容']='Warning: PM2 start failed (exit code: %s), possibly due to hardware incompatibility'
['正在尝试直接使用 Node.js 启动服务...']='Attempting to start service with Node.js directly...'
['已使用 Node.js 直接启动服务 (PID: %s)']='Service started with Node.js directly (PID: %s)'
['注意: 使用此模式时,部分 PM2 管理功能将不可用']='Note: some PM2 management features are unavailable in this mode'
['## 开始执行... %s\n']='## Starting... %s\n'
['\n## 已停止 🛑... %s 耗时 %s 秒%s']='\n## Stopped 🛑... %s took %s seconds%s'
['\n## 完成 ✅... %s 耗时 %s 秒%s']='\n## Completed ✅... %s took %s seconds%s'
['\n## 失败 ❌(退出码 %s)... %s 耗时 %s 秒%s']='\n## Failed ❌(exit code %s)... %s took %s seconds%s'
['%s -> 添加成功']='%s -> Added successfully'
['%s -> 添加失败(%s)']='%s -> Add failed (%s)'
['%s -> 更新成功']='%s -> Updated successfully'
['%s -> 更新失败(%s)']='%s -> Update failed (%s)'
['%s成功🎉']='%s succeeded 🎉'
['%s失败(%s)']='%s failed (%s)'
['检测到有%s的定时任务:']='Found %s scheduled tasks:'
['\n开始尝试自动删除失效的定时任务...']='\nAttempting to remove invalid scheduled tasks...'
['\n开始尝试自动添加定时任务...']='\nAttempting to add scheduled tasks...'
['拉取 %s 成功...\n']='Pull %s succeeded...\n'
['拉取 %s 失败,请检查日志...\n']='Pull %s failed, check logs...\n'
['开始下载:%s 保存路径:%s\n']='Downloading: %s to: %s\n'
['下载 %s 成功...\n']='Download %s succeeded...\n'
['下载 %s 失败,保留之前正常下载的版本...\n']='Download %s failed, keeping previous version...\n'
['%s文件不存在,跳过执行...\n']='%s does not exist, skipping...\n'
['使用 %s 源更新...\n']='Updating using %s mirror...\n'
['更新青龙源文件成功...\n']='Qinglong source updated successfully\n'
['更新青龙源文件失败,请检查网络...\n']='Qinglong source update failed, check network\n'
['更新青龙静态资源成功...\n']='Static assets updated successfully\n'
['更新青龙静态资源失败,请检查网络...\n']='Static assets update failed, check network\n'
['\n开始检测依赖...\n']='\nChecking dependencies...\n'
['\n依赖检测安装成功...\n']='\nDependencies installed successfully\n'
['更新包下载成功...\n']='Package download succeeded\n'
['\n依赖检测安装失败,请检查网络...\n']='\nDependency installation failed, check network\n'
['\n1、安装bot依赖...\n']='\n1. Installing bot dependencies...\n'
['\nbot依赖安装成功...\n']='\nBot dependencies installed\n'
['2、下载bot所需文件...\n']='2. Downloading bot files...\n'
['\nbot文件下载成功...\n']='\nBot files downloaded\n'
['3、安装python3依赖...\n']='3. Installing python3 dependencies...\n'
['\npython3依赖安装成功...\n']='\nPython3 dependencies installed\n'
['4、启动bot程序...\n']='4. Starting bot...\n'
['bot启动成功...\n']='Bot started successfully\n'
# check.sh
['---> 1. 开始检测配置文件\n']='---> 1. Checking config...\n'
['---> 配置文件检测完成\n']='---> Config check complete\n'
['---> 2. 开始安装青龙依赖\n']='---> 2. Installing qinglong dependencies...\n'
['---> 青龙依赖安装完成\n']='---> Dependencies installed\n'
['---> 脚本依赖安装完成\n']='---> Script dependencies installed\n'
['---> 1. 复制通知文件\n']='---> 1. Copying notification files...\n'
['---> 复制一份 %s 为 %s\n']='---> Copying %s to %s\n'
['---> 通知文件复制完成\n']='---> Notification files copied\n'
['---> pm2日志']='---> pm2 log'
['---> 服务诊断信息']='---> Service diagnostics'
['---> 最近的系统日志: %s']='---> Recent system log: %s'
['---> 未找到系统日志: %s']='---> No system log found: %s'
['---> 容器内 PM2 日志不落盘;早期启动错误请执行 docker logs --tail 300 <容器名>']='---> PM2 logs are not persisted in containers; run docker logs --tail 300 <container> for early startup errors'
['\n=====> 检测面板']='\n=====> Checking panel'
['=====> 面板服务启动正常\n']='=====> Panel service running normally\n'
['=====> 面板服务启动异常,请检查上方诊断信息\n']='=====> Panel failed to start; check the diagnostics above\n'
['\n=====> 检测后台']='\n=====> Checking backend'
['=====> 后台服务启动正常\n']='=====> Backend service running normally\n'
['=====> 后台服务启动异常,请检查上方诊断信息\n']='=====> Backend failed to start; check the diagnostics above\n'
['=====> 开始检测']='=====> Starting check'
['\n=====> 检测结束\n']='\n=====> Check complete\n'
# rmlog.sh
['查询文件 %s']='Checking file: %s'
['删除中~']='Deleting...'
['正在被 %s 使用,跳过~']='In use by %s, skipping...'
['查找旧日志文件中...\n']='Looking for old log files...\n'
['删除旧日志执行完毕\n']='Old log cleanup complete\n'
# start.sh
['未找到 qinglong 模块,请先执行 npm i -g @whyour/qinglong 安装']='Module not found. Run: npm i -g @whyour/qinglong'
['请先手动设置 export QL_DIR=%s,环境变量,并手动添加到系统环境变量,然后再次执行命令 qinglong 启动服务']='Set env: export QL_DIR=%s, then run qinglong to start'
['请先手动设置数据存储目录 export QL_DATA_DIR 环境变量,目录必须以斜杠开头的绝对路径,并且以 /data 结尾,例如 /ql/data 并手动添加到系统环境变量']='Set QL_DATA_DIR (absolute path ending with /data, e.g. /ql/data)'
['QL_DATA_DIR 必须以 /data 结尾,例如 /ql/data,如果有历史数据,请新建 data 目录,把历史数据放到 data 目录中']='QL_DATA_DIR must end with /data, e.g. /ql/data'
['暂不支持此系统部署 %s']='Unsupported system for deployment: %s'
# update.sh
['命令输入错误...\n']='Invalid command...\n'
)
+111
View File
@@ -0,0 +1,111 @@
# 中文语言包(zh key → zh value 恒等,en 包做实际翻译)
declare -gA LANG_MESSAGES=(
['任务随机延迟 %s 秒,将于 %s 开始,配置文件参数 RandomDelay 置空可取消延迟\n']='任务随机延迟 %s 秒,将于 %s 开始,配置文件参数 RandomDelay 置空可取消延迟\n'
['开始执行...\n']='开始执行...\n'
['已停止']='已停止'
['完成']='完成'
['失败(退出码 %s)']='失败(退出码 %s)'
['安装 %s 依赖包...\n']='安装 %s 依赖包...\n'
['开始拉取仓库 %s 到 %s\n']='开始拉取仓库 %s 到 %s\n'
['添加成功']='添加成功'
['添加失败(%s)']='添加失败(%s)'
['更新成功']='更新成功'
['更新失败(%s)']='更新失败(%s)'
['成功']='成功'
['失败(%s)']='失败(%s)'
['通知发送成功🎉']='通知发送成功🎉'
['通知失败(%s)']='通知失败(%s)'
['当前有以下脚本可以运行:']='当前有以下脚本可以运行:'
['暂无脚本可以执行']='暂无脚本可以执行'
['警告:工作目录不存在 %s']='警告:工作目录不存在 %s'
['\n缺少并发运行的环境变量参数']='\n缺少并发运行的环境变量参数'
['\n缺少单独运行的参数 task xxx.js desi Test']='\n缺少单独运行的参数 task xxx.js desi Test'
['暂不支持此系统 %s']='暂不支持此系统 %s'
['检测到 pm2 服务正在运行']='检测到 pm2 服务正在运行'
['npm 模块位置: %s']='npm 模块位置: %s'
['导入数据成功 %s']='导入数据成功 %s'
['导出数据成功 %s']='导出数据成功 %s'
['当前版本: %s / 最新版本: %s']='当前版本: %s / 最新版本: %s'
['已是最新版本']='已是最新版本'
['%s 个定时任务正在运行']='%s 个定时任务正在运行'
['执行前置命令\n']='执行前置命令\n'
['\n执行前置命令结束\n']='\n执行前置命令结束\n'
['\n执行后置命令\n']='\n执行后置命令\n'
['\n执行后置命令结束']='\n执行后置命令结束'
['警告: PM2 启动失败 (退出码: %s),可能是由于硬件不兼容']='警告: PM2 启动失败 (退出码: %s),可能是由于硬件不兼容'
['正在尝试直接使用 Node.js 启动服务...']='正在尝试直接使用 Node.js 启动服务...'
['已使用 Node.js 直接启动服务 (PID: %s)']='已使用 Node.js 直接启动服务 (PID: %s)'
['注意: 使用此模式时,部分 PM2 管理功能将不可用']='注意: 使用此模式时,部分 PM2 管理功能将不可用'
['## 开始执行... %s\n']='## 开始执行... %s\n'
['\n## 已停止 🛑... %s 耗时 %s 秒%s']='\n## 已停止 🛑... %s 耗时 %s 秒%s'
['\n## 完成 ✅... %s 耗时 %s 秒%s']='\n## 完成 ✅... %s 耗时 %s 秒%s'
['\n## 失败 ❌(退出码 %s)... %s 耗时 %s 秒%s']='\n## 失败 ❌(退出码 %s)... %s 耗时 %s 秒%s'
['%s -> 添加成功']='%s -> 添加成功'
['%s -> 添加失败(%s)']='%s -> 添加失败(%s)'
['%s -> 更新成功']='%s -> 更新成功'
['%s -> 更新失败(%s)']='%s -> 更新失败(%s)'
['%s成功🎉']='%s成功🎉'
['%s失败(%s)']='%s失败(%s)'
['检测到有%s的定时任务:']='检测到有%s的定时任务:'
['\n开始尝试自动删除失效的定时任务...']='\n开始尝试自动删除失效的定时任务...'
['\n开始尝试自动添加定时任务...']='\n开始尝试自动添加定时任务...'
['拉取 %s 成功...\n']='拉取 %s 成功...\n'
['拉取 %s 失败,请检查日志...\n']='拉取 %s 失败,请检查日志...\n'
['开始下载:%s 保存路径:%s\n']='开始下载:%s 保存路径:%s\n'
['下载 %s 成功...\n']='下载 %s 成功...\n'
['下载 %s 失败,保留之前正常下载的版本...\n']='下载 %s 失败,保留之前正常下载的版本...\n'
['%s文件不存在,跳过执行...\n']='%s文件不存在,跳过执行...\n'
['使用 %s 源更新...\n']='使用 %s 源更新...\n'
['更新青龙源文件成功...\n']='更新青龙源文件成功...\n'
['更新青龙源文件失败,请检查网络...\n']='更新青龙源文件失败,请检查网络...\n'
['更新青龙静态资源成功...\n']='更新青龙静态资源成功...\n'
['更新青龙静态资源失败,请检查网络...\n']='更新青龙静态资源失败,请检查网络...\n'
['\n开始检测依赖...\n']='\n开始检测依赖...\n'
['\n依赖检测安装成功...\n']='\n依赖检测安装成功...\n'
['更新包下载成功...\n']='更新包下载成功...\n'
['\n依赖检测安装失败,请检查网络...\n']='\n依赖检测安装失败,请检查网络...\n'
['\n1、安装bot依赖...\n']='\n1、安装bot依赖...\n'
['\nbot依赖安装成功...\n']='\nbot依赖安装成功...\n'
['2、下载bot所需文件...\n']='2、下载bot所需文件...\n'
['\nbot文件下载成功...\n']='\nbot文件下载成功...\n'
['3、安装python3依赖...\n']='3、安装python3依赖...\n'
['\npython3依赖安装成功...\n']='\npython3依赖安装成功...\n'
['4、启动bot程序...\n']='4、启动bot程序...\n'
['bot启动成功...\n']='bot启动成功...\n'
# check.sh
['---> 1. 开始检测配置文件\n']='---> 1. 开始检测配置文件\n'
['---> 配置文件检测完成\n']='---> 配置文件检测完成\n'
['---> 2. 开始安装青龙依赖\n']='---> 2. 开始安装青龙依赖\n'
['---> 青龙依赖安装完成\n']='---> 青龙依赖安装完成\n'
['---> 脚本依赖安装完成\n']='---> 脚本依赖安装完成\n'
['---> 1. 复制通知文件\n']='---> 1. 复制通知文件\n'
['---> 复制一份 %s 为 %s\n']='---> 复制一份 %s 为 %s\n'
['---> 通知文件复制完成\n']='---> 通知文件复制完成\n'
['---> pm2日志']='---> pm2日志'
['---> 服务诊断信息']='---> 服务诊断信息'
['---> 最近的系统日志: %s']='---> 最近的系统日志: %s'
['---> 未找到系统日志: %s']='---> 未找到系统日志: %s'
['---> 容器内 PM2 日志不落盘;早期启动错误请执行 docker logs --tail 300 <容器名>']='---> 容器内 PM2 日志不落盘;早期启动错误请执行 docker logs --tail 300 <容器名>'
['\n=====> 检测面板']='\n=====> 检测面板'
['=====> 面板服务启动正常\n']='=====> 面板服务启动正常\n'
['=====> 面板服务启动异常,请检查上方诊断信息\n']='=====> 面板服务启动异常,请检查上方诊断信息\n'
['\n=====> 检测后台']='\n=====> 检测后台'
['=====> 后台服务启动正常\n']='=====> 后台服务启动正常\n'
['=====> 后台服务启动异常,请检查上方诊断信息\n']='=====> 后台服务启动异常,请检查上方诊断信息\n'
['=====> 开始检测']='=====> 开始检测'
['\n=====> 检测结束\n']='\n=====> 检测结束\n'
# rmlog.sh
['查询文件 %s']='查询文件 %s'
['删除中~']='删除中~'
['正在被 %s 使用,跳过~']='正在被 %s 使用,跳过~'
['查找旧日志文件中...\n']='查找旧日志文件中...\n'
['删除旧日志执行完毕\n']='删除旧日志执行完毕\n'
# start.sh
['未找到 qinglong 模块,请先执行 npm i -g @whyour/qinglong 安装']='未找到 qinglong 模块,请先执行 npm i -g @whyour/qinglong 安装'
['请先手动设置 export QL_DIR=%s,环境变量,并手动添加到系统环境变量,然后再次执行命令 qinglong 启动服务']='请先手动设置 export QL_DIR=%s,环境变量,并手动添加到系统环境变量,然后再次执行命令 qinglong 启动服务'
['请先手动设置数据存储目录 export QL_DATA_DIR 环境变量,目录必须以斜杠开头的绝对路径,并且以 /data 结尾,例如 /ql/data 并手动添加到系统环境变量']='请先手动设置数据存储目录 export QL_DATA_DIR 环境变量,目录必须以斜杠开头的绝对路径,并且以 /data 结尾,例如 /ql/data 并手动添加到系统环境变量'
['QL_DATA_DIR 必须以 /data 结尾,例如 /ql/data,如果有历史数据,请新建 data 目录,把历史数据放到 data 目录中']='QL_DATA_DIR 必须以 /data 结尾,例如 /ql/data,如果有历史数据,请新建 data 目录,把历史数据放到 data 目录中'
['暂不支持此系统部署 %s']='暂不支持此系统部署 %s'
# update.sh
['命令输入错误...\n']='命令输入错误...\n'
)
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
# Cache discovery only, never the installed packages. Ordinary installs at the
# same root are visible immediately. TTL bounds changes outside known inputs.
ql_node_path_cache_key() {
local pnpm_bin node_bin name directory file
pnpm_bin=$(type -P pnpm) || return 1
node_bin=$(type -P node) || return 1
{
printf '%s\n' 'v1' "$EUID" "$PWD" "$PATH" "${HOME:-}" \
"${PNPM_HOME:-}" "${XDG_CONFIG_HOME:-}" "${XDG_DATA_HOME:-}" \
"$pnpm_bin" "$node_bin"
for name in ${!npm_config_@} ${!NPM_CONFIG_@}; do
printf '%s=%s\n' "$name" "${!name}"
done
# Follow executable symlinks, detecting upgrades at the same command path.
stat -Lc '%n:%i:%s:%Y:%Z' "$pnpm_bin" "$node_bin" 2>/dev/null || \
stat -Lf '%N:%i:%z:%m:%c' "$pnpm_bin" "$node_bin" 2>/dev/null
directory=$PWD
while :; do
for file in "$directory/.npmrc" "$directory/pnpm-workspace.yaml"; do
if [[ -f "$file" ]]; then
printf '%s\n' "$file"
cat -- "$file"
printf '\n'
fi
done
[[ "$directory" == / ]] && break
directory=${directory%/*}
[[ -n "$directory" ]] || directory=/
done
for file in "${HOME:-}/.npmrc" \
"${XDG_CONFIG_HOME:-${HOME:-}/.config}/pnpm/rc" \
"${npm_config_userconfig:-${NPM_CONFIG_USERCONFIG:-/dev/null}}" \
"${npm_config_globalconfig:-${NPM_CONFIG_GLOBALCONFIG:-/dev/null}}"; do
if [[ -f "$file" ]]; then
printf '%s\n' "$file"
cat -- "$file"
printf '\n'
fi
done
} | cksum
}
ql_read_node_path_cache() {
local cache="$1" key="$2" now
local stored_key="" stored_at="" stored_path=""
now=${EPOCHSECONDS:-$(date +%s)}
if [[ -f "$cache" && ! -L "$cache" && -O "$cache" ]]; then
if ! {
IFS= read -r stored_key && IFS= read -r stored_at && IFS= read -r stored_path
} < "$cache"; then
return 1
fi
if [[ "$stored_key" == "$key" && "$stored_at" =~ ^(0|[1-9][0-9]{0,10})$ && "$stored_path" == /* ]] && \
(( now >= stored_at && now - stored_at < 60 )); then
printf '%s\n' "$stored_path"
return 0
fi
fi
return 1
}
# Keep the lock descriptor in a subshell so sourcing this helper never changes
# the caller's descriptors. A crashed refresher releases the kernel lock.
ql_refresh_node_global_path() (
local cache="$1" key="$2" now result previous_umask
# Keep one persistent lock per owner. Removing per-key lock files could
# split concurrent waiters across different inodes during cache cleanup.
local lock="${dir_tmp}/pnpm-root-${EUID}.lock"
previous_umask=$(umask)
umask 077
if type -P flock &>/dev/null && mkdir -p -- "$dir_tmp" 2>/dev/null; then
if [[ ! -L "$lock" && ( ! -e "$lock" || ( -f "$lock" && -O "$lock" ) ) ]] && \
{ exec 9>> "$lock"; } 2>/dev/null; then
# BusyBox flock (Alpine) has no -w. Both implementations support -n;
# bound contention retries and fall back immediately on other failures.
# Never remove the lock file while waiters exist.
local lock_attempt=0 lock_status
while :; do
if flock -n 9 2>/dev/null; then
if ql_read_node_path_cache "$cache" "$key"; then
return 0
fi
break
else
lock_status=$?
fi
[[ "$lock_status" == 1 && "$lock_attempt" -lt 20 ]] || break
lock_attempt=$((lock_attempt + 1))
sleep 0.1
done
fi
fi
# Private lock creation must not change pnpm's inherited creation mask.
umask "$previous_umask"
# Warm lookups never scan the directory. Retire old environment records on
# refresh, while preserving the shared lock and unrelated cache files.
find "$dir_tmp" -maxdepth 1 -type f -user "$EUID" \
-name "pnpm-root-${EUID}-*.cache" -mmin +60 -delete 2>/dev/null || true
now=${EPOCHSECONDS:-$(date +%s)}
result=$(pnpm root -g 9>&- 2>/dev/null) || return $?
# Never cache failed, empty, multiline or non-absolute answers.
if [[ "$result" == /* && "$result" != *$'\n'* && "$result" != *$'\r'* ]]; then
(
umask 077
mkdir -p -- "$dir_tmp" || exit 0
local temporary
temporary=$(mktemp "${cache}.XXXXXX") || exit 0
if printf '%s\n%s\n%s\n' "$key" "$now" "$result" > "$temporary"; then
mv -f -- "$temporary" "$cache" || rm -f -- "$temporary"
else
rm -f -- "$temporary"
fi
) 2>/dev/null
fi
printf '%s\n' "$result"
)
ql_get_node_global_path() {
if [[ "${QL_NODE_PATH_CACHE:-1}" == 0 ]]; then
pnpm root -g 2>/dev/null
return $?
fi
local key cache
key=$(ql_node_path_cache_key) || { pnpm root -g 2>/dev/null; return $?; }
# Node and crond may have different working directories/configuration.
# Preserve both discoveries instead of evicting each other on every run.
cache="${dir_tmp}/pnpm-root-${EUID}-${key// /-}.cache"
if ql_read_node_path_cache "$cache" "$key"; then
return 0
fi
ql_refresh_node_global_path "$cache" "$key"
}
+39 -35
View File
@@ -24,7 +24,13 @@ random_delay() {
done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "任务随机延迟 $delay_second 秒,配置文件参数 RandomDelay 置空可取消延迟 \n"
local start_time
if [[ $is_macos -eq 1 ]]; then
start_time=$(date -v "+${delay_second}S" "+%Y-%m-%d %H:%M:%S")
else
start_time=$(date -d "+${delay_second} seconds" "+%Y-%m-%d %H:%M:%S")
fi
t '任务随机延迟 %s 秒,将于 %s 开始,配置文件参数 RandomDelay 置空可取消延迟\n' "$delay_second" "$start_time"
sleep $delay_second
fi
}
@@ -78,7 +84,9 @@ env_str_to_array() {
}
clear_non_sh_env() {
if [[ $file_param != *.sh ]]; then
# Only automatic Node/Python preloads reconstruct the generated environment.
# Explicit interpreters and other commands must keep the shell-loaded values.
if [[ $isJsOrPythonFile == 'true' ]]; then
clear_env
fi
}
@@ -86,7 +94,16 @@ clear_non_sh_env() {
append_node_dependency_path() {
export PREV_NODE_PATH="${NODE_PATH:=}"
local pnpm_global_path=$(pnpm root -g 2>/dev/null)
# 用户依赖目录加入 NODE_PATH,替代 symlink 到 node_modules 的方式
export NODE_PATH="${NODE_PATH:+${NODE_PATH}:}${dir_dep}"
local pnpm_global_path
if [[ -f "$dir_shell/node_path_cache.sh" ]]; then
. "$dir_shell/node_path_cache.sh"
pnpm_global_path=$(ql_get_node_global_path) || true
else
pnpm_global_path=$(pnpm root -g 2>/dev/null) || true
fi
if [[ -n "$pnpm_global_path" ]]; then
export QL_NODE_GLOBAL_PATH="$pnpm_global_path"
export NODE_PATH="${NODE_PATH:+${NODE_PATH}:}${pnpm_global_path}"
@@ -104,31 +121,22 @@ enter_script_workdir() {
else
_target_dir="${dir_scripts}/${work_dir}"
fi
if [[ ! -d "${_target_dir}" ]]; then
echo -e "错误:工作目录不存在 ${_target_dir}"
exit 1
fi
cd "${_target_dir}" || {
echo -e "错误:无法进入工作目录 ${_target_dir}"
exit 1
}
# 仍然处理 file_param 中的路径前缀,确保命令路径正确
if [[ ${file_param} =~ "/" ]]; then
local script_name="${file_param##*/}"
if [[ "${use_dot_prefix}" == "true" ]]; then
file_param="./${script_name}"
else
file_param="${script_name}"
if [[ -d "${_target_dir}" ]]; then
cd "${_target_dir}"
if [[ ${file_param} =~ "/" ]]; then
local script_name="${file_param##*/}"
if [[ "${use_dot_prefix}" == "true" ]]; then
file_param="./${script_name}"
else
file_param="${script_name}"
fi
fi
return
fi
return
t '警告:工作目录不存在 %s' "${_target_dir}"
fi
# 自动检测:从 file_param 中提取脚本所在目录
cd $dir_scripts || {
echo -e "错误:无法进入 scripts 目录 ${dir_scripts}"
exit 1
}
cd $dir_scripts
if [[ ${file_param} =~ "/" ]]; then
local script_dir="${file_param%/*}"
local script_name="${file_param##*/}"
@@ -156,7 +164,7 @@ run_normal() {
if [[ $isJsOrPythonFile == 'false' ]]; then
clear_non_sh_env
fi
$timeoutCmd $which_program $file_param "${script_params[@]}"
run_task_command $which_program $file_param "${script_params[@]}"
}
handle_env_split() {
@@ -176,7 +184,7 @@ run_concurrent() {
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|" | awk '{$1=$1};1')
if [[ ! $env_param ]]; then
echo -e "\n 缺少并发运行的环境变量参数"
t '\n缺少并发运行的环境变量参数'
exit 1
fi
@@ -195,7 +203,7 @@ run_concurrent() {
export "${env_param}=${array[$i - 1]}"
clear_non_sh_env
fi
eval envParam="${env_param}" numParam="${i}" $timeoutCmd $which_program $file_param "${script_params[@]}" &>$single_log_path &
eval envParam="${env_param}" numParam="${i}" run_task_command $which_program $file_param "${script_params[@]}" &>$single_log_path &
done
wait
@@ -213,7 +221,7 @@ run_designated() {
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|" | awk '{$1=$1};1')
if [[ ! $env_param ]]; then
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test"
t '\n缺少单独运行的参数 task xxx.js desi Test'
exit 1
fi
@@ -235,7 +243,7 @@ run_designated() {
enter_script_workdir
envParam="${env_param}" numParam="${num_param}" $timeoutCmd $which_program $file_param "${script_params[@]}"
envParam="${env_param}" numParam="${num_param}" run_task_command $which_program $file_param "${script_params[@]}"
}
## 运行其他命令
@@ -291,7 +299,7 @@ run_else() {
fi
clear_non_sh_env
$timeoutCmd $which_program $file_param "$@"
run_task_command $which_program $file_param "$@"
}
check_file() {
@@ -328,10 +336,6 @@ check_nounset() {
main() {
if [[ $1 == *.js ]] || [[ $1 == *.mjs ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
if [[ $1 == *.sh ]]; then
timeoutCmd=""
fi
case $# in
1)
run_normal "$1"
@@ -382,4 +386,4 @@ if [[ $isJsOrPythonFile == 'true' ]]; then
fi
run_task_after "${task_shell_params[@]}"
clear_env
handle_task_end "${task_shell_params[@]}" "$_task_exit_code"
handle_task_end "${task_shell_params[@]}"
+19 -1
View File
@@ -1,8 +1,26 @@
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const { readFileSync } = require('fs');
const { join } = require('path');
class GrpcClient {
static #certDir = join(
process.env.QL_DATA_DIR || join(process.env.QL_DIR, 'data'),
'config/grpc',
);
static #loadTlsCredentials() {
try {
return grpc.credentials.createSsl(
readFileSync(join(GrpcClient.#certDir, 'ca.crt')),
readFileSync(join(GrpcClient.#certDir, 'client.key')),
readFileSync(join(GrpcClient.#certDir, 'client.crt')),
);
} catch {
return grpc.credentials.createInsecure();
}
}
static #config = {
protoPath: join(process.env.QL_DIR, 'back/protos/api.proto'),
serverAddress: `localhost:${process.env.GRPC_PORT || '5500'}`,
@@ -58,7 +76,7 @@ class GrpcClient {
this.#client = new apiProto.Api(
serverAddress,
grpc.credentials.createInsecure(),
GrpcClient.#loadTlsCredentials(),
grpcOptions,
);
} catch (error) {
+41
View File
@@ -0,0 +1,41 @@
import { existsSync } from 'node:fs';
import { join } from 'node:path';
const builtinModules = [
'assert', 'buffer', 'child_process', 'cluster', 'crypto', 'dgram', 'dns',
'events', 'fs', 'http', 'https', 'net', 'os', 'path', 'process', 'querystring',
'readline', 'stream', 'string_decoder', 'timers', 'tls', 'tty', 'url', 'util',
'v8', 'zlib',
];
function isBareSpecifier(specifier) {
return !specifier.startsWith('.') &&
!specifier.startsWith('/') &&
!specifier.startsWith('file:') &&
!specifier.startsWith('node:') &&
!builtinModules.includes(specifier) &&
!builtinModules.includes(specifier.split('/')[0]);
}
export function resolve(specifier, context, nextResolve) {
if (!isBareSpecifier(specifier)) {
return nextResolve(specifier, context);
}
// 解析优先级:全局 pnpm > 系统全局
const bases = [
process.env.QL_NODE_GLOBAL_PATH,
'/usr/local/lib/node_modules',
].filter(Boolean);
for (const base of bases) {
if (existsSync(join(base, specifier))) {
return nextResolve(specifier, {
...context,
parentURL: new URL(`${join(base, specifier)}/`, 'file://').href,
});
}
}
return nextResolve(specifier, context);
}
+16 -3
View File
@@ -4,6 +4,11 @@ const path = require('path');
const client = require('./client.js');
require(`./env.js`);
// 注册 ESM loader,使全局安装的包也可通过 import 导入
try {
Module.register(new URL('esm-loader.mjs', `file://${__dirname}/`).href);
} catch (_) {}
function preferGlobalNodeModules() {
const { QL_NODE_GLOBAL_PATH } = process.env;
if (!QL_NODE_GLOBAL_PATH || Module._qlGlobalPathPatched) {
@@ -93,7 +98,11 @@ function run() {
const newEnvObject = JSON.parse(envStr);
if (typeof newEnvObject === 'object' && newEnvObject !== null) {
for (const key in newEnvObject) {
if (Object.prototype.hasOwnProperty.call(newEnvObject, key)) {
if (
Object.prototype.hasOwnProperty.call(newEnvObject, key) &&
key !== 'NODE_PATH' &&
key !== 'QL_NODE_GLOBAL_PATH'
) {
process.env[key] = newEnvObject[key];
}
}
@@ -150,9 +159,13 @@ try {
run();
const { sendNotify } = require('./__ql_notify__.js');
global.QLAPI = {
notify: sendNotify,
notify(...args) {
// Keep notification dependencies out of ordinary script startup.
// require caches the module after the first notification.
const { sendNotify } = require('./__ql_notify__.js');
return sendNotify.apply(this, args);
},
...client,
};
} catch (error) {
+4 -2
View File
@@ -129,10 +129,12 @@ try:
run()
from __ql_notify__ import send
class BaseApi(Client):
def notify(self, *args, **kwargs):
# Most scripts never send a notification. Python caches this
# module after the first call, including its channel dependencies.
from __ql_notify__ import send
return send(*args, **kwargs)
QLAPI = BaseApi()
+8 -8
View File
@@ -1,26 +1,26 @@
#!/usr/bin/env bash
echo -e "开始发布"
echo -e "切换 debian 分支"
git branch -D debian
git checkout -b debian
git push --set-upstream origin debian -f
echo -e "切换master分支"
git branch -D master
git checkout -b master
git push --set-upstream origin master -f
echo -e "更新cdn文件"
ts-node-transpile-only sample/tool.ts
string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*")
version="v$string"
echo -e "当前版本$version-debian"
echo -e "当前版本$version"
echo -e "删除已经存在的本地tag"
git tag -d "$version-debian" &>/dev/null
git tag -d "$version" &>/dev/null
echo -e "删除已经存在的远程tag"
git push origin :refs/tags/$version-debian &>/dev/null
git push origin :refs/tags/$version &>/dev/null
echo -e "创建新tag"
git tag -a "$version-debian" -m "release $version-debian"
git tag -a "$version" -m "release $version"
echo -e "提交tag"
git push --tags
+5 -5
View File
@@ -22,12 +22,12 @@ remove_js_log() {
if [[ $diff_time -gt $((${days} * 86400)) ]]; then
local log_path=$(echo "$log" | sed "s,${dir_log}/,,g")
local result=$(find_cron_api "log_path=$log_path")
echo -e "查询文件 $log_path"
t '查询文件 %s' "$log_path"
if [[ -z $result ]]; then
echo -e "删除中~"
t '删除中~'
rm -vf $log
else
echo -e "正在被 $result 使用,跳过~"
t '正在被 %s 使用,跳过~' "$result"
fi
fi
done
@@ -43,8 +43,8 @@ remove_empty_dir() {
}
if [[ ${days} ]]; then
echo -e "查找旧日志文件中...\n"
t '查找旧日志文件中...\n'
remove_js_log
remove_empty_dir
echo -e "删除旧日志执行完毕\n"
t '删除旧日志执行完毕\n'
fi
+66 -27
View File
@@ -78,6 +78,16 @@ load_ql_envs() {
import_config() {
[[ -f $file_config_user ]] && . $file_config_user
[[ -f $dir_preload/lang_env.sh ]] && . $dir_preload/lang_env.sh
# 加载语言包(bash 4+ 支持 declare -A,不兼容时回退输出中文 key)
local lang=${QL_LANG:-zh}
local lang_file="$dir_shell/lang/${lang}.sh"
if [[ ${BASH_VERSINFO[0]} -ge 4 ]] && [[ -f $lang_file ]]; then
. $lang_file
elif [[ ${BASH_VERSINFO[0]} -ge 4 ]]; then
. "$dir_shell/lang/zh.sh"
fi
load_ql_envs
command_timeout_time=${CommandTimeoutTime:-""}
@@ -91,6 +101,18 @@ import_config() {
fi
}
t() {
local key="$1"
shift
local msg
if declare -p LANG_MESSAGES &>/dev/null; then
msg="${LANG_MESSAGES["$key"]}"
fi
[[ -z $msg ]] && msg="$key"
# shellcheck disable=SC2059
printf -- "$msg\n" "$@"
}
set_proxy() {
local proxy="$1"
if [[ $proxy ]]; then
@@ -225,7 +247,7 @@ npm_install_2() {
local dir_work=$1
cd $dir_work
echo -e "安装 $dir_work 依赖包...\n"
t '安装 %s 依赖包...\n' "$dir_work"
npm_install_sub
cd $dir_current
}
@@ -244,7 +266,7 @@ git_clone_scripts() {
local branch="$3"
local proxy="$4"
[[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始拉取仓库 ${uniq_path} 到 $dir\n"
t '开始拉取仓库 %s 到 %s\n' "${uniq_path}" "$dir"
set_proxy "$proxy"
@@ -277,18 +299,23 @@ reload_pm2() {
return 0
else
local exit_code=$?
echo "警告: PM2 启动失败 (退出码: $exit_code),可能是由于硬件不兼容"
echo "正在尝试直接使用 Node.js 启动服务..."
t '警告: PM2 启动失败 (退出码: %s),可能是由于硬件不兼容' "$exit_code"
t '正在尝试直接使用 Node.js 启动服务...'
# Kill any existing node processes for qinglong
pkill -f "node.*static/build/app.js" 2>/dev/null || true
# Start node directly in the background
nohup node static/build/app.js > $dir_log/qinglong.log 2>&1 &
# Start node directly in the background. In containers, keep application
# output on the container streams instead of duplicating Winston logs on disk.
if [[ "$QL_CONTAINER" == "true" ]]; then
nohup node static/build/app.js >/proc/1/fd/1 2>/proc/1/fd/2 &
else
nohup node static/build/app.js >$dir_log/qinglong.log 2>&1 &
fi
local node_pid=$!
echo "已使用 Node.js 直接启动服务 (PID: $node_pid)"
echo "注意: 使用此模式时,部分 PM2 管理功能将不可用"
t '已使用 Node.js 直接启动服务 (PID: %s)' "$node_pid"
t '注意: 使用此模式时,部分 PM2 管理功能将不可用'
return 0
fi
}
@@ -341,9 +368,16 @@ format_timestamp() {
get_env_array() {
exported_variables=()
while IFS= read -r line; do
exported_variables+=("$line")
done < <(grep '^export ' $file_env | awk '{print $2}' | cut -d= -f1)
# Preserve the legacy export-line/second-field rules without evaluating values.
local export_name_program='/^export / { name = $2; sub(/=.*/, "", name); print name }'
if [[ ${BASH_VERSINFO[0]} -ge 4 ]]; then
builtin mapfile -t exported_variables < <(awk "$export_name_program" "$file_env")
else
# macOS still ships Bash 3, which does not provide mapfile.
while IFS= read -r line; do
exported_variables+=("$line")
done < <(awk "$export_name_program" "$file_env")
fi
}
clear_env() {
@@ -360,16 +394,16 @@ handle_task_start() {
error_message=", 任务状态更新失败(${error})"
fi
fi
echo -e "## 开始执行... ${begin_time}${error_message}\n"
t '## 开始执行... %s\n' "${begin_time}${error_message}"
}
run_task_before() {
. $file_task_before "$@"
if [[ ${task_before:=} ]]; then
echo -e "执行前置命令\n"
t '执行前置命令\n'
eval "${task_before%;}"
echo -e "\n执行前置命令结束\n"
t '\n执行前置命令结束\n'
fi
}
@@ -377,22 +411,27 @@ run_task_after() {
. $file_task_after "$@"
if [[ ${task_after:=} ]]; then
echo -e "\n执行后置命令\n"
t '\n执行后置命令\n'
eval "${task_after%;}"
echo -e "\n执行后置命令结束"
t '\n执行后置命令结束'
fi
}
handle_task_end() {
local etime=$(date "+$time_format")
local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime")
local etime end_time end_timestamp
if [[ $is_macos -ne 1 && $time_format == '%Y-%m-%d %H:%M:%S' ]]; then
IFS='|' read -r end_time end_timestamp < <(date "+$time_format|%s")
else
etime=$(date "+$time_format")
end_time=$(format_time "$time_format" "$etime")
end_timestamp=$(format_timestamp "$time_format" "$etime")
fi
local diff_time=$(($end_timestamp - $begin_timestamp))
local exit_code="${@: -1}"
local exit_code="${_task_exit_code:-0}"
[[ "$diff_time" == 0 ]] && diff_time=1
if [[ $ID ]]; then
local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time")
local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time" "$exit_code")
if [[ $error ]]; then
error_message=", 状态更新失败(${error})"
fi
@@ -401,11 +440,11 @@ handle_task_end() {
record_cron_stat "$ID" "${exit_code:-0}" "$diff_time"
if [[ "${MANUAL:=}" == "true" ]]; then
echo -e "\n## 已停止 ⏹... $end_time 耗时 $diff_time 秒${error_message:=}     "
t '\n## 已停止 🛑... %s 耗时 %s 秒%s' "$end_time" "$diff_time" "${error_message:=}     "
elif [[ $exit_code -eq 0 ]]; then
echo -e "\n## 完成 ✅... $end_time 耗时 $diff_time 秒${error_message:=}     "
t '\n## 完成 ✅... %s 耗时 %s 秒%s' "$end_time" "$diff_time" "${error_message:=}     "
else
echo -e "\n## 失败 ❌ (退出码 ${exit_code})... $end_time 耗时 $diff_time 秒${error_message:=}     "
t '\n## 失败 ❌(退出码 %s)... %s 耗时 %s 秒%s' "$exit_code" "$end_time" "$diff_time" "${error_message:=}     "
fi
}
+29 -55
View File
@@ -1,68 +1,42 @@
#!/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
: "${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"
# 从 .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"
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"
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}"
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
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
. "${QL_DIR}/shell/share.sh"
. "${QL_DIR}/shell/env.sh"
log_with_style() {
local level="$1"
+14 -87
View File
@@ -12,23 +12,23 @@ if [[ ! $QL_DIR ]]; then
elif [[ -d "$pnpm_dir/@whyour/qinglong" ]]; then
QL_DIR="$pnpm_dir/@whyour/qinglong"
else
echo -e "未找到 qinglong 模块,请先执行 npm i -g @whyour/qinglong 安装"
t '未找到 qinglong 模块,请先执行 npm i -g @whyour/qinglong 安装'
fi
if [[ $QL_DIR ]]; then
echo -e "请先手动设置 export QL_DIR=$QL_DIR,环境变量,并手动添加到系统环境变量,然后再次执行命令 qinglong 启动服务"
t '请先手动设置 export QL_DIR=%s,环境变量,并手动添加到系统环境变量,然后再次执行命令 qinglong 启动服务' "$QL_DIR"
fi
exit 1
fi
if [[ ! $QL_DATA_DIR ]]; then
echo -e "请先手动设置数据存储目录 export QL_DATA_DIR 环境变量,目录必须以斜杠开头的绝对路径,并且以 /data 结尾,例如 /ql/data 并手动添加到系统环境变量"
t '请先手动设置数据存储目录 export QL_DATA_DIR 环境变量,目录必须以斜杠开头的绝对路径,并且以 /data 结尾,例如 /ql/data 并手动添加到系统环境变量'
exit 1
fi
if [[ $QL_DATA_DIR != */data ]]; then
echo -e "QL_DATA_DIR 必须以 /data 结尾,例如 /ql/data,如果有历史数据,请新建 data 目录,把历史数据放到 data 目录中"
t 'QL_DATA_DIR 必须以 /data 结尾,例如 /ql/data,如果有历史数据,请新建 data 目录,把历史数据放到 data 目录中'
exit 1
fi
@@ -69,94 +69,21 @@ if [[ $command != "reload" ]]; then
$SUDO apt-get install -y git curl wget tzdata perl openssl jq nginx procps netcat-openbsd openssh-client
;;
*)
echo -e "暂不支持此系统部署 $os_name"
t '暂不支持此系统部署 %s' "$os_name"
exit 1
;;
esac
npm install -g pnpm@8.3.1 pm2 ts-node
npm install -g pnpm@8.3.1 pm2 ts-node typescript@5
fi
# 从 .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"
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
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
# 仅将 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_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" "🎉 启动成功!"
exec /bin/bash "${QL_DIR}/shell/start-simplify.sh" "$@"
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Fork from the current Bash state: an external `bash -c` would lose private
# before-hook variables, arrays and functions used by sourced Shell scripts.
run_task_command() {
local timeout_seconds
if [[ -z ${command_timeout_time:-} ]]; then
"$@"
return $?
fi
timeout_seconds=$(awk -v value="$command_timeout_time" 'BEGIN {
if (value !~ /^([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[smhd]?$/) exit 1
unit = substr(value, length(value), 1)
factor = unit == "d" ? 86400 : unit == "h" ? 3600 : unit == "m" ? 60 : 1
printf "%.9f", (value + 0) * factor
}') || { printf 'Invalid task timeout: %s\n' "$command_timeout_time" >&2; return 125; }
if [[ $timeout_seconds == '0.000000000' ]]; then
"$@"
return $?
fi
(
timeout_dir=$(mktemp -d "${TMPDIR:-/tmp}/ql-task-timeout.XXXXXXXX") || exit 125
# Both children get private process groups. The timer and the wrapper must
# survive terminating the task, and descendants must not outlive a timeout.
set -m
( set +m; "$@" ) <&0 &
timeout_worker=$!
(
set +m
: > "$timeout_dir/ready"
sleep "$timeout_seconds" || exit 125
: > "$timeout_dir/expired"
kill -TERM -- "-$timeout_worker" 2>/dev/null || :
sleep 0.5
kill -KILL -- "-$timeout_worker" 2>/dev/null || :
) &
timeout_watcher=$!
set +m
trap 'kill -KILL -- "-$timeout_watcher" 2>/dev/null || :
if kill -TERM -- "-$timeout_worker" 2>/dev/null; then sleep 0.1; fi
kill -KILL -- "-$timeout_worker" 2>/dev/null || :
wait "$timeout_worker" 2>/dev/null || :
wait "$timeout_watcher" 2>/dev/null || :
rm -rf -- "$timeout_dir"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
trap 'exit 129' HUP
trap 'exit 131' QUIT
# Do not clean up a timer whose process group has not initialized yet.
while [[ ! -f "$timeout_dir/ready" ]]; do
kill -0 "$timeout_watcher" 2>/dev/null || exit 125
sleep 0.001
done
wait "$timeout_worker"
timeout_code=$?
if [[ -f "$timeout_dir/expired" ]]; then
# Let the timer finish escalation even if the task leader exited first.
wait "$timeout_watcher" 2>/dev/null || :
exit 124
fi
exit "$timeout_code"
)
}
+31 -11
View File
@@ -3,13 +3,28 @@
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
. $dir_shell/task-timeout.sh
trap 'single_hanle SIGINT' INT
trap 'single_hanle SIGTERM' TERM
trap 'single_hanle SIGHUP' HUP
trap 'single_hanle SIGALRM' ALRM
trap 'single_hanle SIGTSTP' TSTP
trap 'single_hanle SIGQUIT' QUIT
trap "single_hanle" 2 3 20 15 14 19 1
single_hanle() {
eval MANUAL=true handle_task_end "$@" "$cmd"
_task_exit_code="${_task_exit_code:-$?}"
[[ "$_task_exit_code" == "0" ]] && _task_exit_code="1"
eval MANUAL=true handle_task_end "$@"
exit 1
}
run_shell_script() {
# Preserve inherited variables/functions, but contain exit, exec and shell
# options so the task wrapper can run after hooks and report completion.
( . "$@" )
}
define_program() {
local file_param=$1
if [[ $file_param == *.js ]] || [[ $file_param == *.mjs ]]; then
@@ -17,7 +32,7 @@ define_program() {
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
which_program="python3"
elif [[ $file_param == *.sh ]]; then
which_program="."
which_program="run_shell_script"
elif [[ $file_param == *.ts ]]; then
which_program="ts-node-transpile-only"
else
@@ -44,8 +59,14 @@ handle_log_path() {
fi
fi
time=$(date "+$mtime_format")
log_time=$(format_log_time "$mtime_format" "$time")
if [[ $is_macos -ne 1 && $mtime_format == '%Y-%m-%d %H:%M:%S.%3N' ]]; then
# Render both representations from one clock snapshot, without parsing it
# again in a second date process.
IFS='|' read -r time log_time < <(date "+$mtime_format|%Y-%m-%d-%H-%M-%S-%3N")
else
time=$(date "+$mtime_format")
log_time=$(format_log_time "$mtime_format" "$time")
fi
if [[ -z $log_name ]]; then
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
@@ -92,11 +113,6 @@ format_params() {
mtime_format="%Y-%m-%d %H:%M:%S.%3N"
fi
timeoutCmd=""
if [[ $command_timeout_time ]]; then
if type timeout &>/dev/null; then
timeoutCmd="timeout --foreground -s 2 -k 10s $command_timeout_time "
fi
fi
# params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
# 分割 task 内置参数和脚本参数
@@ -116,7 +132,11 @@ format_params() {
}
init_begin_time() {
begin_time=$(format_time "$time_format" "$time")
if [[ $is_macos -ne 1 && $mtime_format == '%Y-%m-%d %H:%M:%S.%3N' && $time_format == '%Y-%m-%d %H:%M:%S' ]]; then
begin_time=${time%.*}
else
begin_time=$(format_time "$time_format" "$time")
fi
begin_timestamp=$(format_timestamp "$time_format" "$time")
}
+119 -113
View File
@@ -33,7 +33,7 @@ output_list_add_drop() {
local list=$1
local type=$2
if [[ -s $list ]]; then
echo -e "检测到有${type}的定时任务:"
t '检测到有%s的定时任务:' "$type"
cat $list
fi
}
@@ -45,7 +45,7 @@ del_cron() {
local path=$2
local detail=""
local ids=""
echo -e "\n开始尝试自动删除失效的定时任务..."
t '\n开始尝试自动删除失效的定时任务...'
for cron in $(cat $list_drop); do
local id=$(cat $list_crontab_user | grep -E "$cmd_task.* $cron" | perl -pe "s|.*ID=(.*) $cmd_task.* $cron\.*|\1|" | head -1 | awk -F " " '{print $1}')
if [[ $ids ]]; then
@@ -76,7 +76,7 @@ del_cron() {
add_cron() {
local list_add=$1
local path=$2
echo -e "\n开始尝试自动添加定时任务..."
t '\n开始尝试自动添加定时任务...'
local detail=""
cd $dir_scripts
for file in $(cat $list_add); do
@@ -136,10 +136,10 @@ update_repo() {
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}"
if [[ $exit_status -eq 0 ]]; then
echo -e "拉取 ${uniq_path} 成功...\n"
t '拉取 %s 成功...\n' "${uniq_path}"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron"
else
echo -e "拉取 ${uniq_path} 失败,请检查日志...\n"
t '拉取 %s 失败,请检查日志...\n' "${uniq_path}"
fi
}
@@ -160,7 +160,7 @@ update_raw() {
local raw_url="$url"
local suffix="${raw_url##*.}"
local raw_file_name="${uniq_path}.${suffix}"
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
t '开始下载:%s 保存路径:%s\n' "${raw_url}" "$dir_raw/${raw_file_name}"
set_proxy "$proxy"
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
@@ -169,7 +169,7 @@ update_raw() {
if [[ $? -eq 0 ]]; then
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
echo -e "下载 ${raw_file_name} 成功...\n"
t '下载 %s 成功...\n' "${raw_file_name}"
cd $dir_raw
local filename="raw_${raw_file_name}"
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* $filename" | perl -pe "s|.*ID=(.*) $cmd_task.* $filename\.*|\1|" | head -1 | awk -F " " '{print $1}')
@@ -196,7 +196,7 @@ update_raw() {
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
fi
else
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
t '下载 %s 失败,保留之前正常下载的版本...\n' "${raw_file_name}"
[[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
fi
@@ -207,13 +207,67 @@ run_extra_shell() {
if [[ -f $file_extra_shell ]]; then
. $file_extra_shell
else
echo -e "$file_extra_shell文件不存在,跳过执行...\n"
t '%s文件不存在,跳过执行...\n' "$file_extra_shell"
fi
}
## 查看青龙服务日志
show_service_logs() {
local lines="15"
local no_stream="false"
while [[ $# -gt 0 ]]; do
case "$1" in
--lines)
if [[ ! "${2:-}" =~ ^[0-9]+$ ]]; then
echo "ql log: --lines requires a non-negative integer" >&2
return 2
fi
lines="$2"
shift 2
;;
--lines=*)
lines="${1#--lines=}"
if [[ ! "$lines" =~ ^[0-9]+$ ]]; then
echo "ql log: --lines requires a non-negative integer" >&2
return 2
fi
shift
;;
--nostream)
no_stream="true"
shift
;;
*)
echo "ql log: unknown option: $1" >&2
return 2
;;
esac
done
local system_log_dir="$dir_data/syslog"
local latest_system_log
latest_system_log=$(ls -1t "$system_log_dir"/*.log* 2>/dev/null | head -n 1)
if [[ "$lines" != "0" ]] && [[ -f "$latest_system_log" ]]; then
tail -n "$lines" "$latest_system_log"
fi
if [[ "$no_stream" == "true" ]]; then
if [[ -z "$latest_system_log" ]]; then
echo "ql log: no system log found in $system_log_dir" >&2
return 1
fi
return 0
fi
# Winston writes the active runtime log by date. Follow the filename so
# size-based rotations are handled without involving the PM2 log files.
local current_system_log="$system_log_dir/$(date +%F).log"
tail -n 0 --retry -F "$current_system_log"
}
## 脚本用法
usage() {
echo -e "$cmd_update 命令使用方法:"
t "$cmd_update 命令使用方法:"
echo -e "1. $cmd_update update # 更新并重启青龙"
echo -e "2. $cmd_update extra # 运行自定义脚本"
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
@@ -221,105 +275,52 @@ usage() {
echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
echo -e "6. $cmd_update bot # 启动tg-bot"
echo -e "7. $cmd_update check # 检测青龙环境并修复"
echo -e "8. $cmd_update resetlet # 重置登录错误次数"
echo -e "9. $cmd_update resettfa # 禁用两步登录"
echo -e "10. $cmd_update resetpwd # 修改登录密码"
echo -e "11. $cmd_update resetname # 修改登录用户名"
echo -e "8. $cmd_update log [--lines <n>] [--nostream] # 查看青龙运行日志"
echo -e "9. $cmd_update resetlet # 重置登录错误次数"
echo -e "10. $cmd_update resettfa # 禁用两步登录"
echo -e "11. $cmd_update resetpwd # 修改登录密码"
echo -e "12. $cmd_update resetname # 修改登录用户名"
}
reload_qinglong() {
echo -e "[reload_qinglong] deleting Triggered at $(date)" >>${dir_log}/reload.log
local reload_target="${1}"
if [[ "$reload_target" == 'system' ]]; then
QlPort="${ql_port:-${QlPort:-5700}}" QlBaseUrl="${ql_base_url:-${QlBaseUrl:-/}}" node "$dir_shell/upgrade.cjs" reload
return $?
fi
# Data import and a plain service restart retain their existing behavior.
echo -e "[reload_qinglong] deleting Triggered at $(date)" >>"${dir_log}/reload.log"
sleep 3
delete_pm2
echo -e "[reload_qinglong] deleted Triggered at $(date)" >>${dir_log}/reload.log
local reload_target="${1}"
local primary_branch="master"
if [[ "${QL_BRANCH}" == "develop" ]] || [[ "${QL_BRANCH}" == "debian" ]] || [[ "${QL_BRANCH}" == "debian-dev" ]]; then
primary_branch="${QL_BRANCH}"
fi
if [[ "$reload_target" == 'system' ]]; then
rm -rf ${dir_root}/back ${dir_root}/cli ${dir_root}/docker ${dir_root}/sample ${dir_root}/shell ${dir_root}/src
mv -f ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
rm -rf $dir_static/*
mv -f ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/
cp -f $file_config_sample $dir_config/config.sample.sh
fi
if [[ "$reload_target" == 'data' ]]; then
rm -rf ${dir_data}/*
mv -f ${dir_tmp}/data/* ${dir_data}/
rm -rf "${dir_data:?}"/*
mv -f "$dir_tmp"/data/* "$dir_data"/ || return $?
fi
echo -e "[reload_qinglong] starting Triggered at $(date)" >>${dir_log}/reload.log
reload_pm2
echo -e "[reload_qinglong] started Triggered at $(date)\n" >>${dir_log}/reload.log
}
## 更新 qinglong
update_qinglong() {
rm -rf ${dir_tmp}/*
local mirror="gitee"
local downloadQLUrl="https://gitee.com/whyour/qinglong/repository/archive"
local downloadStaticUrl="https://gitee.com/whyour/qinglong-static/repository/archive"
local githubStatus=$(curl -s --noproxy "*" -m 2 -IL "https://google.com" | grep 200)
if [[ ! -z $githubStatus ]]; then
if curl --fail --silent --noproxy "*" --max-time 2 --head "https://github.com" >/dev/null; then
mirror="github"
downloadQLUrl="https://github.com/whyour/qinglong/archive/refs/heads"
downloadStaticUrl="https://github.com/whyour/qinglong-static/archive/refs/heads"
fi
echo -e "使用 ${mirror} 源更新...\n"
local primary_branch="master"
if [[ "${QL_BRANCH}" == "develop" ]] || [[ "${QL_BRANCH}" == "debian" ]] || [[ "${QL_BRANCH}" == "debian-dev" ]]; then
primary_branch="${QL_BRANCH}"
fi
wget -cqO "${dir_tmp}/ql.zip" "${downloadQLUrl}/${primary_branch}.zip"
exit_status=$?
if [[ $exit_status -eq 0 ]]; then
echo -e "更新青龙源文件成功...\n"
unzip -oq ${dir_tmp}/ql.zip -d ${dir_tmp}
update_qinglong_static
else
echo -e "更新青龙源文件失败,请检查网络...\n"
fi
t '使用 %s 源更新...\n' "$mirror"
QlPort="${ql_port:-${QlPort:-5700}}" QlBaseUrl="${ql_base_url:-${QlBaseUrl:-/}}" node "$dir_shell/upgrade.cjs" update "$mirror" "$needRestart"
}
update_qinglong_static() {
wget -cqO "${dir_tmp}/static.zip" "${downloadStaticUrl}/${primary_branch}.zip"
exit_status=$?
if [[ $exit_status -eq 0 ]]; then
echo -e "更新青龙静态资源成功...\n"
unzip -oq ${dir_tmp}/static.zip -d ${dir_tmp}
check_update_dep
# Preserve the updater's status through both tee and the final timing output.
run_update_logged() (
set -o pipefail
if [[ "$real_time" == 'true' ]]; then
"$@"
elif [[ "$no_tee" == 'true' ]]; then
"$@" >>"$file_path" 2>&1
else
echo -e "更新青龙静态资源失败,请检查网络...\n"
"$@" 2>&1 | tee -a "$file_path"
fi
}
check_update_dep() {
echo -e "\n开始检测依赖...\n"
if [[ $(diff $dir_root/package.json ${dir_tmp}/qinglong-${primary_branch}/package.json) ]]; then
npm_install_2 "${dir_tmp}/qinglong-${primary_branch}"
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n依赖检测安装成功...\n"
echo -e "更新包下载成功..."
if [[ "$needRestart" == 'true' ]]; then
reload_qinglong "system"
fi
else
echo -e "\n依赖检测安装失败,请检查网络...\n"
fi
}
)
## 对比脚本
diff_scripts() {
@@ -374,23 +375,19 @@ gen_list_repo() {
cd ${repo_path}
local cmd="find ."
local index=0
if [[ $6 ]]; then
file_extensions="$6"
if [[ $file_extensions =~ "|" ]]; then
file_extensions=$(echo $file_extensions | sed 's/|/ /g')
local extension_string="${6:-$file_extensions}"
extension_string="${extension_string//|/ }"
extension_string="${extension_string//$'\n'/ }"
local -a extensions=() find_args=()
IFS=$' \t\n' read -r -a extensions <<< "$extension_string"
local extension
for extension in "${extensions[@]}"; do
if [[ ${#find_args[@]} -gt 0 ]]; then
find_args+=(-o)
fi
fi
for extension in $file_extensions; do
if [[ $index -eq 0 ]]; then
cmd="${cmd} -name \"*.${extension}\""
else
cmd="${cmd} -o -name \"*.${extension}\""
fi
let index+=1
find_args+=(-name "*.${extension}")
done
files=$(eval $cmd | sed 's/^..//')
files=$(find . "${find_args[@]}" | sed 's/^..//')
if [[ $path ]]; then
files=$(echo "$files" | egrep "$path")
fi
@@ -403,7 +400,7 @@ gen_list_repo() {
if [[ $dependence ]]; then
cd ${repo_path}
results=$(eval $cmd | sed 's/^..//' | egrep "$dependence")
results=$(find . "${find_args[@]}" | sed 's/^..//' | egrep "$dependence")
for _file in ${results}; do
file_path=$(dirname $_file)
make_dir "${dir_scripts}/${uniq_path}/${file_path}"
@@ -471,6 +468,12 @@ main() {
local p8="${8}"
local p9="${9}"
local p10="${10}"
if [[ "$p1" == "log" ]]; then
show_service_logs "${@:2}"
return 0
fi
local log_dir="${p1}"
make_dir "$dir_log/$log_dir"
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
@@ -497,14 +500,17 @@ main() {
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
local command_status=0
case $p1 in
update)
fix_config
local needRestart=${p2:-"true"}
eval update_qinglong $cmd
run_update_logged update_qinglong
command_status=$?
;;
reload)
eval reload_qinglong "$p2" $cmd
run_update_logged reload_qinglong "$p2"
command_status=$?
;;
extra)
eval run_extra_shell $cmd
@@ -514,7 +520,7 @@ main() {
if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" "$p8" "$p9" "$p10"
else
eval echo -e "命令输入错误...\\\n" $cmd
t '命令输入错误...\n'
eval usage $cmd
fi
;;
@@ -523,7 +529,7 @@ main() {
if [[ -n $p2 ]]; then
update_raw "$p2" "$p3" "$p4" "$p5"
else
eval echo -e "命令输入错误...\\\n" $cmd
t '命令输入错误...\n'
eval usage $cmd
fi
;;
@@ -549,7 +555,7 @@ main() {
eval update_auth_config "\\\"username\\\":\\\"$p2\\\"" "重置用户名" $cmd
;;
*)
eval echo -e "命令输入错误...\\\n" $cmd
t '命令输入错误...\n'
eval usage $cmd
;;
esac
@@ -563,9 +569,9 @@ main() {
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒     " $cmd
fi
return "$command_status"
}
import_config "$@"
main "$@"
exit 0
exit $?
+8
View File
@@ -0,0 +1,8 @@
// The image updater and local CLI share one implementation and build contract.
const upgrade = require('../cli/src/internal/maintenance/upgradeArtifacts.cjs');
module.exports = upgrade;
if (require.main === module)
upgrade.main().catch((error) => {
console.error(`更新失败: ${error.message}`);
process.exitCode = 1;
});