diff --git a/README.md b/README.md
index 31873505a..87768c7ed 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/README_CN.md b/README_CN.md
index a51ac3216..e664289f6 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -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` 重启服务即可。
## 迁移项目
diff --git a/bin/install b/bin/install
new file mode 100755
index 000000000..a6c8fe79d
--- /dev/null
+++ b/bin/install
@@ -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:////
+RAW_FALLBACK="https://cdn.jsdelivr.net/gh/kuaifan/dootask" # https://@/
+
+# ---------- 语言判定 ----------
+# 默认英文;仅当 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 "$@"
diff --git a/cmd b/cmd
index f82d56af9..c6ccfa588 100755
--- a/cmd
+++ b/cmd
@@ -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 [options]
+
+📦 Core:
+ install Install DooTask (supports --port --relock)
+ update Update DooTask (supports --branch --force --local)
+ uninstall Uninstall DooTask
+
+⚙️ Configuration:
+ port Change service port
+ url Change access URL
+ env 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 Run Laravel Artisan command
+ composer Run Composer command
+ php 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