一键脚本
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/compose_admin_menu.sh) /vol1/1000/compose/md
bash <(curl -sL ../sh/compose_admin_menu.sh) /vol1/1000/compose/md
| 模式 | 命令 | 说明 |
|---|---|---|
| 交互模式 | ./脚本名.sh |
交互式选择项目 |
| 传参模式 | ./脚本名.sh 项目根目录 |
直接管理指定项目 |
效果预览
补充说明
该脚本用于 Docker Compose 项目的一站式管理,基于 docker-compose/docker compose、sed、awk 命令实现,适合需要批量管理多个容器项目的场景。
功能特点
- 支持传参和交互式两种模式:可指定项目目录或管理当前目录
- 完整的生命周期管理:启动、停止、重启、更新、清理容器
- 配置管理:查看/编辑 docker-compose.yml、查看最终生效配置、打印依赖拓扑图
- 服务监控:查看容器状态、实时日志、资源占用情况
- 高级操作:进入容器终端、修改重启策略、开放访问端口、重新构建镜像
- 智能服务选择:多服务项目中可交互选择要操作的服务
- 跨平台兼容:自动检测并使用 docker-compose 或 docker compose 命令
- 彩色输出:使用 ANSI 颜色码区分不同状态和提示信息
输出说明
脚本输出包含以下字段:
| 字段 | 说明 |
|---|---|
| 当前工作目录 | 显示 Compose 项目的绝对路径 |
| 项目名称 | 显示当前目录名作为项目名称 |
| 主要服务名称 | 从 docker-compose.yml 中自动提取的第一个服务名 |
| 内网 IP 地址 | 显示宿主机的 IP 地址 |
| 容器状态 | 显示主要容器的运行状态(已启动/已停止/不存在) |
| 服务访问链接 | 自动提取端口映射并显示访问 URL |
| 容器列表 | 显示容器 ID、名称、状态、端口、创建时间、镜像等信息 |
注意事项
- 脚本需要 Docker 和 docker-compose/docker compose 命令可用
- 默认操作的是 docker-compose.yml,也支持 docker-compose.yaml
- 停止并清理操作(选项9)会删除容器、网络和卷,请谨慎使用
- 修改重启策略后,建议重新创建容器以使配置完全生效
- 进入容器时优先尝试 bash,不可用则回退到 sh
- 开放端口功能依赖 iptables 命令,请确保系统已安装并配置好防火墙
- 脚本会自动检测 yq 工具来解析 YAML 文件,没有则使用 grep 作为备选
脚本源码
#!/bin/bash
set -uo pipefail
list_color_init() {
export gl_hui=$'\033[38;5;59m'
export gl_hong=$'\033[38;5;9m'
export gl_lv=$'\033[38;5;10m'
export gl_huang=$'\033[38;5;11m'
export gl_lan=$'\033[38;5;32m'
export gl_bai=$'\033[38;5;15m'
export gl_zi=$'\033[38;5;13m'
export gl_bufan=$'\033[38;5;14m'
export reset=$'\033[0m'
}
list_color_init
log_info() { echo -e "${gl_lan}[信息]${gl_bai} $*"; }
log_ok() { echo -e "${gl_lv}[成功]${gl_bai} $*"; }
log_warn() { echo -e "${gl_huang}[警告]${gl_bai} $*"; }
log_error() { echo -e "${gl_hong}[错误]${gl_bai} $*" >&2; }
sleep_fractional() {
local seconds=$1
if sleep "$seconds" 2>/dev/null; then return 0; fi
if command -v perl >/dev/null 2>&1; then perl -e "select(undef, undef, undef, $seconds)"; return 0; fi
if command -v python3 >/dev/null 2>&1; then python3 -c "import time; time.sleep($seconds)"; return 0; fi
if command -v python >/dev/null 2>&1; then python -c "import time; time.sleep($seconds)"; return 0; fi
local int_seconds=$(echo "$seconds" | awk '{print int($1+0.999)}')
sleep "$int_seconds"
}
exit_animation() {
echo -ne "${gl_lv}即将退出 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.5
echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.6
echo ""
clear
}
exit_script() {
local frames=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
local dots=(
"${gl_hong}."
"${gl_huang}."
"${gl_lv}."
"${gl_bufan}."
"${gl_zi}."
)
local dot_buffer=""
local frame_len=${#frames[@]}
local dot_idx=0
local total_dots=${#dots[@]}
for ((i=0; i<20; i++)); do
if (( i > 0 && i % 3 == 0 && dot_idx < total_dots )); then
dot_buffer+=${dots[$dot_idx]}
((dot_idx++))
fi
echo -ne "\r\033[K${gl_bufan}${frames[i % frame_len]}${gl_bai} 正在退出 ${dot_buffer}"
sleep_fractional 0.06
done
echo -e "\r\033[K${gl_lv}✓${gl_bai} 成功退出\n"
clear
exit 0
}
cancel_return() {
local menu_name="${1:-上一级选单}"
echo -e "${gl_lv}即将返回到 ${gl_huang}${menu_name}${gl_lv} ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai} \c"
sleep_fractional 0.6
echo ""
clear
}
cancel_empty() {
local menu_name="${1:-上一级选单}"
echo -e "${gl_hong}空输入,返回 ${gl_huang}${menu_name} ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.5
echo -ne "${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
sleep_fractional 0.6
echo ""
clear
}
break_end() {
echo -e "${gl_bai}按任意键继续 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai} \c"
read -r -n 1 -s -r -p ""
echo ""
clear
}
safe_read() {
local prompt="$1"
local var_name="$2"
local type="${3:-"string"}"
local default_value="${4:-}"
local min_value="${5:-}"
local max_value="${6:-}"
local max_retry=3
local retry_count=0
while [[ $retry_count -lt $max_retry ]]; do
if [[ -n "$default_value" ]]; then
read -r -e -p "$(echo -e "${gl_bai}$prompt (${gl_huang}默认: $default_value${gl_bai}): ")" "$var_name"
[[ -z "${!var_name}" ]] && eval "$var_name=\"\$default_value\""
else
read -r -e -p "$(echo -e "${gl_bai}$prompt: ")" "$var_name"
fi
if [[ -z "${!var_name}" ]]; then
if [[ -n "$default_value" ]]; then
eval "$var_name=\"\$default_value\""
return 0
else
log_error "输入不能为空"
retry_count=$((retry_count + 1))
continue
fi
fi
case "$type" in
"number")
if ! [[ "${!var_name}" =~ ^[0-9]+$ ]]; then
log_error "请输入数字"
retry_count=$((retry_count + 1))
continue
fi
if [[ -n "$min_value" ]] && [[ "${!var_name}" -lt "$min_value" ]]; then
log_error "输入值不能小于 $min_value"
retry_count=$((retry_count + 1))
continue
fi
if [[ -n "$max_value" ]] && [[ "${!var_name}" -gt "$max_value" ]]; then
log_error "输入值不能大于 $max_value"
retry_count=$((retry_count + 1))
continue
fi
;;
"y/n")
if ! [[ "${!var_name}" =~ ^[YyNn]$ ]]; then
log_error "请输入 y 或 n"
retry_count=$((retry_count + 1))
continue
fi
;;
esac
return 0
done
log_error "输入尝试次数过多,返回上一级"
return 1
}
handle_invalid_input() {
echo -ne "\r\033[K${gl_huang}无效的输入,请重新输入! ${gl_zi} 1 ${gl_huang} 秒后返回"
sleep_fractional 1
echo -ne "\r\033[K${gl_lv}无效的输入,请重新输入! ${gl_zi}0${gl_lv} 秒后返回"
sleep_fractional 0.5
echo -ne "\r\033[K"
return 2
}
column_if_available() {
if command -v column &> /dev/null; then
column -t -s $'\t'
else
cat
fi
}
compose_ps_beautify() {
if [[ ! -f "docker-compose.yml" && ! -f "compose.yml" ]]; then
echo -e "${gl_hong}❌ 当前目录未找到 docker-compose.yml / compose.yml${gl_bai}"
return 1
fi
{
# 表头,使用 -- 隔离参数,防止 -- 开头解析错误
printf -- "容器ID\t名称\t状态\t端口\t创建时间\t镜像\n"
printf -- "----------\t----------\t----------\t----------\t----------\t----------\n"
docker compose ps --format "{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}\t{{.RunningFor}}\t{{.Image}}"
} | awk -v green="$gl_lv" -v red="$gl_hong" -v yellow="$gl_huang" -v blue="$gl_lan" -v cyan="$gl_bufan" -v white="$gl_bai" -v hui="$gl_hui" -v reset="$reset" '
BEGIN {FS="\t"; OFS="\t"}
{
if(NR == 1){
print hui $0 reset
next
}
if(NR == 2){
print hui $0 reset
next
}
id = substr($1, 1, 12)
name = $2
status = $3
ports = $4
time = $5
image = $6
gsub(/healthy/, "健康", status)
gsub(/unhealthy/, "不健康", status)
gsub(/starting/, "启动中", status)
gsub(/Up /, "已运行 ", status)
gsub(/days/, "天", status)
gsub(/hours/, "小时", status)
gsub(/minutes/, "分钟", status)
gsub(/seconds/, "秒", status)
if (status !~ /健康|不健康|启动中/) {
status = status " (正常)"
}
gsub(/[0-9]+/, green "&" reset, status)
gsub(/健康/, green "&" reset, status)
gsub(/不健康/, red "&" reset, status)
gsub(/启动中/, yellow "&" reset, status)
gsub(/ years ago/, "年前", time)
gsub(/ year ago/, "年前", time)
gsub(/ months ago/, "个月前", time)
gsub(/ month ago/, "个月前", time)
gsub(/ weeks ago/, "周前", time)
gsub(/ week ago/, "周前", time)
gsub(/ days ago/, "天前", time)
gsub(/ day ago/, "天前", time)
gsub(/ hours ago/, "小时前", time)
gsub(/ hour ago/, "小时前", time)
gsub(/ minutes ago/, "分钟前", time)
gsub(/ minute ago/, "分钟前", time)
gsub(/ seconds ago/, "秒前", time)
gsub(/ second ago/, "秒前", time)
gsub(/About /, "", time)
gsub(/[0-9]+/, green "&" reset, time)
if (length(ports) > 35) {
ports = substr(ports,1,35) "..."
}
print cyan id reset, green name reset, yellow status reset, blue ports reset, white time reset, white image reset
}' | column_if_available
}
pre_clean_remote_tag() {
local repo_path="$1"
local tag="$2"
if [[ "${PRE_CLEAN_REMOTE_TAG}" != "1" ]]; then
return 0
fi
if [[ -z "$repo_path" || -z "$tag" ]]; then
return 0
fi
pushd "$repo_path" >/dev/null 2>&1 || return 0
local repo_slug
repo_slug=$(get_repo_slug "$repo_path")
local release_deleted=0
if [[ -n "$repo_slug" ]] && command -v gh >/dev/null 2>&1; then
if gh release view "$tag" -R "$repo_slug" >/dev/null 2>&1; then
log_warn "远端已存在 Release ${tag},删除 Release + tag"
if gh release delete "$tag" -R "$repo_slug" --yes --cleanup-tag >/dev/null 2>&1; then
log_ok "Release ${tag} 及远端 tag 已删除"
release_deleted=1
else
if gh release delete "$tag" -R "$repo_slug" --yes >/dev/null 2>&1; then
log_ok "Release ${tag} 已删除"
release_deleted=1
else
log_warn "Release ${tag} 删除失败(继续尝试清理 tag)"
fi
fi
else
log_info "远端无同名 Release ${tag}"
fi
fi
if [[ "$release_deleted" != "1" ]]; then
local remote_has_tag
remote_has_tag=$(git ls-remote --tags origin "refs/tags/${tag}" 2>/dev/null)
if [[ -n "$remote_has_tag" ]]; then
log_warn "远端已存在 tag ${tag},主动删除"
if git push origin --delete "$tag" >/dev/null 2>&1; then
log_ok "远端 tag ${tag} 已删除"
elif git push origin ":refs/tags/${tag}" >/dev/null 2>&1; then
log_ok "远端 tag ${tag} 已删除(refs 形式)"
else
log_warn "远端 tag ${tag} 删除失败(可能需要手动处理)"
fi
else
log_info "远端无同名 tag ${tag}"
fi
fi
if git rev-parse --verify "refs/tags/${tag}" >/dev/null 2>&1; then
git tag -d "$tag" >/dev/null 2>&1 && log_ok "本地 tag ${tag} 已删除" || true
fi
popd >/dev/null 2>&1
return 0
}
show_dockerhub_info() {
local repo="$1"
if [[ "${SHOW_DOCKERHUB_INFO}" != "1" ]]; then
return 0
fi
if [[ -z "$repo" ]]; then
log_error "用法: show_dockerhub_info <dockerhub_repo>"
return 1
fi
echo -e "${gl_zi}>>> Docker Hub 信息:${gl_huang}${repo}${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if [[ -n "$DOCKERHUB_USERNAME" ]]; then
echo -e "${gl_bai}Docker Hub 用户名:${gl_lv}${DOCKERHUB_USERNAME}${gl_bai}"
else
echo -e "${gl_bai}Docker Hub 用户名:${gl_hong}未设置${gl_bai}"
fi
if [[ -n "$DOCKERHUB_TOKEN" ]]; then
echo -e "${gl_bai}Docker Hub Token :${gl_lv}已设置${gl_bai}(长度 ${#DOCKERHUB_TOKEN})"
else
echo -e "${gl_bai}Docker Hub Token :${gl_hong}未设置${gl_bai}"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
}
ensure_gh_auth() {
local repo_path="${1:-.}"
if [[ -z "$GH_TOKEN" ]]; then
log_error "环境变量 GH_TOKEN 未设置,无法认证 GitHub CLI"
echo -e " ${gl_huang}export GH_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx${gl_bai}"
return 1
fi
if ! command -v gh >/dev/null 2>&1; then
pkg_install gh || return 1
fi
if gh auth status -h github.com >/dev/null 2>&1; then
log_ok "GitHub CLI 已登录,跳过认证"
else
if ! echo "$GH_TOKEN" | gh auth login --with-token; then
log_error "GitHub CLI Token 登录失败(请检查 Token 是否有效/未过期)"
return 1
fi
log_ok "GitHub CLI 登录完成"
fi
local repo_slug
repo_slug=$(get_repo_slug "$repo_path")
if [[ -z "$repo_slug" ]]; then
log_error "无法从 git remote 解析 GitHub 仓库地址(owner/repo)"
return 1
fi
if gh repo set-default "$repo_slug" >/dev/null 2>&1; then
log_ok "默认仓库已设置为 ${repo_slug}"
else
log_warn "set-default 未成功(不影响后续,后续命令已显式使用 -R)"
fi
return 0
}
ensure_cnb_token() {
if [[ "${ENABLE_CNB_CHECK:-}" != "1" ]]; then
log_info "ENABLE_CNB_CHECK=0,跳过CNB_ACCESS_TOKEN校验"
return 0
fi
if [[ -z "${CNB_ACCESS_TOKEN:-}" ]]; then
log_warn "CNB_ACCESS_TOKEN 未设置"
return 1
fi
log_ok "CNB_ACCESS_TOKEN 环境变量已配置"
return 0
}
ensure_dockerhub_auth() {
if [[ "${LOCAL_DOCKER_LOGIN}" != "1" ]]; then
log_info "LOCAL_DOCKER_LOGIN=0,跳过本地 docker login"
return 0
fi
if ! command -v docker >/dev/null 2>&1; then
log_warn "未检测到 docker 命令,跳过本地 docker login"
return 0
fi
if [[ -z "$DOCKERHUB_USERNAME" || -z "$DOCKERHUB_TOKEN" ]]; then
log_warn "DOCKERHUB_USERNAME 或 DOCKERHUB_TOKEN 未设置,跳过本地 docker login"
return 0
fi
local current_user=""
local cfg="${HOME}/.docker/config.json"
if [[ -f "$cfg" ]]; then
if command -v jq >/dev/null 2>&1; then
current_user=$(jq -r '.auths["https://index.docker.io/v1/"].auth // empty' "$cfg" 2>/dev/null \
| base64 -d 2>/dev/null | cut -d: -f1)
elif command -v python3 >/dev/null 2>&1; then
current_user=$(python3 -c "
import json,base64,os
try:
with open(os.path.expanduser('~/.docker/config.json')) as f:
d = json.load(f)
a = d.get('auths', {}).get('https://index.docker.io/v1/', {}).get('auth')
if a:
print(base64.b64decode(a).decode().split(':',1)[0])
except Exception:
pass
" 2>/dev/null)
fi
fi
if [[ -n "$current_user" && "$current_user" == "$DOCKERHUB_USERNAME" ]]; then
log_ok "Docker Hub 已登录为 ${current_user},跳过认证"
return 0
fi
echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin >/dev/null 2>&1
if [[ $? -eq 0 ]]; then
log_ok "Docker Hub 登录完成(用户名:${DOCKERHUB_USERNAME})"
return 0
else
log_error "Docker Hub 登录失败(请检查 Token 是否有效/未过期)"
return 1
fi
}
trigger_release_workflow() {
local repo_path="$1"
local version="${2:-}"
if [[ -z "$repo_path" ]]; then
log_error "用法: trigger_release_workflow <仓库绝对路径> [版本号]"
return 99
fi
if [[ ! -d "${repo_path}/.git" ]]; then
log_error "${repo_path} 不是 Git 仓库"
return 98
fi
if [[ -z "$GH_TOKEN" ]]; then
log_error "环境变量 GH_TOKEN 未设置,无法调用 GitHub API"
return 96
fi
local repo_slug
repo_slug=$(get_repo_slug "$repo_path")
if [[ -z "$repo_slug" ]]; then
log_error "无法从 git remote 解析 GitHub 仓库地址(owner/repo)"
return 95
fi
pushd "${repo_path}" >/dev/null || {
log_error "无法进入目录 ${repo_path}"
return 97
}
if ! gh auth status -h github.com >/dev/null 2>&1; then
if ! echo "$GH_TOKEN" | gh auth login --with-token; then
log_error "GitHub CLI Token 登录失败"
popd >/dev/null
return 10
fi
fi
echo -e "${gl_bai}【第 ${gl_huang}1${gl_bai} 步】设置 Actions 密钥 DOCKERHUB_USERNAME"
if [[ -n "$DOCKERHUB_USERNAME" ]]; then
if gh secret set DOCKERHUB_USERNAME -R "$repo_slug" --body "$DOCKERHUB_USERNAME" >/dev/null; then
log_ok "DOCKERHUB_USERNAME 已设置"
else
log_error "设置 DOCKERHUB_USERNAME 失败"
popd >/dev/null
return 1
fi
else
log_warn "DOCKERHUB_USERNAME 未配置,跳过"
fi
echo -e "${gl_bai}【第 ${gl_huang}2${gl_bai} 步】设置 Actions 密钥 DOCKERHUB_TOKEN"
if [[ -n "$DOCKERHUB_TOKEN" ]]; then
if gh secret set DOCKERHUB_TOKEN -R "$repo_slug" --body "$DOCKERHUB_TOKEN" >/dev/null; then
log_ok "DOCKERHUB_TOKEN 已设置"
else
log_error "设置 DOCKERHUB_TOKEN 失败"
popd >/dev/null
return 2
fi
else
log_warn "DOCKERHUB_TOKEN 未配置,跳过"
fi
if [[ "${EXTRA_TRIGGER_WORKFLOW}" == "1" ]]; then
echo -e "${gl_bai}【第 ${gl_huang}3${gl_bai} 步】手动触发 release workflow(main 分支)"
if ! gh workflow run release --ref main -R "$repo_slug"; then
log_error "启动 release workflow 失败"
popd >/dev/null
return 3
fi
log_ok "release workflow 已触发"
else
echo -e "${gl_bai}【第 ${gl_huang}3${gl_bai} 步】跳过手动触发(tag push 已自动触发 workflow)"
echo -e " ${gl_hui}如需强制手动触发,请设置 EXTRA_TRIGGER_WORKFLOW=1${gl_bai}"
fi
echo
echo -e "${gl_lv}🎉 Workflow 处理完成!${gl_bai}"
if [[ -n "$version" ]]; then
echo -e "${gl_hui}💡 查看本次 tag 触发的运行记录:gh run list --workflow=release.yml -R ${repo_slug} --limit 5${gl_bai}"
echo -e "${gl_hui}💡 查看发布结果:gh release view ${version} -R ${repo_slug}${gl_bai}"
fi
popd >/dev/null
return 0
}
build_and_push() {
local project_root="$1"
local version="${2:-}"
local msg="${3:-}"
if [[ -z "$project_root" ]]; then
log_error "项目根目录为空,无法继续(应由菜单传入)"
echo -e "${col43}43.${gl_bai} FanVideoCT 视频剪切 ${col44}44.${gl_bai} FanNginx 反向代理"
break_end
return 1
fi
local script_path="${project_root}/scripts/build-and-push.sh"
if [[ ! -d "$project_root" ]]; then
log_error "目录不存在:$project_root"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
return 1
fi
if [[ ! -f "$script_path" ]]; then
echo -e "${gl_huang}❌ 脚本不存在:$script_path${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
return 1
fi
if [[ ! -x "$script_path" ]]; then
echo -e "${gl_huang}⚠️ 脚本缺少执行权限,自动执行 chmod +x${gl_bai}"
chmod +x "$script_path"
fi
local cur_ver next_ver
cur_ver=$(get_git_latest_tag "$project_root")
next_ver=$(next_version "$cur_ver")
if [[ -z "$version" ]]; then
echo -e ""
echo -e "${gl_huang}>>> 添加版本号 ${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "当前版本:${gl_lv}${cur_ver}${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e " ${gl_bufan}1.${gl_bai} 将作为 GitHub 标签/发布 版本号"
echo -e " ${gl_bufan}2.${gl_bai} 输入后回车开始构建、提交、推送(默认递增,如 ${gl_lv}${next_ver}${gl_bai})"
echo -e " ${gl_huang}0.${gl_bai} 返回上一级选单"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
while true; do
read -r -e -p "请输入版本号(回车默认 ${gl_huang}${next_ver}${gl_bai}): " version
if [[ -z "$version" ]]; then
version="$next_ver"
echo -ne "\r\033[K"
break
fi
if [[ "$version" == "0" ]]; then
echo -e "\r\033[K${gl_huang}📤 返回上级${gl_bai}"
sleep_fractional 0.5
return 2
fi
echo -ne "\r\033[K"
break
done
fi
if [[ -z "$msg" ]]; then
while true; do
read -r -e -p "请输入提交注释 (回车默认 ${gl_huang}日常更新${gl_bai}): " msg
if [[ -z "$msg" ]]; then
msg="日常更新"
echo -ne "\r\033[K"
break
fi
if [[ "$msg" == "0" ]]; then
echo -e "\r\033[K${gl_huang}📤 返回上级${gl_bai}"
sleep_fractional 0.5
return 2
fi
echo -ne "\r\033[K"
break
done
fi
echo
echo -e "项目目录: ${gl_lv}$project_root${gl_bai}"
echo -e "脚本路径: ${gl_lv}$script_path${gl_bai}"
echo -e "版 本 号: ${gl_lv}$version${gl_bai}"
echo -e "注释内容: ${gl_lv}$msg${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local confirm=""
local c_prompt="确认开始构建推送? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}):"
while true; do
read -r -e -p "$c_prompt " confirm
case "$confirm" in
"")
echo -ne "\033[1A\r\033[K"
c_prompt="${gl_hong}❌ 不能为空${gl_bai} → 确认开始构建推送? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}):"
continue
;;
"0")
echo -e "\r\033[K${gl_huang}📤 返回上级${gl_bai}"
sleep_fractional 0.5
return 2
;;
[Yy])
echo -ne "\r\033[K"
break
;;
[Nn])
echo -e "\r\033[K${gl_huang}✅ 已取消操作${gl_bai}"
sleep_fractional 0.5
return 2
;;
*)
echo -ne "\033[1A\r\033[K"
c_prompt="${gl_hong}❌ 无效输入${gl_bai} → 确认开始构建推送? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}):"
continue
;;
esac
done
echo
echo -e "${gl_zi}>>> 第 1/5 步:确保 GitHub CLI 认证 ${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if ! ensure_gh_auth "$project_root"; then
log_error "GitHub CLI 认证失败,中止后续步骤"
return 1
fi
echo
echo -e "${gl_zi}>>> 第 2/5 步:确保 Docker Hub 认证 ${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if ! ensure_dockerhub_auth; then
log_warn "Docker Hub 认证失败,继续执行(如不需要本地推送镜像可忽略)"
fi
export ENABLE_CNB_CHECK=1
if ! ensure_cnb_token; then
log_warn "CNB_TOKEN 校验失败,继续执行(如不需要CNB能力可忽略)"
fi
echo
echo -e "${gl_zi}>>> 第 3/5 步:清理远端 Release / tag & 推送未提交改动 ${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
pre_clean_remote_tag "$project_root" "$version"
echo
echo -e "${gl_hui} 提交并推送工作区的未提交改动(保证构建脚本遇到干净工作区)${gl_bai}"
pushd "$project_root" >/dev/null 2>&1 || {
log_error "无法进入目录 $project_root"
return 1
}
git_push "$msg"
local push_rc=$?
popd >/dev/null 2>&1
if [[ "$push_rc" -ne 0 ]]; then
log_error "git_push 失败,中止后续步骤"
return 1
fi
echo
echo -e "${gl_zi}>>> 第 4/5 步:执行项目构建脚本 ${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if ! "$script_path" "$version" --yes -m "$msg"; then
log_error "构建脚本执行失败,中止后续步骤"
return 1
fi
echo
echo -e "${gl_zi}>>> 第 5/5 步:处理 Release Workflow ${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if ! trigger_release_workflow "$project_root" "$version"; then
log_error "Release Workflow 处理失败"
return 1
fi
}
function count_md_docs {
local cmd_count
local ops_count
local win_count
local total_count
cmd_count=$(find "./command" -type f -name "*.md" -print0 | grep -zc .)
ops_count=$(find "./ops-script" -type f -name "*.md" -print0 | grep -zc .)
win_count=$(find "./windows" -type f -name "*.md" -print0 | grep -zc .)
total_count=$((cmd_count + ops_count + win_count))
echo
echo -e "${gl_huang}>>> CmdBox 文章统计${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo "- 命令文章:${gl_lv}${cmd_count}${reset} 篇"
echo "- 运维脚本:${gl_lv}${ops_count}${reset} 篇"
echo "- Win 脚本:${gl_lv}${win_count}${reset} 篇"
echo "- 合计文章:${gl_huang}${total_count}${reset} 篇"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
}
project_push() {
local proj_name="$1"
local proj_dir="$2"
local docker_filter="$3"
local dockerhub_repo="$4"
echo -e ""
echo -e "${gl_zi}>>> ${proj_name} 项目推送 ${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if [[ -n "${docker_filter}" ]]; then
compose_ps_beautify
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
fi
build_and_push "${proj_dir}"
local rc=$?
if [[ "$rc" -eq 2 ]]; then
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
return 0
fi
if [[ "$rc" -ne 0 ]]; then
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
return 0
fi
if [[ -n "$dockerhub_repo" ]]; then
echo
show_dockerhub_info "$dockerhub_repo" 5
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
}
fan_panel_push() { project_push "Fan Panel" "/vol1/1000/GitHub/fan-panel" "fan-panel" "mobufan/fan-panel"; }
fan_video_push() { project_push "Fan Video" "/vol1/1000/GitHub/fan-video" "fan-video" "mobufan/fan-video"; }
fan_md_push() { project_push "Fan MD" "/vol1/1000/GitHub/fan-md" "fan-md" "mobufan/fan-md"; }
dufs-zh_push() { project_push "Dufs-zh" "/vol1/1000/GitHub/dufs-zh" "dufs-zh" "mobufan/dufs-zh"; }
2panel_push() { project_push "2Panel" "/vol1/1000/GitHub/2panel" "" "mobufan/2panel"; }
fan-shop_push() { project_push "Fan Shop" "/vol1/1000/GitHub/fan-shop" "fan-shop" "mobufan/fan-shop"; }
fan-files_push() { project_push "Fan Files" "/vol1/1000/GitHub/fan-files" "" "mobufan/fan-files"; }
fan-reubah_push() { project_push "Fan Reubah" "/vol1/1000/GitHub/fan-reubah" "fan-reubah" "mobufan/fan-reubah"; }
cmdbox-main_push() { project_push "CmdBox" "/vol1/1000/GitHub/cmdbox-main" "cmdbox" "mobufan/cmdbox"; }
fan-webssh_push() { project_push "Fan WebSSH" "/vol1/1000/GitHub/fan-webssh" "fan-webssh" "mobufan/fan-webssh"; }
fan_random_push() { project_push "Fan Random" "/vol1/1000/GitHub/fan-random" "fan-random" "mobufan/fan-random"; }
fan_video_dl_push() { project_push "Fan Video DL" "/vol1/1000/GitHub/fan-video-dl" "fan-video-dl" "mobufan/fan-video-dl"; }
fan_video_ct_push() { project_push "Fan Video CT" "/vol1/1000/GitHub/fan-video-ct" "fan-video-ct" "mobufan/fan-video-ct"; }
fan_nginx_push() { project_push "Fan Nginx" "/vol1/1000/GitHub/fan-nginx" "fan-nginx" "mobufan/fan-nginx"; }
git_project_menu() {
check_tokens || true
while true; do
clear
echo -e "${gl_zi}>>> Git 项目构建并推送${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}1. ${gl_bai}FanPanel 导航页 ${gl_bufan}2. ${gl_bai}FanVideo 影视库"
echo -e "${gl_bufan}3. ${gl_bai}FanMD 云文档 ${gl_bufan}4. ${gl_bai}Dufs-zh 文件服务"
echo -e "${gl_bufan}5. ${gl_bai}2Panel 计划任务 ${gl_bufan}6. ${gl_bai}FanShop 容器管理"
echo -e "${gl_bufan}7. ${gl_bai}FanFiles 文件管理 ${gl_bufan}8. ${gl_bai}FanReubah 格式转换"
echo -e "${gl_bufan}9. ${gl_bai}CmdBox 命令 ${gl_bufan}10. ${gl_bai}FanWebSSH 终端面板"
echo -e "${gl_bufan}11. ${gl_bai}FanRandom 随机壁纸 ${gl_bufan}12. ${gl_bai}FanVideoDL 视频下载"
echo -e "${gl_bufan}13. ${gl_bai}FanVideoCT 视频剪切 ${gl_bufan}14. ${gl_bai}FanNginx 反向代理"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_huang}0. ${gl_bai}返回上一级选单 ${gl_hong}00. ${gl_bai}退出脚本"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}请输入你的选择: ")" action
case "$action" in
1) fan_panel_push ;;
2) fan_video_push ;;
3) fan_md_push ;;
4) dufs-zh_push ;;
5) 2panel_push ;;
6) fan-shop_push ;;
7) fan-files_push ;;
8) fan-reubah_push ;;
9) count_md_docs; cmdbox-main_push ;;
10) fan-webssh_push ;;
11) fan_random_push ;;
12) fan_video_dl_push ;;
13) fan_video_ct_push ;;
14) fan_nginx_push ;;
0)
proj_mgmt_tool
;;
00 | 000 | 0000)
exit_script
;;
"")
echo -ne "\033[1A\r\033[K"
echo -e "${gl_hong}❌ 不能为空${gl_bai},请重新选择"
sleep_fractional 0.6
echo -ne "\033[1A\r\033[K"
continue
;;
*)
handle_invalid_input
;;
esac
done
}
show_service_url() {
local service="${1:-fan-video}"
local url=""
local port=""
local ip
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
[ -z "$ip" ] && ip=$(ip route get 1 2>/dev/null | awk '{print $7}' | head -1)
[ -z "$ip" ] && ip=$(ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | head -1)
[ -z "$ip" ] && ip="127.0.0.1"
port=$(journalctl -u "$service" --no-pager -n 200 -o cat 2>/dev/null \
| grep -E 'msg":"fan-video 启动于 :[0-9]+|Listening at: http://0\.0\.0\.0:[0-9]+|Server\(http\) is running on: http://localhost:[0-9]+' \
| grep -oE ':[0-9]+$' | sed 's/^://' | head -1)
if [ -z "$port" ];then
port=$(grep -E '^PORT=' "/etc/${service}.conf" 2>/dev/null | head -1 | cut -d= -f2)
fi
if [ -z "$port" ];then
local exec_cmd
exec_cmd=$(systemctl show -p ExecStart "$service" 2>/dev/null | cut -d= -f2-)
port=$(echo "$exec_cmd" | grep -oE ' -{1,2}port[ =]+[0-9]+' | grep -oE '[0-9]+' | head -1)
[ -z "$port" ] && port=$(echo "$exec_cmd" | grep -oE -e '--bind 0\.0\.0\.0:[0-9]+' | grep -oE '[0-9]+$' | head -1)
fi
if [ -z "$port" ] && command -v ss >/dev/null 2>&1;then
local pid
pid=$(systemctl show -p MainPID "$service" 2>/dev/null | cut -d= -f2-)
if [[ "$pid" =~ ^[0-9]+$ && "$pid" -gt 0 ]];then
port=$(ss -tlnp 2>/dev/null | grep ",pid=$pid," | grep -oE ':[0-9]+' | sed 's/^://' | head -1)
fi
fi
if [ -z "$port" ] && command -v docker >/dev/null 2>&1;then
local dports
dports=$(docker port "$service" 2>/dev/null | head -1)
if [ -n "$dports" ];then
port=$(echo "$dports" | grep -oE '0\.0\.0\.0:[0-9]+|\[::\]:[0-9]+' | grep -oE '[0-9]+$' | head -1)
fi
fi
if [ -z "$port" ];then
port=$(grep -E "ports:" -A5 "/vol1/1000/compose/${service}/docker-compose.yml" 2>/dev/null \
| grep -oE '"[0-9]+:[0-9]+"|[0-9]+:[0-9]+' | head -1 | cut -d: -f1 | tr -d '"')
fi
if [ -n "$port" ]; then
url="http://${ip}:${port}"
fi
if [ -n "$url" ]; then
echo -e "访问地址:${gl_lv}${url}${gl_bai}"
else
echo -e "访问地址:${gl_hong}无法获取访问地址${gl_bai}"
return 1
fi
}
show_service_status() {
local service="${1:-opencode}"
local version=""
local ver_regex='\b(v[0-9]+\.[0-9]+\.[0-9]+|[0-9]+\.[0-9]+\.[0-9]+)\b'
if command -v "$service" &>/dev/null; then
version=$("$service" --version 2>/dev/null | grep -vE '[_#]{3,}' | grep -oE "$ver_regex" | head -1)
[ -z "$version" ] && version=$("$service" version 2>/dev/null | grep -vE '[_#]{3,}' | grep -oE "$ver_regex" | head -1)
fi
if [ -z "$version" ]; then
local exec_path
exec_path=$(systemctl show -p ExecStart "$service" 2>/dev/null | cut -d= -f2 | awk '{print $1}')
if [ -n "$exec_path" ] && [ -x "$exec_path" ]; then
version=$("$exec_path" --version 2>/dev/null | grep -vE '[_#]{3,}' | grep -oE "$ver_regex" | head -1)
[ -z "$version" ] && version=$("$exec_path" version 2>/dev/null | grep -vE '[_#]{3,}' | grep -oE "$ver_regex" | head -1)
fi
fi
if [ -z "$version" ] && command -v journalctl &>/dev/null; then
version=$(journalctl -u "$service" --no-pager -n 50 -o cat 2>/dev/null | grep -oE "$ver_regex" | head -1)
fi
if [[ ! "$version" =~ $ver_regex ]]; then
version=""
fi
if systemctl is-active --quiet "$service"; then
echo -e "运行状态:${gl_lv}$service 正在运行${gl_bai}"
else
echo -e "运行状态:${gl_hong}$service 未运行${gl_bai}"
fi
if [ -n "$version" ]; then
echo -e "版本信息:${gl_huang}$version${gl_bai}"
else
echo -e "版本信息:${gl_huang}无法获取${gl_bai}"
fi
}
docker_compose_manager() {
local FWORK_DIR="${1:-$PWD}"
if ! cd "$FWORK_DIR" 2>/dev/null; then
echo -e ""
log_error "无法进入目录: ${gl_huang}$FWORK_DIR${gl_bai}"
exit_animation
return 1
fi
safe_read() {
local prompt="$1"
local var_name="$2"
local type="${3:-"string"}"
local default_value="${4:-}"
local min_value="${5:-}"
local max_value="${6:-}"
local max_retry=3
local retry_count=0
while [[ $retry_count -lt $max_retry ]]; do
if [[ -n "$default_value" ]]; then
read -r -e -p "$(echo -e "${gl_bai}$prompt (${gl_huang}默认: $default_value${gl_bai}): ")" "$var_name"
[[ -z "${!var_name}" ]] && eval "$var_name=\"\$default_value\""
else
read -r -e -p "$(echo -e "${gl_bai}$prompt: ")" "$var_name"
fi
if [[ -z "${!var_name}" ]]; then
if [[ -n "$default_value" ]]; then
eval "$var_name=\"\$default_value\""
return 0
else
log_error "输入不能为空"
retry_count=$((retry_count + 1))
continue
fi
fi
case "$type" in
"number")
if ! [[ "${!var_name}" =~ ^[0-9]+$ ]]; then
log_error "请输入数字"
retry_count=$((retry_count + 1))
continue
fi
if [[ -n "$min_value" ]] && [[ "${!var_name}" -lt "$min_value" ]]; then
log_error "输入值不能小于 $min_value"
retry_count=$((retry_count + 1))
continue
fi
if [[ -n "$max_value" ]] && [[ "${!var_name}" -gt "$max_value" ]]; then
log_error "输入值不能大于 $max_value"
retry_count=$((retry_count + 1))
continue
fi
;;
"y/n")
if ! [[ "${!var_name}" =~ ^[YyNn]$ ]]; then
log_error "请输入 y 或 n"
retry_count=$((retry_count + 1))
continue
fi
;;
esac
return 0
done
log_error "输入尝试次数过多,返回上一级"
return 1
}
handle_invalid_input() {
echo -ne "\r\033[K${gl_huang}无效的输入,请重新输入! ${gl_zi} 1 ${gl_huang} 秒后"
sleep_fractional 1
echo -ne "\r\033[K"
sleep_fractional 1
echo -ne "\r\033[K${gl_lv}无效的输入,请重新输入! ${gl_zi}0${gl_lv} 秒后"
sleep_fractional 1
echo -ne "\r\033[K"
sleep_fractional 0.5
echo -ne "\r\033[K"
return 2
}
column_if_available() {
if command -v column &> /dev/null; then
column -t -s $'\t'
else
cat
fi
}
list_beautify_compose_images() {
if [[ ! -f "docker-compose.yml" && ! -f "compose.yml" ]]; then
echo -e "${gl_hong}❌ 当前目录未找到 docker-compose.yml / compose.yml${gl_bai}"
return 1
fi
{
printf "%s%s\t%s\t%s\t%s\t%s%s\n" "$gl_hui" "服务名" "完整镜像名" "镜像ID" "大小" "架构" "$reset"
printf "%s%s\t%s\t%s\t%s\t%s%s\n" "$gl_hui" "----------" "----------" "----------" "----------" "----------" "$reset"
docker compose config | awk '
BEGIN { in_svc=0; svc_name="" }
/^services:/ { in_svc=1; next }
in_svc == 1 && /^ [^ ]/ {
sub(/:$/,"");
svc_name=$0;
gsub(/^[ \t]+/,"",svc_name); # 移除服务名开头空格
next
}
in_svc ==1 && /^ image:/ {
sub(/^ image:[ \t]*/,"");
print svc_name "\t" $0;
}' | while IFS=$'\t' read -r svc img; do
if [[ -z "$svc" || -z "$img" ]]; then
continue
fi
if ! docker image inspect "$img" &>/dev/null; then
img_id="${gl_hong}镜像不存在${reset}"
size=""
platform=""
else
img_id=$(docker image inspect --format '{{.Id}}' "$img" | sed 's/^sha256://' | cut -c1-12)
size_raw=$(docker image inspect --format '{{.Size}}' "$img")
if [[ "$size_raw" -gt 1073741824 ]]; then
size=$(awk -v s="$size_raw" 'BEGIN{printf "%.2f GB", s/1073741824}')
elif [[ "$size_raw" -gt 1048576 ]]; then
size=$(awk -v s="$size_raw" 'BEGIN{printf "%.2f MB", s/1048576}')
elif [[ "$size_raw" -gt 1024 ]]; then
size=$(awk -v s="$size_raw" 'BEGIN{printf "%.2f KB", s/1024}')
else
size="${size_raw} B"
fi
platform=$(docker image inspect --format '{{.Os}}/{{.Architecture}}' "$img")
fi
printf "%s\t%s\t%s\t%s\t%s\n" \
"${gl_lv}${svc}${reset}" \
"${gl_huang}${img}${reset}" \
"${gl_bufan}${img_id}${reset}" \
"${gl_lan}${size}${reset}" \
"${gl_bai}${platform}${reset}"
done
} | column_if_available
}
list_beautify_all() {
echo
echo -e "${gl_zi}>>> 当前 ${gl_huang}$current_dir_name${gl_zi} 项目镜像详情${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
list_beautify_compose_images
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
}
get_internal_ip() {
local ip=""
if command -v hostname >/dev/null 2>&1; then
ip=$(hostname -I | awk '{print $1}')
elif command -v ip >/dev/null 2>&1; then
ip=$(ip route get 1 2>/dev/null | awk '{print $7}' | head -1)
elif command -v ifconfig >/dev/null 2>&1; then
ip=$(ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | head -1)
fi
echo "$ip"
}
show_inner_url() {
local yml="docker-compose.yml"
[[ -f $yml ]] || {
echo -e "当前目录没有 $yml"
return 1
}
local port
port=$(awk '
match($0, /-[[:space:]]*"?([0-9]+):[0-9]+/) {
split(substr($0, RSTART, RLENGTH), a, /:/);
gsub(/[^0-9]/, "", a[1]);
print a[1]; exit
}' "$yml")
[[ -z $port ]] && {
echo -e "未检测到 http 端口映射"
return 2
}
local ip=$(hostname -I | awk '{print $1}')
echo -e "服务访问链接:${gl_lv}http://${ip}:${port}${gl_bai}"
}
check_container_status() {
local container_name="$1"
if docker inspect "$container_name" &>/dev/null; then
if docker inspect -f '{{.State.Running}}' "$container_name" 2>/dev/null | grep -q "true"; then
echo "${gl_lv}"
else
echo "${gl_hong}"
fi
else
echo "${gl_hui}"
fi
}
create_file() {
local file_name=${1:-}
echo -e ""
echo -e "${gl_zi}>>> 创建文件 ${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if [[ -z $file_name ]]; then
read -r -e -p "$(echo -e "${gl_bai}请输入文件名 (${gl_huang}0${gl_bai}返回): ")" file_name
[[ -z "$file_name" ]] && { cancel_return "上一级选单"; return 1; }
[[ "$file_name" == "0" ]] && { cancel_return "上一级选单"; return 1; }
fi
if [[ -e $file_name ]]; then
echo -e "${gl_hong}文件已存在:$file_name${gl_bai}"
local overwrite
read -r -e -p "$(echo -e "${gl_bai}是否覆盖?(${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai},${gl_huang}0${gl_bai}返回): ")" overwrite
[[ "$overwrite" == "0" ]] && { cancel_return "上一级选单"; return 1; }
[[ "$overwrite" != [yY] ]] && { echo -e "${gl_huang}已取消操作 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai} \c"; sleep_fractional 0.6; return; }
fi
echo -e "${gl_huang}将创建文件:$file_name${gl_bai}"
echo -e "${gl_bufan}按回车键开始用 nano 编辑文件(${gl_huang}Ctrl+X${gl_bai}退出nano)${gl_bai}"
read -r -n 1 -s
local tmp line_count=0
tmp=$(mktemp)
if nano "$tmp"; then
if [[ -s $tmp ]]; then
line_count=$(wc -l < "$tmp" 2>/dev/null || echo 0)
mv "$tmp" "$file_name"
echo -e "${gl_huang}文件创建成功,共写入 $line_count 行${gl_bai}"
[[ -s $file_name ]] && cat -n "$file_name"
else
touch "$file_name"
echo -e "${gl_bufan}创建空文件完成${gl_bai}"
fi
else
echo -e "${gl_hong}nano 编辑失败或已取消${gl_bai}"
rm -f "$tmp"
return
fi
if [[ $file_name =~ \.(sh|py|pl)$ ]]; then
local add_exec
echo -e ""
read -r -e -p "$(echo -e "${gl_bai}检测到脚本文件,是否添加执行权限? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai},${gl_huang}0${gl_bai}返回): ")" add_exec
[[ "$add_exec" == "0" ]] && { echo -e "${gl_huang}已跳过权限设置${gl_bai}"; }
[[ "$add_exec" == [yY] ]] && chmod +x "$file_name"
new_oct=$(stat -c "%a" "$file_name")
new_sym=$(stat -c "%A" "$file_name")
echo -e ""
log_ok "权限已修改!"
echo -e ""
echo -e "${gl_bai}修改后 ${gl_huang}${file_name}${gl_bai} 权限: ${gl_lv}${new_sym}${gl_bai} (${gl_huang}${new_oct}${gl_bai})"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
}
install() {
[[ $# -eq 0 ]] && {
log_error "未提供软件包参数!"
return 1
}
local pkg mgr ver cmd_ver pkg_ver installed=false
for pkg in "$@"; do
installed=false
ver=""
if command -v "$pkg" &>/dev/null; then
cmd_ver=$("$pkg" --version 2>/dev/null | head -n1 | tr -cd '[:print:]' | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo "")
[[ -n "$cmd_ver" ]] && ver="$cmd_ver"
installed=true
fi
if [[ "$pkg" == "7zip" || "$pkg" == "7z" ]]; then
if command -v 7z &>/dev/null; then
ver=$(7z 2>&1 | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo "")
[[ -n "$ver" ]] && installed=true
fi
fi
if [[ "$installed" == false ]]; then
if command -v opkg &>/dev/null; then
if opkg list-installed | grep -q "^${pkg} "; then
installed=true
ver=$(opkg list-installed | grep "^${pkg} " | awk '{print $3}' 2>/dev/null || echo "")
fi
elif command -v dpkg-query &>/dev/null; then
if dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "install ok installed"; then
installed=true
ver=$(dpkg-query -W -f='${Version}' "$pkg" 2>/dev/null || echo "")
fi
elif command -v rpm &>/dev/null; then
if rpm -q "$pkg" &>/dev/null; then
installed=true
ver=$(rpm -q --qf '%{VERSION}' "$pkg" 2>/dev/null || echo "")
fi
elif command -v apk &>/dev/null; then
if apk info "$pkg" 2>/dev/null | grep -q "^installed"; then
installed=true
ver=$(apk info -a "$pkg" 2>/dev/null | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo "")
fi
elif command -v pacman &>/dev/null; then
if pacman -Qi "$pkg" &>/dev/null; then
installed=true
ver=$(pacman -Qi "$pkg" 2>/dev/null | grep -i "version" | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo "")
fi
fi
fi
if [[ "$installed" == true ]]; then
echo -e "${gl_huang}${pkg}${gl_bai} ${gl_lv}已安装${gl_bai}" \
"$([[ -n "$ver" ]] && echo "版本 ${gl_lv}${ver}${gl_bai}")"
continue
fi
echo -e ""
echo -e "${gl_huang}开始安装:${gl_bai}${pkg}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local install_success=false
for mgr in opkg dnf yum apt apk pacman zypper pkg; do
if ! command -v "$mgr" &>/dev/null; then
continue
fi
case $mgr in
opkg)
echo -e "${gl_bai}使用包管理器: ${gl_zi}opkg (OpenWrt/iStoreOS)${gl_bai}"
if [[ "$pkg" == "7zip" || "$pkg" == "7z" ]]; then
echo -e "${gl_bai}正在安装: ${gl_lv}p7zip${gl_bai}"
opkg update && opkg install p7zip && install_success=true
else
opkg update && opkg install "$pkg" && install_success=true
fi
;;
dnf)
echo -e "${gl_bai}使用包管理器: ${gl_zi}dnf (Fedora/RHEL)${gl_bai}"
dnf -y update && dnf install -y "$pkg" && install_success=true
;;
yum)
echo -e "${gl_bai}使用包管理器: ${gl_zi}yum (CentOS/RHEL)${gl_bai}"
yum -y update && yum install -y "$pkg" && install_success=true
;;
apt)
echo -e "${gl_bai}使用包管理器: ${gl_zi}apt (Debian/Ubuntu)${gl_bai}"
apt update -y && apt install -y "$pkg" && install_success=true
;;
apk)
echo -e "${gl_bai}使用包管理器: ${gl_zi}apk (Alpine)${gl_bai}"
apk update && apk add "$pkg" && install_success=true
;;
pacman)
echo -e "${gl_bai}使用包管理器: ${gl_zi}pacman (Arch/Manjaro)${gl_bai}"
pacman -Syu --noconfirm && pacman -S --noconfirm "$pkg" && install_success=true
;;
zypper)
echo -e "${gl_bai}使用包管理器: ${gl_zi}zypper (openSUSE)${gl_bai}"
zypper refresh && zypper install -y "$pkg" && install_success=true
;;
pkg)
echo -e "${gl_bai}使用包管理器: ${gl_zi}pkg (FreeBSD)${gl_bai}"
pkg update && pkg install -y "$pkg" && install_success=true
;;
esac
[[ "$install_success" == true ]] && break
done
if [[ "$install_success" == true ]]; then
echo -e "${gl_lv}✓ ${pkg} 安装成功${gl_bai}"
else
echo -e "${gl_hong}✗ ${pkg} 安装失败${gl_bai}"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
done
}
get_main_service() {
local compose_file="${1:-docker-compose.yml}"
local service_name=""
if [[ ! -f "$compose_file" ]]; then
compose_file="docker-compose.yaml"
fi
if [[ ! -f "$compose_file" ]]; then
echo ""
return
fi
if command -v yq &>/dev/null; then
service_name=$(yq e '.services | keys | .[0]' "$compose_file" 2>/dev/null)
elif command -v docker-compose &>/dev/null; then
service_name=$(docker-compose config --services 2>/dev/null | head -1)
elif docker compose version &>/dev/null; then
service_name=$(docker compose config --services 2>/dev/null | head -1)
else
service_name=$(grep -A 5 "^services:" "$compose_file" | grep -E "^ [a-zA-Z0-9_-]+:" | head -1 | tr -d ': ' 2>/dev/null)
fi
if [[ -z "$service_name" ]]; then
service_name=$(basename "$(pwd)")
fi
echo "$service_name"
}
select_service() {
local compose_file="${1:-docker-compose.yml}"
local services=()
if [[ ! -f "$compose_file" ]]; then
compose_file="docker-compose.yaml"
fi
if [[ ! -f "$compose_file" ]]; then
echo ""
return
fi
if command -v yq &>/dev/null; then
mapfile -t services < <(yq e '.services | keys | .[]' "$compose_file" 2>/dev/null)
elif command -v docker-compose &>/dev/null; then
mapfile -t services < <(docker-compose config --services 2>/dev/null)
elif docker compose version &>/dev/null; then
mapfile -t services < <(docker compose config --services 2>/dev/null)
else
mapfile -t services < <(grep -A 20 "^services:" "$compose_file" | grep -E "^ [a-zA-Z0-9_-]+:" | tr -d ': ' 2>/dev/null)
fi
if [[ ${#services[@]} -eq 0 ]]; then
echo ""
return
fi
if [[ ${#services[@]} -eq 1 ]]; then
echo "${services[0]}"
return
fi
clear
echo -e ""
echo -e "${gl_zi}>>> 选择服务 ${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bai}检测到多个服务,请选择要操作的服务:${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
for i in "${!services[@]}"; do
local service="${services[$i]}"
local container_color=$(check_container_status "$service")
local status_text=""
if [[ "$container_color" == "${gl_lv}" ]]; then
status_text="${gl_lv}[运行中]${gl_bai}"
elif [[ "$container_color" == "${gl_hong}" ]]; then
status_text="${gl_hong}[已停止]${gl_bai}"
else
status_text="${gl_hui}[不存在]${gl_bai}"
fi
echo -e "${gl_bufan}$((i + 1)). ${gl_bai}${container_color}${service}${gl_bai} $status_text"
done
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_huang}0. ${gl_bai}返回上一级选单"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}请输入你的选择 (${gl_huang}0${gl_bai}-${gl_hong}${#services[@]}${gl_bai}): ")" service_choice
if [[ "$service_choice" =~ ^[0-9]+$ ]]; then
if [[ $service_choice -eq 0 ]]; then
echo ""
return
elif [[ $service_choice -le ${#services[@]} ]]; then
echo "${services[$((service_choice - 1))]}"
return
fi
fi
echo ""
}
view_compose_logs() {
echo -e ""
echo -e "${gl_bai}正在实时跟踪 ${gl_huang}$current_dir_name${gl_bai} 项目日志 ${gl_hong}(Ctrl+C 退出日志查看)${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
docker compose logs -f -t --tail 100
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
}
show_compose_commands_menu() {
local WORK_DIR="${1:-.}"
if ! cd "$WORK_DIR" 2>/dev/null; then
echo -e ""
log_error "无法进入目录: ${gl_huang}$WORK_DIR${gl_bai}"
exit_animation
return 1
fi
local current_dir="$(pwd)"
local current_dir_name=$(basename "$PWD")
local MAIN_SERVICE=$(get_main_service)
[[ -z "$MAIN_SERVICE" ]] && MAIN_SERVICE="$current_dir_name"
while true; do
clear
echo -e ""
echo -e "${gl_zi}>>> Compose项目菜单 ${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bai}当前工作目录: ${gl_huang}$current_dir${gl_bai}"
echo -e "${gl_bai}当前项目名称: ${gl_huang}$current_dir_name${gl_bai}"
echo -e "${gl_bai}内网 IP 地址: ${gl_huang}$(get_internal_ip)${gl_bai}"
docker inspect -f \
'{{if .State.Running}}'"$gl_lv"'已启动'"$gl_bai"'{{else}}'"$gl_hui"'[已停止]'"$gl_bai"'{{end}}' \
"$MAIN_SERVICE" >/dev/null 2>&1 && {
echo -e "${gl_bai}主要容器状态:$(docker inspect -f \
'{{if .State.Running}}'"$gl_lv"'已启动'"$gl_bai"'{{else}}'"$gl_hui"'[已停止]'"$gl_bai"'{{end}}' \
"$MAIN_SERVICE")"
} || {
printf "${gl_bai}主要容器状态:${gl_hui}容器 ${gl_huang}%s${gl_hui} [不存在]${gl_bai}\n" "$MAIN_SERVICE"
}
show_inner_url
local container_color=$(check_container_status "$MAIN_SERVICE")
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
compose_ps_beautify
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
list_beautify_compose_images
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}1. ${gl_bai}停止${container_color}$current_dir_name${gl_bai}服务 ${gl_bufan}2. ${gl_bai}启动${container_color}$current_dir_name${gl_bai}服务"
echo -e "${gl_bufan}3. ${gl_bai}重启${container_color}$current_dir_name${gl_bai}服务 ${gl_bufan}4. ${gl_bai}更新${container_color}$current_dir_name${gl_bai}容器"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}5. ${gl_bai}查看${container_color}$current_dir_name${gl_bai}配置文件 ${gl_bufan}6. ${gl_bai}编辑${container_color}$current_dir_name${gl_bai}配置"
echo -e "${gl_bufan}7. ${gl_bai}创建${container_color}$current_dir_name${gl_bai}配置文件 ${gl_bufan}8. ${gl_bai}查看${container_color}$current_dir_name${gl_bai}最终配置"
echo -e "${gl_bufan}9. ${container_color}$current_dir_name${gl_bai}服务日志 ${gl_bufan}10. ${gl_bai}跟踪${container_color}$current_dir_name${gl_bai}日志"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}11. ${gl_bai}查看${container_color}$current_dir_name${gl_bai}服务状态 ${gl_bufan}12. ${gl_bai}查看${container_color}$current_dir_name${gl_bai}镜像详情"
echo -e "${gl_bufan}13. ${gl_bai}查看${container_color}$current_dir_name${gl_bai}资源占用 ${gl_bufan}14. ${gl_bai}拉取${container_color}$current_dir_name${gl_bai}镜像(不启动)"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}23. ${gl_bai}开放${container_color}$current_dir_name${gl_bai}访问端口 ${gl_bufan}24. ${gl_bai}重新构建${container_color}$current_dir_name${gl_bai}"
echo -e "${gl_bufan}25. ${gl_bai}进入${container_color}$MAIN_SERVICE${gl_bai}服务终端 ${gl_bufan}26. ${gl_bai}修改${container_color}$MAIN_SERVICE${gl_bai}重启策略"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_huang}88. ${gl_huang}停止并清理${container_color}$current_dir_name${gl_bai} ${gl_hong}99. ${gl_hong}彻底清理${container_color}$current_dir_name${gl_bai}"
echo -e "${gl_huang}0. ${gl_bai}返回${container_color}$MAIN_SERVICE${gl_bai}上一级 ${gl_hong}00. ${gl_bai}退出脚本"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}请输入你的选择: ")" cmd_choice
case $cmd_choice in
1)
echo -e ""
echo -e "${gl_bai}正在停止并删除 ${gl_huang}$current_dir_name${gl_bai} 服务 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
docker-compose down
echo -e "\n"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
2)
echo -e ""
echo -e "正在启动 ${gl_huang}$current_dir_name${gl_bai} 服务 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
docker-compose up -d --remove-orphans
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
3)
echo -e ""
echo -e "${gl_bai}正在重启 ${gl_huang}$current_dir_name${gl_bai} 服务 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
docker-compose down && docker-compose up -d --remove-orphans
echo -e "\n"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
4)
echo -e ""
echo -e "${gl_bai}正在更新 ${gl_huang}$current_dir_name${gl_bai} 容器 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
docker-compose pull && docker-compose up -d --remove-orphans && docker image prune -f
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
5)
echo -e ""
echo -e "${gl_bai}正在显示 ${gl_huang}$current_dir_name${gl_bai} 配置文件内容 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if [ -f "docker-compose.yml" ]; then
cat docker-compose.yml | sed '/^$/d'
else
echo -e "${gl_hong}[错误]: docker-compose.yml 文件不存在${gl_bai}"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
6)
echo -e ""
echo -e "${gl_bai}正在打开 ${gl_huang}$current_dir_name${gl_bai} 配置文件编辑器 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if [ -f "docker-compose.yml" ]; then
nano docker-compose.yml
else
echo -e "${gl_hong}[错误]: docker-compose.yml 文件不存在${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}docker-compose.yml 不存在,要创建吗? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" create_choice
if [[ "$create_choice" =~ ^[Yy]$ ]]; then
echo -e "${gl_lv}正在创建 docker-compose.yml ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
create_file docker-compose.yml
else
echo -e "${gl_huang}已取消创建${gl_bai}"
fi
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
7)
echo
echo -e "${gl_bai}正在创建${gl_huang}$current_dir_name${gl_bai}配置文件"
create_file docker-compose.yml
;;
8)
echo -e ""
echo -e "${gl_bai}正在查看 ${gl_huang}$current_dir_name${gl_bai} 服务最终生效配置 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
docker-compose config
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
9)
echo -e ""
echo -e "${gl_bai}正在显示 ${gl_huang}$current_dir_name${gl_bai} 服务日志 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
docker-compose logs
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
10)
view_compose_logs
;;
11)
echo -e ""
echo -e "${gl_bai}正在显示 ${gl_huang}$current_dir_name${gl_bai} 服务状态 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
docker-compose ps
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
12)
# docker-compose images
list_beautify_all
;;
13)
echo
echo -e "${gl_huang}$current_dir_name${gl_bai}服务列表 & 实时资源占用(Ctrl-C 退出)"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
bash <(curl -sL https://cmdbox.meimolihan.eu.org/sh/docker_occupy_find.sh) $current_dir_name
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
;;
14)
echo
echo -e "${gl_bai}仅拉取${gl_huang}$current_dir_name${gl_bai}镜像(不启动)"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
docker-compose pull
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
23)
echo -e ""
echo -e "${gl_zi}>>> 开放$current_dir_name访问端口 ${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
default_port=$(
awk '
match($0, /-[[:space:]]*([0-9]+):[0-9]+/) {
print substr($0, RSTART+1, RLENGTH-1) + 0;
exit
}
' docker-compose.yml 2>/dev/null
)
[[ -z $default_port ]] && default_port=""
if [[ -n "$default_port" ]]; then
echo -e "${gl_bai}默认端口: ${gl_lv}${default_port}${gl_bai} (直接回车使用此端口)"
read -r -e -p "$(echo -e "${gl_bai}请输入要放行的端口号 (${gl_huang}0${gl_bai}返回): ")" port
[[ -z "$port" ]] && port="$default_port"
else
read -r -e -p "$(echo -e "${gl_bai}请输入要放行的端口号 (${gl_huang}0${gl_bai}返回): ")" port
fi
[ "$port" == "0" ] && { cancel_return "上一级选单"; continue; }
if [[ $port =~ ^[0-9]+$ && $port -ge 1 && $port -le 65535 ]]; then
iptables -A INPUT -p tcp --dport "$port" -j ACCEPT
echo -e ""
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
log_ok "已放行 TCP 端口 $port"
else
echo -e ""
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
log_error "端口号非法,已跳过"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
24)
echo -e ""
echo -e "${gl_bai}正在重新构建 ${gl_huang}$current_dir_name${gl_bai} 并启动服务 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
docker-compose up -d --build
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
25)
local TARGET_SERVICE="$MAIN_SERVICE"
if [[ -f "docker-compose.yml" ]] || [[ -f "docker-compose.yaml" ]]; then
local selected_service=$(select_service)
[[ -n "$selected_service" ]] && TARGET_SERVICE="$selected_service"
fi
if [[ -z "$TARGET_SERVICE" ]]; then
echo -e "${gl_hong}未找到可进入的服务${gl_bai}"
break_end
continue
fi
echo -e ""
echo -e "${gl_bai}进入 ${gl_huang}$TARGET_SERVICE${gl_bai} 服务 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if ! docker inspect "$TARGET_SERVICE" &>/dev/null; then
echo -e "${gl_hong}容器 $TARGET_SERVICE 不存在${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}是否启动容器?(${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" start_choice
if [[ "$start_choice" =~ ^[Yy]$ ]]; then
docker-compose up -d "$TARGET_SERVICE"
sleep 2
else
break_end
continue
fi
fi
if ! docker inspect -f '{{.State.Running}}' "$TARGET_SERVICE" 2>/dev/null | grep -q "true"; then
echo -e "${gl_hong}容器 $TARGET_SERVICE 未运行${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}是否启动容器?(${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" start_choice
if [[ "$start_choice" =~ ^[Yy]$ ]]; then
docker-compose up -d "$TARGET_SERVICE"
sleep 2
else
break_end
continue
fi
fi
if ! docker exec -it "$TARGET_SERVICE" bash 2>/dev/null; then
echo -e "${gl_hong}bash 不可用,尝试使用 sh ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
docker exec -it "$TARGET_SERVICE" sh
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
26)
local TARGET_SERVICE="$MAIN_SERVICE"
if [[ -f "docker-compose.yml" ]] || [[ -f "docker-compose.yaml" ]]; then
local selected_service=$(select_service)
[[ -n "$selected_service" ]] && TARGET_SERVICE="$selected_service"
fi
if [[ -z "$TARGET_SERVICE" ]]; then
echo -e "${gl_hong}未找到可修改的服务${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
continue
fi
install yq
clear
echo -e ""
echo -e "${gl_zi}>>> 永久修改重启策略 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if [ ! -f "docker-compose.yml" ] && [ ! -f "docker-compose.yaml" ]; then
echo -e "${gl_hong}❌ 未找到 docker-compose.yml 文件${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
continue
fi
compose_file="docker-compose.yml"
if [ ! -f "$compose_file" ]; then
compose_file="docker-compose.yaml"
fi
echo -e "${gl_bai}配置文件: ${gl_huang}$compose_file${gl_bai}"
echo -e "${gl_bai}目标服务: ${gl_huang}$TARGET_SERVICE${gl_bai}"
echo -e ""
echo -e "${gl_huang}当前配置:${gl_bai}"
if command -v yq &>/dev/null; then
yq e ".services.$TARGET_SERVICE" "$compose_file" 2>/dev/null || {
echo -e "${gl_bai}使用grep过滤注释行:${gl_bai}"
grep -A 20 "^\s*$TARGET_SERVICE:" "$compose_file" | grep -v "^\s*#" | head -15
}
else
echo -e "${gl_bai}服务 $TARGET_SERVICE 的配置:${gl_bai}"
grep -A 20 "^\s*$TARGET_SERVICE:" "$compose_file" | grep -v "^\s*#" | head -15
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e ""
current_restart=$(grep -A 10 "^\s*$TARGET_SERVICE:" "$compose_file" | grep "^\s*restart:" | head -1 | sed 's/^\s*restart:\s*//' || echo "未设置")
echo -e "${gl_bai}当前重启策略: ${gl_lv}${current_restart:-未设置}${gl_bai}"
echo -e "${gl_huang}>>> 请选择重启策略:${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}1. ${gl_lv}no${gl_bai} - 不自动重启 (默认)"
echo -e "${gl_bufan}2. ${gl_lv}always${gl_bai} - 总是重启"
echo -e "${gl_bufan}3. ${gl_lv}on-failure${gl_bai} - 失败时重启"
echo -e "${gl_bufan}4. ${gl_lv}unless-stopped${gl_bai} - 除非手动停止,否则总是重启"
echo -e "${gl_bufan}5. ${gl_huang}自定义策略 (如: on-failure:3)${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_huang}0. ${gl_bai}返回上一级选单"
echo -e "${gl_hong}00. ${gl_bai}退出脚本"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -p "请输入选择 [0-5]: " policy_choice
case $policy_choice in
1) new_policy="no" ;;
2) new_policy="always" ;;
3) new_policy="on-failure" ;;
4) new_policy="unless-stopped" ;;
5)
read -r -e -p "$(echo -e "${gl_bai}请输入自定义策略${gl_huang}on-failure:3${gl_bai}(${gl_huang}0${gl_bai}返回): ")" new_policy
[ "$new_policy" == "0" ] && { cancel_return "上一级选单"; continue; }
;;
0) cancel_return; continue ;;
00|000|0000) exit_script; return 0 ;;
*) handle_invalid_input ;;
esac
echo -e ""
if grep -q "^\s*restart:" "$compose_file"; then
if sed -i "s/^\(\s*\)restart:.*/\1restart: $new_policy/" "$compose_file"; then
echo -e "${gl_lv}✓ 已更新重启策略${gl_bai}"
else
echo -e "${gl_hong}✗ 更新重启策略失败${gl_bai}"
fi
else
if grep -q "^\s*$TARGET_SERVICE:" "$compose_file"; then
line_num=$(grep -n "^\s*$TARGET_SERVICE:" "$compose_file" | head -1 | cut -d: -f1)
if [ -n "$line_num" ]; then
sed -i "${line_num}a\ \ \ \ restart: $new_policy" "$compose_file"
echo -e "${gl_lv}✓ 已添加重启策略${gl_bai}"
else
echo -e "${gl_hong}✗ 未找到 $TARGET_SERVICE 服务定义${gl_bai}"
fi
else
echo -e "${gl_hong}✗ 未找到 $TARGET_SERVICE 服务定义${gl_bai}"
fi
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_huang}修改后的配置:${gl_bai}"
if command -v yq &>/dev/null; then
yq e ".services.$TARGET_SERVICE" "$compose_file" 2>/dev/null || {
echo -e "${gl_bai}使用grep显示修改后的配置:${gl_bai}"
grep -A 20 "^\s*$TARGET_SERVICE:" "$compose_file" | grep -v "^\s*#" | head -20
}
else
echo -e "${gl_bai}服务 $TARGET_SERVICE 的配置:${gl_bai}"
grep -A 20 "^\s*$TARGET_SERVICE:" "$compose_file" | grep -v "^\s*#" | head -20
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if docker inspect "$TARGET_SERVICE" >/dev/null 2>&1; then
echo -e ""
echo -e "${gl_huang}检测到容器 $TARGET_SERVICE 正在运行${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}是否立即更新容器的重启策略? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" update_now
if [[ "$update_now" =~ ^[Yy]$ ]]; then
echo -e "${gl_bai}更新容器重启策略 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
if docker update --restart="$new_policy" "$TARGET_SERVICE" >/dev/null 2>&1; then
echo -e "${gl_lv}✓ 容器重启策略已更新${gl_bai}"
else
echo -e "${gl_hong}✗ 容器重启策略更新失败${gl_bai}"
fi
else
echo -e "${gl_huang}注意: 配置文件已修改,但容器重启策略未更新${gl_bai}"
echo -e "${gl_bai}如需应用到容器,请手动执行:${gl_bai}"
echo -e "${gl_lv}docker update --restart=$new_policy $TARGET_SERVICE${gl_bai}"
fi
echo -e ""
echo -e "${gl_huang}是否重新创建容器以应用配置?${gl_bai}"
echo -e "${gl_bai}重新创建容器会停止并重新启动容器,但会保留数据卷${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "输入 ${gl_bufan}y${gl_bai} 重新创建,其他键跳过:")" recreate_choice
if [ "$recreate_choice" = "y" ] || [ "$recreate_choice" = "Y" ]; then
echo -e "${gl_bai}重新创建容器 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
if command -v docker-compose &>/dev/null; then
docker-compose down && docker-compose up -d
elif command -v docker &>/dev/null && docker compose version &>/dev/null; then
docker compose down && docker compose up -d
else
echo -e "${gl_hong}未找到 docker-compose 或 docker compose 命令${gl_bai}"
fi
echo -e "${gl_lv}✓ 容器已重新创建${gl_bai}"
else
echo -e "${gl_huang}注意: 需要重新创建容器以使配置文件完全生效${gl_bai}"
echo -e "${gl_bai}手动执行: ${gl_lv}docker-compose down && docker-compose up -d${gl_bai}"
fi
else
echo -e "${gl_huang}容器 $TARGET_SERVICE 未运行${gl_bai}"
echo -e "${gl_bai}下次启动容器时会应用新的重启策略${gl_bai}"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
88)
echo -e ""
echo -e "${gl_bai}正在停止并删除 ${gl_huang}$current_dir_name${gl_bai} 服务及相关资源 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_hong}⚠️ 警告: 此操作将删除容器、网络和卷、删除项目镜像!${gl_bai}"
echo -e "${gl_bai}数据卷不会被删除,但网络和相关资源将被清理${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}确认执行? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" confirm
if [[ "$confirm" =~ ^[Yy]$ ]]; then
# ⚠️ 警告: 此操作将删除当前项目容器、网络、项目数据卷;同时全局清理所有闲置镜像和闲置数据卷!
# docker-compose down --volumes --remove-orphans && docker system prune -af --volumes
# 停止容器、网络、删除项目镜像,同时清理孤儿容器 + 全部未使用镜像、全部未使用网络、全部未使用卷、悬空构建缓存
docker compose down --rmi all --remove-orphans && docker system prune -af --volumes
echo -e ""
echo -e "${gl_lv}✓ 服务、网络和卷已清理完成${gl_bai}"
else
echo -e "${gl_huang}已取消操作${gl_bai}"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
99)
echo -e ""
echo -e "${gl_bai}正在停止并删除 ${gl_huang}$current_dir_name${gl_bai} 服务及相关资源 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_hong}⚠️ 警告: 此操作将删除容器、网络、卷、镜像,并且【删除整个项目目录】!${gl_bai}"
echo -e "${gl_hong}项目目录路径:${gl_huang}${current_dir}${gl_bai}"
echo -e "${gl_bai}目录内所有文件都会被永久删除!${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_hong}确认要执行吗? ${gl_bai}(${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" confirm
if [[ "$confirm" =~ ^[Yy]$ ]]; then
docker compose down --rmi all --remove-orphans && docker system prune -af --volumes
echo -e "${gl_lv}✓ Compose容器、网络、卷已清理${gl_bai}"
local parent_dir=$(dirname "${current_dir}")
cd "${parent_dir}" || {
log_error "无法切换到上级目录 ${parent_dir}"
read -r
break_end
}
echo -e "${gl_huang}正在删除项目目录:${current_dir}${gl_bai}"
rm -rf "${current_dir}"
echo -e "${gl_lv}✓ 项目目录已彻底删除${gl_bai}"
return 0
else
echo -e "${gl_huang}已取消操作${gl_bai}"
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
0)
proj_mgmt_tool
;;
00 | 000 | 0000)
exit_script
;;
*) handle_invalid_input ;;
esac
done
}
show_help() {
echo -e "${gl_lv}使用说明:${gl_bai}"
echo -e " ${gl_bai}$0 ${gl_lan}[项目目录]${gl_bai}"
echo -e ""
echo -e "${gl_lv}参数:${gl_bai}"
echo -e " ${gl_lan}[项目目录]${gl_bai} Docker Compose 项目目录路径"
echo -e ""
echo -e "${gl_lv}示例:${gl_bai}"
echo -e " ${gl_bai}$0 ${gl_lan}/compose/myapp${gl_bai} # 管理指定目录的 Compose 项目"
echo -e " ${gl_bai}$0${gl_bai} # 管理当前目录的 Compose 项目"
echo -e " ${gl_bai}$0 ${gl_lan}-h${gl_bai} # 显示帮助信息"
echo -e ""
return 0
}
check_docker() {
if ! docker info &>/dev/null; then
log_error "Docker 服务未运行"
return 1
fi
if ! command -v docker-compose &>/dev/null && ! docker compose version &>/dev/null; then
log_error "未找到 docker-compose 命令"
echo -e "${gl_bai}请安装 docker-compose 或使用 docker compose${gl_bai}"
return 1
fi
}
main() {
local WORK_DIR="."
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
show_help
return 0
;;
*)
WORK_DIR="$1"
shift
;;
esac
done
check_docker || return 1
if [[ ! -d "$WORK_DIR" ]]; then
log_error "目录不存在: $WORK_DIR"
return 1
fi
if [[ ! -f "$WORK_DIR/docker-compose.yml" ]] && [[ ! -f "$WORK_DIR/docker-compose.yaml" ]]; then
echo -e "${gl_huang}警告: 在 $WORK_DIR 中未找到 docker-compose.yml 文件${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}是否创建新的 docker-compose.yml 文件?(${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" create_choice
if [[ "$create_choice" =~ ^[Yy]$ ]]; then
if ! cd "$WORK_DIR" 2>/dev/null; then
log_error "无法进入目录: $WORK_DIR"
return 1
fi
create_file docker-compose.yml
else
echo -e "${gl_huang}已取消${gl_bai}"
return 0
fi
fi
show_compose_commands_menu "$WORK_DIR"
return 0
}
main "$@"
return $?
}
manage_fan_video() {
SERVICE="fan-video"
INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/meimolihan/fan-video/main/scripts/install.sh"
UNINSTALL_SCRIPT_URL="https://raw.githubusercontent.com/meimolihan/fan-video/main/scripts/uninstall.sh"
BACKUP_SCRIPT_URL="https://raw.githubusercontent.com/meimolihan/fan-video/main/scripts/fan-video_backup.sh"
RECOVER_SCRIPT_URL="https://raw.githubusercontent.com/meimolihan/fan-video/main/scripts/fan-video_recover.sh"
while true; do
clear
echo -e ""
echo -e "${gl_zi}>>> fan-video 管理工具${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
show_service_status fan-video
show_service_url fan-video
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}1. ${gl_bai}停止 fan-video ${gl_bufan}2. ${gl_bai}启动 fan-video"
echo -e "${gl_bufan}3. ${gl_bai}重启 fan-video ${gl_bufan}4. ${gl_bai}查看服务状态"
echo -e "${gl_bufan}5. ${gl_bai}查看开机自启状态 ${gl_bufan}6. ${gl_bai}开启开机自启"
echo -e "${gl_bufan}7. ${gl_bai}禁用开机自启 ${gl_bufan}8. ${gl_bai}查看日志(100行)"
echo -e "${gl_bufan}9. ${gl_bai}实时跟踪日志"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_lv}66. ${gl_bai}安装/升级 fan-video ${gl_huang}77. ${gl_bai}备份数据"
echo -e "${gl_lv}88. ${gl_bai}恢复数据 ${gl_hong}99. ${gl_bai}卸载 fan-video"
echo -e "${gl_huang}0. ${gl_bai}返回上一级选单 ${gl_hong}00. ${gl_bai}退出脚本"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}请输入你的选择: ")" action
case "$action" in
1)
echo -e ""
echo -e "${gl_zi}>>> 正在停止 fan-video 服务 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl stop ${SERVICE}
log_ok "fan-video 服务已停止"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
2)
echo -e ""
echo -e "${gl_zi}>>> 正在启动 fan-video 服务 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl start ${SERVICE}
log_ok "fan-video 服务已启动"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
3)
echo -e ""
echo -e "${gl_zi}>>> 正在重启 fan-video 服务 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl restart ${SERVICE}
log_ok "fan-video 服务已重启"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
4)
echo -e ""
echo -e "${gl_zi}>>> fan-video 服务状态 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl status ${SERVICE}
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
5)
echo -e ""
echo -e "${gl_zi}>>> fan-video 开机自启状态 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local status=$(sudo systemctl is-enabled ${SERVICE} 2>/dev/null)
case "$status" in
enabled) echo -e "${gl_lv}已启用${gl_bai}" ;;
disabled) echo -e "${gl_hong}已禁用${gl_bai}" ;;
static) echo "静态(非服务单元)" ;;
indirect) echo "间接(依赖其他单元)" ;;
*) echo "$status" ;;
esac
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
6)
echo -e ""
echo -e "${gl_zi}>>> 正在开启 fan-video 开机自启 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl enable ${SERVICE}
log_ok "已开启 fan-video 开机自启"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
7)
echo -e ""
echo -e "${gl_zi}>>> 正在禁用 fan-video 开机自启 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl disable ${SERVICE}
log_ok "已禁用 fan-video 开机自启"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
8)
echo -e ""
echo -e "${gl_zi}>>> fan-video 日志(最近100行)${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo journalctl -u ${SERVICE} -n 100
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
9)
echo -e ""
echo -e "${gl_zi}>>> 实时跟踪 fan-video 日志(按 Ctrl+C 退出)${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo journalctl -u ${SERVICE} -f
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
66)
bash -c "$(curl -sSL ${INSTALL_SCRIPT_URL})"
break_end
continue
;;
77)
bash <(curl -sL ${BACKUP_SCRIPT_URL}) "/vol2/1000/file/backup/fan-video-backup" 6
break_end
continue
;;
88)
bash <(curl -sL ${RECOVER_SCRIPT_URL}) "/vol2/1000/file/backup/fan-video-backup"
break_end
continue
;;
99)
bash <(curl -sSL ${UNINSTALL_SCRIPT_URL})
break_end
continue
;;
0)
proj_mgmt_tool
;;
00 | 000 | 0000)
exit_script
;;
*)
handle_invalid_input
;;
esac
done
}
manage_opencode() {
while true; do
clear
echo -e ""
echo -e "${gl_zi}>>> OpenCode 管理工具${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
show_service_status opencode
show_service_url opencode
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}1. ${gl_bai}停止 OpenCode ${gl_bufan}2. ${gl_bai}启动 OpenCode"
echo -e "${gl_bufan}3. ${gl_bai}重启 OpenCode ${gl_bufan}4. ${gl_bai}查看服务状态"
echo -e "${gl_bufan}5. ${gl_bai}查看开机自启状态 ${gl_bufan}6. ${gl_bai}禁用开机自启"
echo -e "${gl_bufan}7. ${gl_bai}查看日志 ${gl_bufan}8. ${gl_bai}实时跟踪日志"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_lv}66. ${gl_bai}安装 OpenCode ${gl_hong}99. ${gl_bai}卸载 OpenCode"
echo -e "${gl_huang}0. ${gl_bai}返回上一级选单 ${gl_hong}00. ${gl_bai}退出脚本"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}请输入你的选择: ")" action
case "$action" in
1)
echo -e ""
echo -e "${gl_zi}>>> 正在停止 OpenCode 服务 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl stop opencode
log_ok "OpenCode 服务已停止"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
2)
echo -e ""
echo -e "${gl_zi}>>> 正在启动 OpenCode 服务 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl start opencode
log_ok "OpenCode 服务已启动"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
3)
echo -e ""
echo -e "${gl_zi}>>> 正在重启 OpenCode 服务 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl restart opencode
log_ok "OpenCode 服务已重启"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
4)
echo -e ""
echo -e "${gl_zi}>>> OpenCode 服务状态 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl status opencode
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
5)
echo -e ""
echo -e "${gl_zi}>>> OpenCode 开机自启状态 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local status=$(sudo systemctl is-enabled opencode 2>/dev/null)
case "$status" in
enabled) echo -e "已启用" ;;
disabled) echo -e "已禁用" ;;
static) echo "静态(非服务单元)" ;;
indirect) echo "间接(依赖其他单元)" ;;
*) echo "$status" ;;
esac
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
6)
echo -e ""
echo -e "${gl_zi}>>> 正在禁用 OpenCode 开机自启 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl disable opencode
log_ok "已禁用 OpenCode 开机自启"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
7)
echo -e ""
echo -e "${gl_zi}>>> OpenCode 日志(最近100行)${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo journalctl -u opencode -n 100
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
8)
echo -e ""
echo -e "${gl_zi}>>> 实时跟踪 OpenCode 日志(按 Ctrl+C 退出)${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo journalctl -u opencode -f
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
66)
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/lx_install_opencode.sh)
;;
99)
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/lx_uninstall_opencode.sh)
;;
0)
proj_mgmt_tool
;;
00 | 000 | 0000)
exit_script
;;
*)
handle_invalid_input
;;
esac
done
}
manage_2panel() {
SERVICE="2panel"
INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/meimolihan/2Panel/main/scripts/install.sh"
UNINSTALL_SCRIPT_URL="https://raw.githubusercontent.com/meimolihan/2Panel/main/scripts/uninstall.sh"
BACKUP_SCRIPT_URL="gitee.com/meimolihan/cmdbox/raw/master/sh/2panel_backup.sh"
RECOVER_SCRIPT_URL="gitee.com/meimolihan/cmdbox/raw/master/sh/2panel_recover.sh"
while true; do
clear
echo -e ""
echo -e "${gl_zi}>>> 2Panel 管理工具${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
show_service_status 2panel
show_service_url 2panel
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_bufan}1. ${gl_bai}停止 2Panel ${gl_bufan}2. ${gl_bai}启动 2Panel"
echo -e "${gl_bufan}3. ${gl_bai}重启 2Panel ${gl_bufan}4. ${gl_bai}查看服务状态"
echo -e "${gl_bufan}5. ${gl_bai}查看开机自启状态 ${gl_bufan}6. ${gl_bai}开启开机自启"
echo -e "${gl_bufan}7. ${gl_bai}禁用开机自启 ${gl_bufan}8. ${gl_bai}查看日志(100行)"
echo -e "${gl_bufan}9. ${gl_bai}实时跟踪日志"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_lv}66. ${gl_bai}安装/升级 2Panel ${gl_huang}77. ${gl_bai}备份数据"
echo -e "${gl_lv}88. ${gl_bai}恢复数据 ${gl_hong}99. ${gl_bai}卸载 2Panel"
echo -e "${gl_huang}0. ${gl_bai}返回上一级选单 ${gl_hong}00. ${gl_bai}退出脚本"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}请输入你的选择: ")" action
case "$action" in
1)
echo -e ""
echo -e "${gl_zi}>>> 正在停止 2Panel 服务 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl stop ${SERVICE}
log_ok "2Panel 服务已停止"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
2)
echo -e ""
echo -e "${gl_zi}>>> 正在启动 2Panel 服务 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl start ${SERVICE}
log_ok "2Panel 服务已启动"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
3)
echo -e ""
echo -e "${gl_zi}>>> 正在重启 2Panel 服务 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl restart ${SERVICE}
log_ok "2Panel 服务已重启"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
4)
echo -e ""
echo -e "${gl_zi}>>> 2Panel 服务状态 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl status ${SERVICE}
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
5)
echo -e ""
echo -e "${gl_zi}>>> 2Panel 开机自启状态 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local status=$(sudo systemctl is-enabled ${SERVICE} 2>/dev/null)
case "$status" in
enabled) echo -e "${gl_lv}已启用${gl_bai}" ;;
disabled) echo -e "${gl_hong}已禁用${gl_bai}" ;;
static) echo "静态(非服务单元)" ;;
indirect) echo "间接(依赖其他单元)" ;;
*) echo "$status" ;;
esac
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
6)
echo -e ""
echo -e "${gl_zi}>>> 正在开启 2Panel 开机自启 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl enable ${SERVICE}
log_ok "已开启 2Panel 开机自启"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
7)
echo -e ""
echo -e "${gl_zi}>>> 正在禁用 2Panel 开机自启 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo systemctl disable ${SERVICE}
log_ok "已禁用 2Panel 开机自启"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
8)
echo -e ""
echo -e "${gl_zi}>>> 2Panel 日志(最近100行)${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo journalctl -u ${SERVICE} -n 100
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
9)
echo -e ""
echo -e "${gl_zi}>>> 实时跟踪 2Panel 日志(按 Ctrl+C 退出)${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
sudo journalctl -u ${SERVICE} -f
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
break_end
;;
66)
bash -c "$(curl -sSL ${INSTALL_SCRIPT_URL})"
;;
77)
bash <(curl -sL ${BACKUP_SCRIPT_URL}) "/vol2/1000/file/backup/2panel-backup" 6
break_end
;;
88)
bash <(curl -sL ${RECOVER_SCRIPT_URL}) /vol2/1000/file/backup/2panel-backup
break_end
;;
99)
bash <(curl -sSL ${UNINSTALL_SCRIPT_URL})
;;
0)
proj_mgmt_tool
;;
00 | 000 | 0000)
exit_script
;;
*)
handle_invalid_input
;;
esac
done
}
show_help() {
echo -e "${gl_lv}使用说明:${gl_bai}"
echo -e " ${gl_bai}$0 ${gl_lan}[项目目录]${gl_bai}"
echo -e ""
echo -e "${gl_lv}参数:${gl_bai}"
echo -e " ${gl_lan}[项目目录]${gl_bai} Docker Compose 项目目录路径"
echo -e ""
echo -e "${gl_lv}示例:${gl_bai}"
echo -e " ${gl_bai}$0 ${gl_lan}/compose/myapp${gl_bai} # 管理指定目录的 Compose 项目"
echo -e " ${gl_bai}$0${gl_bai} # 管理当前目录的 Compose 项目"
echo -e " ${gl_bai}$0 ${gl_lan}-h${gl_bai} # 显示帮助信息"
echo -e ""
exit 0
}
check_docker() {
if ! docker info &>/dev/null; then
log_error "Docker 服务未运行"
exit 1
fi
if ! command -v docker-compose &>/dev/null && ! docker compose version &>/dev/null; then
log_error "未找到 docker-compose 命令"
echo -e "${gl_bai}请安装 docker-compose 或使用 docker compose${gl_bai}"
exit 1
fi
}
main() {
local WORK_DIR="."
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
show_help
;;
*)
WORK_DIR="$1"
shift
;;
esac
done
check_docker
if [[ ! -d "$WORK_DIR" ]]; then
log_error "目录不存在: $WORK_DIR"
exit 1
fi
if [[ ! -f "$WORK_DIR/docker-compose.yml" ]] && [[ ! -f "$WORK_DIR/docker-compose.yaml" ]]; then
echo -e "${gl_huang}警告: 在 $WORK_DIR 中未找到 docker-compose.yml 文件${gl_bai}"
read -r -e -p "$(echo -e "${gl_bai}是否创建新的 docker-compose.yml 文件?(${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" create_choice
if [[ "$create_choice" =~ ^[Yy]$ ]]; then
if ! cd "$WORK_DIR" 2>/dev/null; then
log_error "无法进入目录: $WORK_DIR"
exit 1
fi
create_file docker-compose.yml
else
echo -e "${gl_huang}已取消${gl_bai}"
exit 0
fi
fi
docker_compose_manager "$WORK_DIR"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
创建本地脚本
new_script="new_test.sh"
cat > "$new_script" << 'EOF'
#!/bin/bash
# 粘贴脚本源码
EOF
# 保留本地脚本,去掉 rm -f "$new_script"
chmod +x "$new_script" && ./"$new_script" && rm -f "$new_script"