feat: 新增 bin/install 一键安装/升级脚本并支持 CLI 双语

- bin/install:curl 一行命令按当前目录自动判断 空目录安装 / 续装 / 升级;升级时取线上最新 cmd 执行,规避旧 cmd 导致的两次升级;输出按 locale 中英双语,判空时忽略系统垃圾文件
- cmd:所有用户可见输出按 locale 中英双语(msg 查表 + (*) 占位),业务逻辑不变
- README / README_CN:安装段补充一键脚本命令、升级段补充一键命令并移除升级重试提示;删除 0.x 迁移到 1.x 章节

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
kuaifan 2026-06-24 02:45:21 +00:00
parent 0896f09878
commit e5a88c2957
4 changed files with 549 additions and 90 deletions

View File

@ -9,14 +9,6 @@ English | **[中文文档](./README_CN.md)**
- Group Number: `546574618`
## 📍 Migration from 0.x to 1.x
- Please ensure to back up your data before upgrading!
- If the upgrade fails, try running `./cmd update` multiple times.
- If you encounter "Container xxx not found" during upgrade, run `./cmd reup` and then execute `./cmd update`.
- If you see a 502 error after upgrading, run `./cmd reup` to restart the services.
- If you encounter "Application 'xxx' not installed" after upgrading, log in with the admin account and install the relevant applications from the App Store.
## Installation Requirements
- Required: `Docker v20.10+` and `Docker Compose v2.0+`
@ -27,6 +19,16 @@ English | **[中文文档](./README_CN.md)**
### Deploy Project
**Option 1: One-line script (recommended)**
Run it in an empty directory to clone and install automatically; run it inside an existing installation to check and upgrade:
```bash
curl -fsSL https://raw.githubusercontent.com/kuaifan/dootask/pro/bin/install | bash
```
**Option 2: Manual deployment**
```bash
# 1、Clone the project to your local machine or server
@ -105,11 +107,18 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
**Note: Please backup your data before upgrading!**
Recommended: use the one-line script (run it inside an existing installation; it pulls the latest code and finishes the upgrade in a single run):
```bash
curl -fsSL https://raw.githubusercontent.com/kuaifan/dootask/pro/bin/install | bash
```
Or use the local command:
```bash
./cmd update
```
* Please retry if upgrade fails across major versions.
* If you encounter 502 errors after upgrade, run `./cmd reup` to restart services.
## Project Migration

View File

@ -9,14 +9,6 @@
- QQ群号: `546574618`
## 📍 0.x 迁移到 1.x
- 升级时请务必备份好数据!
- 如果升级失败请尝试执行 `./cmd update` 重试几次。
- 如果升级中出现 `没有找到 xxx 容器` 的提示,请运行 `./cmd reup` 后再执行 `./cmd update`
- 如果升级后出现502错误请运行 `./cmd reup` 重启服务即可。
- 如果升级后出现 `应用「xxx」未安装` 的提示,请使用管理员账号进入应用商店安装相关应用。
## 安装程序
- 必须安装:`Docker v20.10+``Docker Compose v2.0+`
@ -27,6 +19,16 @@
### 部署项目
**方式一:一键脚本(推荐)**
在空目录中执行即自动克隆并安装;在已安装目录中执行则自动检查并升级:
```bash
curl -fsSL https://raw.githubusercontent.com/kuaifan/dootask/pro/bin/install | bash
```
**方式二:手动部署**
```bash
# 1、克隆项目到您的本地或服务器
@ -105,11 +107,18 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
**注意:在升级之前请备份好你的数据!**
推荐使用一键脚本升级(在已安装目录中执行,自动拉取最新代码并完成升级,无需重复执行):
```bash
curl -fsSL https://raw.githubusercontent.com/kuaifan/dootask/pro/bin/install | bash
```
或使用本地命令:
```bash
./cmd update
```
* 跨越大版本升级失败时请重试执行一次。
* 如果升级后出现502请运行 `./cmd reup` 重启服务即可。
## 迁移项目

296
bin/install Executable file
View File

@ -0,0 +1,296 @@
#!/bin/bash
#
# DooTask 一键安装 / 升级脚本
#
# 用法(在目标目录执行):
# curl -fsSL https://raw.githubusercontent.com/kuaifan/dootask/pro/bin/install | bash
#
# 脚本会根据「当前目录」自动判断该做什么,无需额外参数:
# - 空目录 : 全新安装(克隆代码到当前目录 + ./cmd install
# - 已克隆但未安装 : 继续安装(./cmd install
# - 已安装 : 检查更新,确认后用「线上最新 cmd」执行升级
# - 非空且不是 DooTask : 拒绝操作并提示(绝不在此克隆或重置)
#
# 升级一次到位的关键:升级时从「线上 raw」取最新 cmd 到临时文件执行,
# 既不依赖用户机器上那份可能过时的 cmd也不写本地 .git规避属主/权限问题),
# 真正的 git pull / 依赖 / 迁移 / 重启全部交给这份最新 cmd避免「升两次」。
#
# 输出语言:仅当 locale 明确是中文 UTF-8 时显示中文,否则一律英文。
#
set -u
# ---------- 配置 ----------
BRANCH="pro" # 全新安装默认分支(升级时跟随当前分支)
REPO_GITHUB="https://github.com/kuaifan/dootask.git"
REPO_GITEE="https://gitee.com/aipaw/dootask.git"
# raw 基址:升级时取版本号与最新 cmd 用。后期可把 RAW_PRIMARY 换成官网映射的域名。
RAW_PRIMARY="https://raw.githubusercontent.com/kuaifan/dootask" # https://<base>/<branch>/<path>
RAW_FALLBACK="https://cdn.jsdelivr.net/gh/kuaifan/dootask" # https://<base>@<branch>/<path>
# ---------- 语言判定 ----------
# 默认英文;仅当 locale 明确是「中文 UTF-8」时才用中文中文非 UTF-8 如 GBK 也用英文以免乱码)。
DT_LANG="en"
__loc="${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}"
case "$__loc" in
zh_*|zh-*|zh)
case "$__loc" in
*[Uu][Tt][Ff]*) DT_LANG="zh" ;; # 明确 UTF-8 → 中文
*.*) DT_LANG="en" ;; # 其他编码(如 .GBK→ 英文,避免乱码
*) DT_LANG="zh" ;; # 无编码后缀(裸 zh_CN→ 现代默认 UTF-8
esac
;;
esac
unset __loc
# ---------- 文案 ----------
# 调用处只写中文(动态值用 (*) 占位,顺序对应后续参数,与前端 $L 风格一致)。
# 中文环境直接用原文;英文环境在此集中查表翻译,未登记的中文原样返回。
msg() {
local tpl="$1"; shift
local out="$tpl"
if [ "$DT_LANG" != "zh" ]; then
case "$tpl" in
"成功") out="OK" ;;
"警告") out="WARN" ;;
"错误") out="ERROR" ;;
"未知") out="unknown" ;;
"git 未安装,请先安装后重试")
out="git is not installed. Please install it and retry." ;;
"curl 未安装,请先安装后重试")
out="curl is not installed. Please install it and retry." ;;
"Docker 未安装,请先安装后重试")
out="Docker is not installed. Please install it and retry." ;;
"docker-compose或 docker compose 插件)未安装,请先安装后重试")
out="docker-compose (or the docker compose plugin) is not installed. Please install it and retry." ;;
"当前目录为空,开始全新安装 DooTask ...")
out="Current directory is empty. Starting a fresh DooTask installation..." ;;
"克隆代码GitHub...")
out="Cloning source (GitHub)..." ;;
"GitHub 克隆失败,尝试 Gitee 镜像 ...")
out="GitHub clone failed, trying the Gitee mirror..." ;;
"代码克隆失败,请检查网络后重试")
out="Failed to clone the source. Please check your network and retry." ;;
"代码克隆完成")
out="Source cloned." ;;
"执行安装 ...")
out="Running installation..." ;;
"DooTask 安装完成")
out="DooTask installation complete." ;;
"检测到已克隆但尚未安装,执行安装 ...")
out="Repository found but not yet installed. Running installation..." ;;
"检测到已安装的 DooTask正在检查更新 ...")
out="Existing DooTask installation detected. Checking for updates..." ;;
"无法获取远程版本信息(分支 (*)),请检查网络后重试")
out="Unable to fetch remote version info (branch (*)). Please check your network and retry." ;;
"当前已是最新版本v(*)")
out="Already up to date (v(*))." ;;
"发现新版本:当前 v(*) → 最新 v(*)(分支 (*)")
out="New version available: current v(*) -> latest v(*) (branch (*))." ;;
"是否立即升级?")
out="Upgrade now?" ;;
"已取消升级")
out="Upgrade cancelled." ;;
"获取最新 cmd 失败,请检查网络后重试")
out="Failed to fetch the latest cmd. Please check your network and retry." ;;
"开始升级 ...")
out="Starting upgrade..." ;;
"DooTask 升级完成")
out="DooTask upgrade complete." ;;
"当前目录非空,且不是 DooTask 项目目录。")
out="Current directory is not empty and is not a DooTask project." ;;
"请在「空目录」中执行全新安装,或进入「已安装的 DooTask 目录」执行升级。")
out="Run this in an empty directory for a fresh install, or inside an existing DooTask directory to upgrade." ;;
esac
fi
# 动态值:依次把 (*) 替换为参数
local a
for a in "$@"; do
out="${out/(\*)/$a}"
done
printf '%s' "$out"
}
# ---------- 输出 ----------
if [ -t 1 ]; then
Red="\033[31m"; Green="\033[32m"; Yellow="\033[33m"; Blue="\033[36m"; Font="\033[0m"
else
Red=""; Green=""; Yellow=""; Blue=""; Font=""
fi
info() { echo -e "${Blue}==>${Font} $1"; }
success() { echo -e "${Green}[$(msg 成功)]${Font} $1"; }
warning() { echo -e "${Yellow}[$(msg 警告)]${Font} $1"; }
error() { echo -e "${Red}[$(msg 错误)]${Font} $1" >&2; }
die() { error "$1"; exit 1; }
# ---------- 交互输入 ----------
# curl | bash 时 stdin 被管道占用,交互一律从 /dev/tty 读,否则 read 会读到 EOF
has_tty() { [ -e /dev/tty ]; }
confirm() {
# $1=提示语,默认 Y无终端时返回失败不擅自执行需确认的操作
local prompt="$1" ans
has_tty || return 1
read -r -p "$prompt [Y/n] " ans < /dev/tty
[[ -z "$ans" || "$ans" =~ ^[Yy]([Ee][Ss])?$ ]]
}
# ---------- 提权执行 ----------
# install / update 需要 root统一用 bash 执行脚本(规避 /tmp noexec
# 交互(含 cmd 内部的 read 与 sudo 密码)接到 /dev/tty。
# git clone 不走这里,用当前用户执行,避免代码属主变成 root。
run_cmd() {
local script="$1"; shift
local stdin_src="/dev/stdin"
has_tty && stdin_src="/dev/tty"
if [ "$(id -u)" -eq 0 ]; then
bash "$script" "$@" < "$stdin_src"
else
sudo bash "$script" "$@" < "$stdin_src"
fi
}
# ---------- 前置检查 ----------
precheck() {
command -v git >/dev/null 2>&1 || die "$(msg 'git 未安装,请先安装后重试')"
command -v curl >/dev/null 2>&1 || die "$(msg 'curl 未安装,请先安装后重试')"
command -v docker >/dev/null 2>&1 || die "$(msg 'Docker 未安装,请先安装后重试')"
if ! docker compose version >/dev/null 2>&1 && ! docker-compose version >/dev/null 2>&1; then
die "$(msg 'docker-compose或 docker compose 插件)未安装,请先安装后重试')"
fi
}
# ---------- 工具 ----------
# 从 raw 取「指定分支的文件」到 stdout主源失败时回退 jsdelivr 镜像
fetch_raw() {
# $1=branch, $2=path
curl -fsSL "${RAW_PRIMARY}/$1/$2" 2>/dev/null \
|| curl -fsSL "${RAW_FALLBACK}@$1/$2" 2>/dev/null
}
# 从 stdin 读取 package.json 内容并提取版本号
read_pkg_version() {
grep -m1 '"version"' | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/'
}
is_dootask_project() {
# 只读 .git不写当前用户读取一般文件不受属主影响
if [ -d .git ] && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
local url; url="$(git config --get remote.origin.url 2>/dev/null || true)"
[[ "$url" == *dootask* ]] && return 0
fi
[ -f cmd ] && [ -f docker-compose.yml ] && return 0
return 1
}
is_installed() { [ -f vendor/autoload.php ]; }
# 可忽略的系统垃圾文件(判断空目录时忽略;全新安装前会清除以便 git clone .
_IGNORABLE=".DS_Store .localized .Spotlight-V100 .fseventsd .TemporaryItems .Trashes .DocumentRevisions-V100 .VolumeIcon.icns .AppleDouble .AppleDB .AppleDesktop Thumbs.db ehthumbs.db desktop.ini .directory"
# 是否「可忽略的系统文件」(含 macOS AppleDouble 的 ._xxx
_is_ignorable() {
case " $_IGNORABLE " in *" $1 "*) return 0 ;; esac
case "$1" in ._*) return 0 ;; esac
return 1
}
# 当前目录是否「实质为空」:只剩可忽略的系统垃圾文件也算空
dir_empty() {
local f
while IFS= read -r f; do
_is_ignorable "${f##*/}" || return 1
done < <(find . -maxdepth 1 -mindepth 1 2>/dev/null)
return 0
}
# 清除可忽略的系统垃圾文件(仅白名单),确保 git clone . 不被这些文件挡住
clean_ignorable() {
local f
while IFS= read -r f; do
_is_ignorable "${f##*/}" && rm -rf "$f"
done < <(find . -maxdepth 1 -mindepth 1 2>/dev/null)
}
current_branch() { git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "$BRANCH"; }
# ---------- 动作:全新安装 ----------
do_fresh_install() {
info "$(msg '当前目录为空,开始全新安装 DooTask ...')"
clean_ignorable # 清掉 .DS_Store 等系统垃圾,确保 git clone . 不被挡住
info "$(msg '克隆代码GitHub...')"
if ! git clone --depth=1 --branch "$BRANCH" "$REPO_GITHUB" . 2>/dev/null; then
warning "$(msg 'GitHub 克隆失败,尝试 Gitee 镜像 ...')"
rm -rf .git # 仅清理 clone 残留;若工作树仍有残留,下一步 clone 会报错而非误删
git clone --depth=1 --branch "$BRANCH" "$REPO_GITEE" . \
|| die "$(msg '代码克隆失败,请检查网络后重试')"
fi
success "$(msg '代码克隆完成')"
info "$(msg '执行安装 ...')"
run_cmd ./cmd install
success "$(msg 'DooTask 安装完成')"
}
# ---------- 动作:续装 ----------
do_install() {
info "$(msg '检测到已克隆但尚未安装,执行安装 ...')"
run_cmd ./cmd install
success "$(msg 'DooTask 安装完成')"
}
# ---------- 动作:升级 ----------
do_upgrade() {
info "$(msg '检测到已安装的 DooTask正在检查更新 ...')"
local branch; branch="$(current_branch)"
local local_ver remote_ver
local_ver="$( [ -f package.json ] && read_pkg_version < package.json )"
remote_ver="$(fetch_raw "$branch" package.json | read_pkg_version)"
[ -z "$local_ver" ] && local_ver="$(msg 未知)"
# 取不到远程版本(网络/分支异常)→ 报错,避免误判
[ -z "$remote_ver" ] && die "$(msg '无法获取远程版本信息(分支 (*)),请检查网络后重试' "$branch")"
if [ "$local_ver" = "$remote_ver" ]; then
success "$(msg '当前已是最新版本v(*)' "$local_ver")"
exit 0
fi
echo
info "$(msg '发现新版本:当前 v(*) → 最新 v(*)(分支 (*)' "$local_ver" "$remote_ver" "$branch")"
if ! confirm "$(msg '是否立即升级?')"; then
warning "$(msg '已取消升级')"
exit 0
fi
# 从 raw 取「线上最新 cmd」到临时文件执行不碰本地 .git、不依赖磁盘旧 cmd
# 真正的 git pull / 装依赖 / 迁移 / 重启由这份最新 cmd 完成,一次到位。
local tmp; tmp="$(mktemp)"
if ! fetch_raw "$branch" cmd > "$tmp" || [ ! -s "$tmp" ]; then
rm -f "$tmp"; die "$(msg '获取最新 cmd 失败,请检查网络后重试')"
fi
info "$(msg '开始升级 ...')"
run_cmd "$tmp" update
rm -f "$tmp"
success "$(msg 'DooTask 升级完成')"
}
# ---------- 主流程 ----------
main() {
precheck
if is_dootask_project; then
if is_installed; then
do_upgrade
else
do_install
fi
elif dir_empty; then
do_fresh_install
else
error "$(msg '当前目录非空,且不是 DooTask 项目目录。')"
error "$(msg '请在「空目录」中执行全新安装,或进入「已安装的 DooTask 目录」执行升级。')"
exit 1
fi
}
main "$@"

289
cmd
View File

@ -9,10 +9,104 @@ YellowBG="\033[43;37m"
RedBG="\033[41;37m"
Font="\033[0m"
# 语言判定:默认英文;仅当 locale 明确是「中文 UTF-8」时才用中文中文非 UTF-8 如 GBK 也用英文以免乱码)。
DT_LANG="en"
__loc="${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}"
case "$__loc" in
zh_*|zh-*|zh)
case "$__loc" in
*[Uu][Tt][Ff]*) DT_LANG="zh" ;;
*.*) DT_LANG="en" ;;
*) DT_LANG="zh" ;;
esac
;;
esac
unset __loc
# 文案:调用处只写中文(动态值用 (*) 占位,顺序对应后续参数)。
# 中文环境直接用原文;英文环境在此集中查表翻译,未登记的中文原样返回。
msg() {
local tpl="$1"; shift
local out="$tpl"
if [ "$DT_LANG" != "zh" ]; then
case "$tpl" in
"警告") out="WARN" ;;
"错误") out="ERROR" ;;
"地址") out="URL" ;;
"(*) 完成") out="(*) done" ;;
"(*) 失败") out="(*) failed" ;;
"备份数据库") out="Backing up database" ;;
"还原数据库") out="Restoring database" ;;
"无法创建脚本副本") out="Failed to create script copy" ;;
"没有找到 (*) 容器!") out="Container (*) not found!" ;;
"请使用 sudo 运行此脚本") out="Please run this script with sudo" ;;
"未安装 Docker") out="Docker is not installed!" ;;
"未安装 Docker-compose") out="Docker-compose is not installed!" ;;
"Docker-compose 版本过低请升级至v2+") out="Docker-compose is too old. Please upgrade to v2+!" ;;
"未安装 npm") out="npm is not installed!" ;;
"未安装 Node.js") out="Node.js is not installed!" ;;
"Node.js 版本过低请升级至v20+") out="Node.js is too old. Please upgrade to v20+!" ;;
"备份文件:(*)") out="Backup file: (*)" ;;
"没有备份文件!") out="No backup files found!" ;;
"可用备份列表:") out="Available backups:" ;;
"请输入备份文件编号还原:") out="Enter the backup number to restore: " ;;
"编号无效,请重新输入。") out="Invalid number, please try again." ;;
"HTTP服务端口不是80是否修改并继续操作 [Y/n]") out="HTTP port is not 80. Change it and continue? [Y/n]" ;;
"HTTPS服务端口不是443是否修改并继续操作 [Y/n]") out="HTTPS port is not 443. Change it and continue? [Y/n]" ;;
"继续操作") out="Continuing" ;;
"操作终止") out="Operation aborted" ;;
"任务已存在,无需添加。") out="Cron job already exists, skipped." ;;
"任务已添加。") out="Cron job added." ;;
"设置env参数失败") out="Failed to set env variable!" ;;
"APP_ID(*))已被其他实例使用:(*)") out="APP_ID ((*)) is already used by another instance: (*)" ;;
"请先清空 .env 中的 APP_ID 和 APP_IPPR 再重新安装") out="Please clear APP_ID and APP_IPPR in .env, then reinstall" ;;
"端口 (*) 已被占用,请指定其他端口") out="Port (*) is already in use, please specify another port" ;;
"目录权限检测失败!请检查目录权限设置") out="Directory permission check failed! Please check directory permissions" ;;
"目录【(*)】权限不足!") out="Directory [(*)] is not writable!" ;;
"安装依赖失败") out="Failed to install dependencies" ;;
"安装依赖失败,请重试!") out="Failed to install dependencies, please retry!" ;;
"生成密钥失败") out="Failed to generate app key" ;;
"数据库迁移失败") out="Database migration failed" ;;
"安装完成") out="Installation complete" ;;
"请先执行安装命令") out="Please run the install command first" ;;
"检测到本地修改,是否强制更新?[Y/n]") out="Local changes detected. Force update? [Y/n]" ;;
"取消更新,请先处理本地修改") out="Update cancelled, please handle local changes first" ;;
"获取远程更新失败") out="Failed to fetch remote updates" ;;
"设置远程Fetch配置失败") out="Failed to set remote fetch config" ;;
"获取远程分支 (*) 失败") out="Failed to fetch remote branch (*)" ;;
"切换分支到 (*) 失败") out="Failed to switch to branch (*)" ;;
"数据库有迁移变动,执行数据库备份...") out="Database migrations changed, backing up database..." ;;
"数据库备份失败") out="Database backup failed" ;;
"数据库备份完成") out="Database backup complete" ;;
"强制更新代码失败") out="Failed to force-update code" ;;
"代码拉取失败,可能存在冲突,请使用 --force 参数") out="Failed to pull code (possible conflict), please use --force" ;;
"更新PHP依赖失败") out="Failed to update PHP dependencies" ;;
"执行数据库备份...") out="Backing up database..." ;;
"重启服务失败") out="Failed to restart services" ;;
"更新完成") out="Update complete" ;;
"警告:此操作将永久删除以下内容:") out="WARNING: This will permanently delete:" ;;
"- 数据库") out="- Database" ;;
"- 应用程序") out="- Application" ;;
"- 日志文件") out="- Log files" ;;
"确认要继续卸载吗?(y/N): ") out="Confirm uninstall? (y/N): " ;;
"开始卸载...") out="Uninstalling..." ;;
"终止卸载。") out="Uninstall aborted." ;;
"卸载完成") out="Uninstall complete" ;;
"修改成功") out="Changed successfully" ;;
esac
fi
# 动态值:依次把 (*) 替换为参数
local a
for a in "$@"; do
out="${out/(\*)/$a}"
done
printf '%s' "$out"
}
# 通知信息
OK="${Green}[OK]${Font}"
Warn="${Yellow}[警告]${Font}"
Error="${Red}[错误]${Font}"
Warn="${Yellow}[$(msg 警告)]${Font}"
Error="${Red}[$(msg 错误)]${Font}"
# 基本参数
WORK_DIR="$(pwd)"
@ -28,7 +122,7 @@ fi
# 缓存执行
if [ -z "$CACHED_EXECUTION" ] && [ "$1" == "update" ]; then
if ! cat "$0" > ._cmd 2>/dev/null; then
error "无法创建脚本副本"
error "$(msg '无法创建脚本副本')"
exit 1
fi
chmod +x ._cmd
@ -42,10 +136,10 @@ fi
# 判断是否成功
judge() {
if [[ 0 -eq $? ]]; then
success "$1 完成"
success "$(msg '(*) 完成' "$1")"
sleep 1
else
error "$1 失败"
error "$(msg '(*) 失败' "$1")"
exit 1
fi
}
@ -128,7 +222,7 @@ switch_debug() {
# 检查是否有sudo
check_sudo() {
if [ "$EUID" -ne 0 ]; then
error "请使用 sudo 运行此脚本"
error "$(msg '请使用 sudo 运行此脚本')"
exit 1
fi
}
@ -137,21 +231,21 @@ check_sudo() {
check_docker() {
docker --version &> /dev/null
if [ $? -ne 0 ]; then
error "未安装 Docker"
error "$(msg '未安装 Docker')"
exit 1
fi
docker-compose version &> /dev/null
if [ $? -ne 0 ]; then
docker compose version &> /dev/null
if [ $? -ne 0 ]; then
error "未安装 Docker-compose"
error "$(msg '未安装 Docker-compose')"
exit 1
fi
COMPOSE="docker compose"
fi
if [[ -n `$COMPOSE version | grep -E "\s+v1\."` ]]; then
$COMPOSE version
error "Docker-compose 版本过低请升级至v2+"
error "$(msg 'Docker-compose 版本过低请升级至v2+')"
exit 1
fi
}
@ -160,17 +254,17 @@ check_docker() {
check_node() {
npm --version &> /dev/null
if [ $? -ne 0 ]; then
error "未安装 npm"
error "$(msg '未安装 npm')"
exit 1
fi
node --version &> /dev/null
if [ $? -ne 0 ]; then
error "未安装 Node.js"
error "$(msg '未安装 Node.js')"
exit 1
fi
if [[ -n `node --version | grep -E "v1"` ]]; then
node --version
error "Node.js 版本过低请升级至v20+"
error "$(msg 'Node.js 版本过低请升级至v20+')"
exit 1
fi
}
@ -244,7 +338,7 @@ container_exec() {
local cmd=$@
local name=$(docker_name "$container")
if [ -z "$name" ]; then
error "没有找到 ${container} 容器!"
error "$(msg '没有找到 (*) 容器!' "$container")"
exit 1
fi
docker exec $TTY_FLAG "$name" /bin/sh -c "$cmd"
@ -272,8 +366,8 @@ mysql_snapshot() {
mkdir -p ${WORK_DIR}/docker/mysql/backup
filename="${WORK_DIR}/docker/mysql/backup/${database}_$(date "+%Y%m%d%H%M%S").sql.gz"
container_exec mariadb "exec mysqldump --databases $database -u${username} -p${password}" | gzip > $filename
judge "备份数据库"
[ -f "$filename" ] && echo "备份文件:${filename}"
judge "$(msg '备份数据库')"
[ -f "$filename" ] && echo "$(msg '备份文件:(*)' "$filename")"
elif [ "$1" = "recovery" ]; then
database=$(env_get DB_DATABASE)
username=$(env_get DB_USERNAME)
@ -284,31 +378,31 @@ mysql_snapshot() {
backup_files=("${WORK_DIR}/docker/mysql/backup/"*.sql.gz)
shopt -u nullglob
if [ ${#backup_files[@]} -eq 0 ]; then
error "没有备份文件!"
error "$(msg '没有备份文件!')"
exit 1
fi
echo "可用备份列表:"
echo "$(msg '可用备份列表:')"
for idx in "${!backup_files[@]}"; do
printf "%2d) %s\n" "$((idx + 1))" "$(basename "${backup_files[$idx]}")"
done
while true; do
read -rp "请输入备份文件编号还原:" selection
read -rp "$(msg '请输入备份文件编号还原:')" selection
if [[ "$selection" =~ ^[0-9]+$ ]] && [ "$selection" -ge 1 ] && [ "$selection" -le ${#backup_files[@]} ]; then
break
fi
warning "编号无效,请重新输入。"
warning "$(msg '编号无效,请重新输入。')"
done
filename="${backup_files[$((selection - 1))]}"
inputname="$(basename "$filename")"
container_name=`docker_name mariadb`
if [ -z "$container_name" ]; then
error "没有找到 mariadb 容器!"
error "$(msg '没有找到 (*) 容器!' mariadb)"
exit 1
fi
docker cp "$filename" "${container_name}:/"
container_exec mariadb "gunzip < '/${inputname}' | mysql -u${username} -p${password} $database"
container_exec php "php artisan migrate"
judge "还原数据库"
judge "$(msg '还原数据库')"
fi
}
@ -339,33 +433,33 @@ remove_by_network() {
https_auto() {
restart_nginx="n"
if [[ "$(env_get APP_PORT)" != "80" ]]; then
warning "HTTP服务端口不是80是否修改并继续操作 [Y/n]"
warning "$(msg 'HTTP服务端口不是80是否修改并继续操作 [Y/n]')"
read -r continue_http
[[ -z ${continue_http} ]] && continue_http="Y"
case $continue_http in
[yY][eE][sS] | [yY])
success "继续操作"
success "$(msg '继续操作')"
env_set "APP_PORT" "80"
restart_nginx="y"
;;
*)
error "操作终止"
error "$(msg '操作终止')"
exit 1
;;
esac
fi
if [[ "$(env_get APP_SSL_PORT)" != "443" ]]; then
warning "HTTPS服务端口不是443是否修改并继续操作 [Y/n]"
warning "$(msg 'HTTPS服务端口不是443是否修改并继续操作 [Y/n]')"
read -r continue_https
[[ -z ${continue_https} ]] && continue_https="Y"
case $continue_https in
[yY][eE][sS] | [yY])
success "继续操作"
success "$(msg '继续操作')"
env_set "APP_SSL_PORT" "443"
restart_nginx="y"
;;
*)
error "操作终止"
error "$(msg '操作终止')"
exit 1
;;
esac
@ -380,13 +474,13 @@ https_auto() {
new_job="* 6 * * * docker run --rm -v $(pwd):/work nginx:alpine sh /work/bin/https renew"
current_crontab=$(crontab -l 2>/dev/null)
if ! echo "$current_crontab" | grep -v "https renew"; then
echo "任务已存在,无需添加。"
echo "$(msg '任务已存在,无需添加。')"
else
crontab -l |{
cat
echo "$new_job"
} | crontab -
echo "任务已添加。"
echo "$(msg '任务已添加。')"
fi
}
@ -416,7 +510,7 @@ env_set() {
docker run $TTY_FLAG --rm -v ${WORK_DIR}:/www nginx:alpine sh -c "sed -i "/^${key}=/c\\${key}=${val}" /www/.env"
fi
if [ $? -ne 0 ]; then
error "设置env参数失败"
error "$(msg '设置env参数失败')"
exit 1
fi
fi
@ -468,7 +562,8 @@ arg_get() {
# 显示帮助信息
show_help() {
cat << 'EOF'
if [ "$DT_LANG" = "zh" ]; then
cat << 'EOF'
DooTask 管理脚本
用法: ./cmd <命令> [参数]
@ -516,6 +611,56 @@ DooTask 管理脚本
./cmd mysql backup 备份数据库
./cmd artisan migrate 执行数据库迁移
EOF
else
cat << 'EOF'
DooTask Management Script
Usage: ./cmd <command> [options]
📦 Core:
install Install DooTask (supports --port <port> --relock)
update Update DooTask (supports --branch <branch> --force --local)
uninstall Uninstall DooTask
⚙️ Configuration:
port <port> Change service port
url <address> Change access URL
env <key> <value> Set environment variable
debug [true|false] Toggle debug mode
repassword [username] Reset database password
🚀 Build:
serve, dev Start dev mode
build, prod Production build
electron Build desktop app
🔧 Services:
up [service] Start containers
down [service] Stop containers
restart [service] Restart containers
reup Rebuild and start
💾 Database:
mysql backup Back up database
mysql recovery Restore database
🛠️ Dev tools:
artisan <command> Run Laravel Artisan command
composer <command> Run Composer command
php <command> Run PHP command
📚 Others:
doc Generate API docs
https Configure HTTPS
--help, -h Show this help
Examples:
./cmd install --port 8080 Install on port 8080
./cmd update --branch dev Switch to dev branch and update
./cmd mysql backup Back up database
./cmd artisan migrate Run database migration
EOF
fi
}
# 检测APP_ID是否与其他实例冲突
@ -524,8 +669,8 @@ check_instance() {
local container_name="dootask-php-${app_id}"
local mount_path=$(docker inspect "$container_name" --format '{{range .Mounts}}{{if eq .Destination "/var/www"}}{{.Source}}{{end}}{{end}}' 2>/dev/null)
if [[ -n "$mount_path" ]] && [[ "$mount_path" != "$WORK_DIR" ]]; then
error "APP_ID${app_id})已被其他实例使用:${mount_path}"
error "请先清空 .env 中的 APP_ID 和 APP_IPPR 再重新安装"
error "$(msg 'APP_ID(*))已被其他实例使用:(*)' "$app_id" "$mount_path")"
error "$(msg '请先清空 .env 中的 APP_ID 和 APP_IPPR 再重新安装')"
exit 1
fi
}
@ -537,7 +682,7 @@ check_port() {
local current_port=$2
if [[ "$port" -gt 0 ]] && [[ "$port" != "$current_port" ]]; then
if ! docker run --rm -p "${port}:80" --entrypoint true nginx:alpine 2>/dev/null; then
error "端口 ${port} 已被占用,请指定其他端口"
error "$(msg '端口 (*) 已被占用,请指定其他端口' "$port")"
exit 1
fi
fi
@ -582,13 +727,13 @@ handle_install() {
writable="yes"
docker run --rm ${cmda} nginx:alpine sh -c "${cmdb} touch /usr/share/docker/dootask.lock" &> /dev/null
if [ $? -ne 0 ]; then
error "目录权限检测失败!请检查目录权限设置"
error "$(msg '目录权限检测失败!请检查目录权限设置')"
exit 1
fi
for vol in "${volumes[@]}"; do
if [ ! -f "${vol}/dootask.lock" ]; then
if [ $remaining -lt 0 ]; then
error "目录【${vol}】权限不足!"
error "$(msg '目录【(*)】权限不足!' "$vol")"
exit 1
else
writable="no"
@ -619,28 +764,28 @@ handle_install() {
$COMPOSE up php -d
# 安装PHP依赖
exec_judge "container_exec php 'composer install --optimize-autoloader'" "安装依赖失败"
exec_judge "container_exec php 'composer install --optimize-autoloader'" "$(msg '安装依赖失败')"
# 最终检查
if [ ! -f "${WORK_DIR}/vendor/autoload.php" ]; then
error "安装依赖失败,请重试!"
error "$(msg '安装依赖失败,请重试!')"
exit 1
fi
# 生成应用密钥
[[ -z "$(env_get APP_KEY)" ]] && exec_judge "container_exec php 'php artisan key:generate'" "生成密钥失败"
[[ -z "$(env_get APP_KEY)" ]] && exec_judge "container_exec php 'php artisan key:generate'" "$(msg '生成密钥失败')"
# 设置生产模式
switch_debug "false"
# 数据库迁移
exec_judge "container_exec php 'php artisan migrate --seed'" "数据库迁移失败"
exec_judge "container_exec php 'php artisan migrate --seed'" "$(msg '数据库迁移失败')"
# 启动所有容器
$COMPOSE up -d --remove-orphans
success "安装完成"
echo -e "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
success "$(msg '安装完成')"
echo -e "$(msg '地址'): http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
container_exec mariadb "sh /etc/mysql/repassword.sh"
}
@ -654,7 +799,7 @@ handle_update() {
# 检查是否已经安装
if [ ! -f "${WORK_DIR}/vendor/autoload.php" ]; then
error "请先执行安装命令"
error "$(msg '请先执行安装命令')"
exit 1
fi
@ -667,7 +812,7 @@ handle_update() {
# 检查本地修改
if ! git diff --quiet || ! git diff --cached --quiet; then
if [[ "$force_update" != "yes" ]]; then
warning "检测到本地修改,是否强制更新?[Y/n]"
warning "$(msg '检测到本地修改,是否强制更新?[Y/n]')"
read -r confirm_force
[[ -z ${confirm_force} ]] && confirm_force="Y"
case $confirm_force in
@ -675,7 +820,7 @@ handle_update() {
force_update="yes"
;;
*)
error "取消更新,请先处理本地修改"
error "$(msg '取消更新,请先处理本地修改')"
exit 1
;;
esac
@ -683,21 +828,21 @@ handle_update() {
fi
# 远程更新模式
exec_judge "git fetch --all" "获取远程更新失败"
exec_judge "git fetch --all" "$(msg '获取远程更新失败')"
# 确定目标分支
if [[ -n "$target_branch" ]]; then
current_branch="$target_branch"
if ! git config --get "branch.${current_branch}.remote" | grep -q "origin"; then
exec_judge "git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'" "设置远程Fetch配置失败"
exec_judge "git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'" "$(msg '设置远程Fetch配置失败')"
fi
if ! git show-ref --verify --quiet refs/heads/${current_branch}; then
exec_judge "git fetch origin ${current_branch}:${current_branch}" "获取远程分支 ${current_branch} 失败"
exec_judge "git fetch origin ${current_branch}:${current_branch}" "$(msg '获取远程分支 (*) 失败' "$current_branch")"
fi
if [[ "$force_update" == "yes" ]]; then
exec_judge "git checkout -f ${current_branch}" "切换分支到 ${current_branch} 失败"
exec_judge "git checkout -f ${current_branch}" "$(msg '切换分支到 (*) 失败' "$current_branch")"
else
exec_judge "git checkout ${current_branch}" "切换分支到 ${current_branch} 失败"
exec_judge "git checkout ${current_branch}" "$(msg '切换分支到 (*) 失败' "$current_branch")"
fi
else
current_branch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
@ -706,27 +851,27 @@ handle_update() {
# 检查数据库迁移变动
db_changes=$(git diff --name-only HEAD..origin/${current_branch} 2>/dev/null | grep -E "^database/" || true)
if [[ -n "$db_changes" ]]; then
echo "数据库有迁移变动,执行数据库备份..."
exec_judge "mysql_snapshot backup" "数据库备份失败" "数据库备份完成"
echo "$(msg '数据库有迁移变动,执行数据库备份...')"
exec_judge "mysql_snapshot backup" "$(msg '数据库备份失败')" "$(msg '数据库备份完成')"
fi
# 更新代码
if [[ "$force_update" == "yes" ]]; then
exec_judge "git reset --hard origin/${current_branch}" "强制更新代码失败"
exec_judge "git reset --hard origin/${current_branch}" "$(msg '强制更新代码失败')"
else
exec_judge "git pull --ff-only origin ${current_branch}" "代码拉取失败,可能存在冲突,请使用 --force 参数"
exec_judge "git pull --ff-only origin ${current_branch}" "$(msg '代码拉取失败,可能存在冲突,请使用 --force 参数')"
fi
# 更新依赖
exec_judge "container_run php 'composer install --optimize-autoloader'" "更新PHP依赖失败"
exec_judge "container_run php 'composer install --optimize-autoloader'" "$(msg '更新PHP依赖失败')"
else
# 本地更新模式
echo "执行数据库备份..."
exec_judge "mysql_snapshot backup" "数据库备份失败" "数据库备份完成"
echo "$(msg '执行数据库备份...')"
exec_judge "mysql_snapshot backup" "$(msg '数据库备份失败')" "$(msg '数据库备份完成')"
fi
# 数据库迁移
exec_judge "container_run php 'php artisan migrate'" "数据库迁移失败"
exec_judge "container_run php 'php artisan migrate'" "$(msg '数据库迁移失败')"
# 停止服务
$COMPOSE stop php nginx &> /dev/null
@ -736,30 +881,30 @@ handle_update() {
$COMPOSE up -d --remove-orphans
if [[ 0 -ne $? ]]; then
$COMPOSE down --remove-orphans
exec_judge "$COMPOSE up -d" "重启服务失败"
exec_judge "$COMPOSE up -d" "$(msg '重启服务失败')"
fi
env_set UPDATE_TIME "$(date +%s)"
success "更新完成"
success "$(msg '更新完成')"
}
# 卸载函数
handle_uninstall() {
check_sudo
# 确认卸载
echo -e "${RedBG}警告:此操作将永久删除以下内容:${Font}"
echo "- 数据库"
echo "- 应用程序"
echo "- 日志文件"
echo -e "${RedBG}$(msg '警告:此操作将永久删除以下内容:')${Font}"
echo "$(msg '- 数据库')"
echo "$(msg '- 应用程序')"
echo "$(msg '- 日志文件')"
echo ""
read -rp "确认要继续卸载吗?(y/N): " confirm_uninstall
read -rp "$(msg '确认要继续卸载吗?(y/N): ')" confirm_uninstall
[[ -z ${confirm_uninstall} ]] && confirm_uninstall="N"
case $confirm_uninstall in
[yY][eE][sS] | [yY])
echo -e "${RedBG}开始卸载...${Font}"
echo -e "${RedBG}$(msg '开始卸载...')${Font}"
;;
*)
echo -e "${GreenBG}终止卸载。${Font}"
echo -e "${GreenBG}$(msg '终止卸载。')${Font}"
exit 1
;;
esac
@ -780,7 +925,7 @@ handle_uninstall() {
find "./docker/appstore/log" -name "*.log" -delete 2>/dev/null
find "./storage/logs" -name "*.log" -delete 2>/dev/null
success "卸载完成"
success "$(msg '卸载完成')"
}
####################################################################################
@ -818,14 +963,14 @@ case "$1" in
check_port "$1" "$(env_get APP_PORT)"
env_set APP_PORT "$1"
$COMPOSE up -d
success "修改成功"
echo -e "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
success "$(msg '修改成功')"
echo -e "$(msg '地址'): http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
;;
"url")
shift 1
env_set APP_URL "$1"
restart_php
success "修改成功"
success "$(msg '修改成功')"
;;
"env")
shift 1
@ -833,7 +978,7 @@ case "$1" in
env_set $1 "$2"
fi
restart_php
success "修改成功"
success "$(msg '修改成功')"
;;
"repassword")
shift 1