Files
3m-ui/scripts/3m-ui
T
風雪 68aafdea86 feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
feat(scripts/3m-ui): add channel stable|pre subcommand
3m-ui channel [stable|pre] [--update] — show/set update channel and optional upgrade.
2026-09-14 23:01:35 +08:00

741 lines
25 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
# 3m-ui unified management entrypoint (/usr/local/bin/3m-ui).
# Application binary: /usr/local/lib/3m-ui/3m-ui-bin
# Original work for 3m-ui (EPL-2.0). Ideas inspired by common panel ops menus;
# not a derivative of any third-party management script source.
set -eu
ROOT="${THREE_M_UI_ROOT:-}"
BASE="$ROOT/usr/local/lib/3m-ui"
MANAGER="$BASE/3m-ui.sh"
INSTALLER="$BASE/install.sh"
UPDATER="$BASE/update.sh"
UNINSTALLER="$BASE/uninstall.sh"
APP_BIN="$BASE/3m-ui-bin"
CONFIG_FILE="$ROOT/etc/3m-ui/config.yaml"
DATA_DIR="${THREE_M_UI_DATA_DIR:-}"
if [ -z "$DATA_DIR" ] && [ -s "$BASE/DATA_DIRECTORY" ]; then
DATA_DIR="$(cat "$BASE/DATA_DIRECTORY")"
fi
DATA_DIR="${DATA_DIR:-$ROOT/var/lib/3m-ui}"
# --- helpers -----------------------------------------------------------------
say() { printf '%s\n' "$*"; }
err() { printf '%s\n' "$*" >&2; }
need_root() {
[ "$(id -u)" -eq 0 ] || { err '请使用 root 运行 3m-ui 管理命令。 / Run as root.'; exit 1; }
}
confirm() {
# usage: confirm "prompt" [default y|n]
prompt="$1"
def="${2:-n}"
printf '%s [%s]: ' "$prompt" "$def"
IFS= read -r ans || return 1
[ -z "$ans" ] && ans="$def"
case "$ans" in y|Y|yes|YES) return 0 ;; *) return 1 ;; esac
}
pause() {
printf '\n%s' '按回车返回菜单 / Press Enter to return... '
IFS= read -r _ || true
}
run_script() {
script="$1"
shift
[ -x "$script" ] || {
err "管理脚本不存在: $script"
return 1
}
"$script" "$@"
}
run_app() {
cmd="$1"
if [ ! -x "$APP_BIN" ]; then
err "未找到程序: $APP_BIN"
return 1
fi
mkdir -p "$DATA_DIR"
(cd "$DATA_DIR" && THREE_M_UI_CONFIG="$CONFIG_FILE" THREE_M_UI_DATA_DIR="$DATA_DIR" "$APP_BIN" "$cmd")
}
init_system() {
if [ -d "$ROOT/run/systemd/system" ] && command -v systemctl >/dev/null 2>&1; then
printf '%s' systemd
elif command -v rc-service >/dev/null 2>&1; then
printf '%s' openrc
else
printf '%s' unsupported
fi
}
service_action() {
action="$1"
case "$(init_system)" in
systemd) systemctl "$action" 3m-ui ;;
openrc) rc-service 3m-ui "$action" ;;
*) err '未检测到 systemd 或 OpenRC。'; return 1 ;;
esac
}
service_is_active() {
case "$(init_system)" in
systemd) systemctl is-active --quiet 3m-ui ;;
openrc) rc-service 3m-ui status >/dev/null 2>&1 ;;
*) return 1 ;;
esac
}
service_enable() {
case "$(init_system)" in
systemd)
if systemctl enable 3m-ui; then
say '已设置开机自启 / Enabled on boot.'
else
err 'enable 失败(服务单元是否已安装?)'
return 1
fi
;;
openrc)
rc-update add 3m-ui default && say '已设置开机自启 / Enabled on boot.'
;;
*) err '不支持的 init'; return 1 ;;
esac
}
service_disable() {
case "$(init_system)" in
systemd)
if systemctl disable 3m-ui; then
say '已取消开机自启 / Disabled on boot.'
else
err 'disable 失败'
return 1
fi
;;
openrc)
rc-update del 3m-ui default && say '已取消开机自启 / Disabled on boot.'
;;
*) err '不支持的 init'; return 1 ;;
esac
}
yaml_get() {
# yaml_get KEY_LINE_PREFIX e.g. " port:"
key="$1"
if [ ! -f "$CONFIG_FILE" ]; then
return 1
fi
awk -v k="$key" 'index($0, k) == 1 { sub(/^[^:]+:[[:space:]]*/, ""); gsub(/["\047]/, ""); print; exit }' "$CONFIG_FILE"
}
yaml_set() {
# yaml_set " port:" "8080" — sets or inserts under server:
key="$1"
val="$2"
if [ ! -f "$CONFIG_FILE" ]; then
err "配置不存在: $CONFIG_FILE"
return 1
fi
tmp="${CONFIG_FILE}.tmp.$$"
if grep -q "^${key}" "$CONFIG_FILE" 2>/dev/null; then
# Escape for sed
esc=$(printf '%s' "$val" | sed -e 's/[\\|&]/\\&/g')
sed "s|^${key}.*|${key} ${esc}|" "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE"
else
awk -v k="$key" -v v="$val" '
BEGIN { done=0 }
{ print }
/^server:/ && !done { print k " " v; done=1 }
' "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE"
fi
}
# --- service ops -------------------------------------------------------------
status() {
case "$(init_system)" in
systemd) systemctl --no-pager --full status 3m-ui || true ;;
openrc) rc-service 3m-ui status || true ;;
*) say 'init: unsupported' ;;
esac
printf '\n'
if service_is_active; then
say '状态: 运行中 / running'
else
say '状态: 未运行 / stopped'
fi
case "$(init_system)" in
systemd)
if systemctl is-enabled --quiet 3m-ui 2>/dev/null; then
say '开机自启: 是 / enabled'
else
say '开机自启: 否 / disabled'
fi
;;
esac
if [ -x "$APP_BIN" ]; then
say "应用程序: $APP_BIN"
"$APP_BIN" --version 2>/dev/null || true
else
say '应用程序二进制不存在。'
fi
}
logs() {
case "$(init_system)" in
systemd)
if [ "${1:-}" = "-f" ] || [ "${1:-}" = "follow" ]; then
# Ctrl+C must not abort the interactive menu under set -e
journalctl -u 3m-ui -f --no-pager || true
else
journalctl -u 3m-ui -n 100 --no-pager || true
fi
;;
openrc)
if [ -f /var/log/3m-ui/3m-ui.log ]; then
if [ "${1:-}" = "-f" ] || [ "${1:-}" = "follow" ]; then
tail -n 50 -f /var/log/3m-ui/3m-ui.log || true
else
tail -n 100 /var/log/3m-ui/3m-ui.log || true
fi
else
say '未找到日志文件 /var/log/3m-ui/3m-ui.log'
fi
;;
*) say '未找到可用的日志系统。' ;;
esac
}
version() {
printed=0
if [ -x "$APP_BIN" ]; then
out="$("$APP_BIN" --version 2>/dev/null || true)"
[ -z "$out" ] && out="$("$APP_BIN" version 2>/dev/null || true)"
if [ -n "$out" ]; then
printf '%s\n' "$out"
printed=1
fi
fi
for f in VERSION CHANNEL MIHOMO_VERSION REPOSITORY; do
if [ -s "$BASE/$f" ]; then
case "$f" in
VERSION) printf 'Installed release: %s\n' "$(cat "$BASE/$f")" ;;
CHANNEL) printf 'Update channel: %s\n' "$(cat "$BASE/$f")" ;;
MIHOMO_VERSION) printf 'Bundled Mihomo: %s\n' "$(cat "$BASE/$f")" ;;
REPOSITORY) printf 'Release source: %s\n' "$(cat "$BASE/$f")" ;;
esac
printed=1
fi
done
if [ -x "$APP_BIN" ]; then
printf 'Binary: %s\n' "$APP_BIN"
printed=1
fi
if [ "$printed" -eq 0 ]; then
err '3m-ui 未安装或无法读取版本信息。'
return 1
fi
}
# --- config / access ---------------------------------------------------------
show_access() {
port=$(yaml_get ' port:' 2>/dev/null || true)
port="${port:-8080}"
listen=$(yaml_get ' listen:' 2>/dev/null || true)
pub=$(yaml_get ' public_url:' 2>/dev/null || true)
web=$(yaml_get ' web_path:' 2>/dev/null || true)
subp=$(yaml_get ' sub_path:' 2>/dev/null || true)
subport=$(yaml_get ' sub_port:' 2>/dev/null || true)
say "配置文件: $CONFIG_FILE"
say "面板端口 / port: $port"
[ -n "$listen" ] && say "监听 / listen: $listen"
[ -n "$pub" ] && [ "$pub" != "null" ] && say "public_url: $pub"
[ -n "$web" ] && say "web_path: $web"
[ -n "$subp" ] && say "sub_path: $subp"
[ -n "$subport" ] && [ "$subport" != "0" ] && say "sub_port: $subport"
path_suffix=""
case "$web" in
/*) path_suffix="$web" ;;
"") path_suffix="" ;;
*) path_suffix="/$web" ;;
esac
if [ -n "$pub" ] && [ "$pub" != "null" ]; then
# strip trailing slash for display
base=$(printf '%s' "$pub" | sed 's|/*$||')
say "面板访问 / Panel URI: ${base}${path_suffix}/"
else
say "面板访问示例 (请换成公网 IP/域名):"
say " http://<IP>:${port}${path_suffix}/"
say " https://<域名>/ (若已启用面板 SSL 且监听 443)"
fi
say '面板启用 HTTPS 后,请使用面板中配置的域名和 HTTPS 端口。'
if run_app healthcheck >/dev/null 2>&1; then
say '面板健康检查通过 / healthcheck OK'
else
say '面板健康检查未通过或未就绪 (可稍后重试)'
fi
say '管理命令: 3m-ui'
say '初始密码在首次安装时随机生成;遗失可用菜单「重置管理员密码」。'
}
change_port() {
if [ ! -f "$CONFIG_FILE" ]; then
err "配置文件不存在: $CONFIG_FILE"
return 1
fi
current=$(yaml_get ' port:' 2>/dev/null || echo "8080")
printf '当前面板端口: %s\n' "$current"
printf '输入新端口 (1-65535, 回车取消): '
IFS= read -r new_port || return 0
[ -z "$new_port" ] && return 0
case "$new_port" in
''|*[!0-9]*) err '无效端口。'; return 1 ;;
esac
[ "$new_port" -ge 1 ] && [ "$new_port" -le 65535 ] || {
err '端口必须在 1-65535 范围内。'
return 1
}
[ "$new_port" = "$current" ] && { say '端口未变更。'; return 0; }
yaml_set ' port:' "$new_port"
say "端口已更改: $current → $new_port"
say '正在重启 3m-ui...'
service_action restart
sleep 1
if service_is_active; then
say "✓ 已重启。配置端口: $new_port"
else
err '✗ 启动失败,请检查: 3m-ui logs'
fi
}
set_public_url() {
if [ ! -f "$CONFIG_FILE" ]; then
err "配置不存在: $CONFIG_FILE"
return 1
fi
cur=$(yaml_get ' public_url:' 2>/dev/null || true)
printf '当前 public_url: %s\n' "${cur:-<empty>}"
printf '输入新的 public_url (例如 https://example.com ,回车取消): '
IFS= read -r url || return 0
[ -z "$url" ] && return 0
yaml_set ' public_url:' "\"$url\""
say '已写入 public_url。'
if confirm '是否立即重启面板?' y; then
service_action restart
sleep 1
service_is_active && say '✓ 已重启' || err '✗ 重启后未运行'
fi
}
set_panel_paths() {
if [ ! -f "$CONFIG_FILE" ]; then
err "配置不存在: $CONFIG_FILE"
return 1
fi
say '留空则保持现有值。 / Leave blank to keep current.'
cur_web=$(yaml_get ' web_path:' 2>/dev/null || true)
cur_sub=$(yaml_get ' sub_path:' 2>/dev/null || true)
cur_sp=$(yaml_get ' sub_port:' 2>/dev/null || true)
cur_listen=$(yaml_get ' listen:' 2>/dev/null || true)
printf 'web_path (面板路径前缀, 现: %s): ' "${cur_web:-<empty>}"
IFS= read -r web || true
printf 'sub_path (订阅路径前缀, 现: %s): ' "${cur_sub:-<empty>}"
IFS= read -r sub || true
printf 'sub_port (0=与面板同端口, 现: %s): ' "${cur_sp:-0}"
IFS= read -r sp || true
printf 'listen (空=全部接口, 现: %s): ' "${cur_listen:-<empty>}"
IFS= read -r lis || true
changed=0
if [ -n "${web:-}" ]; then
yaml_set ' web_path:' "\"$web\""
changed=1
fi
if [ -n "${sub:-}" ]; then
yaml_set ' sub_path:' "\"$sub\""
changed=1
fi
if [ -n "${sp:-}" ]; then
case "$sp" in
''|*[!0-9]*) err 'sub_port 无效' ;;
*) yaml_set ' sub_port:' "$sp"; changed=1 ;;
esac
fi
if [ -n "${lis:-}" ]; then
yaml_set ' listen:' "\"$lis\""
changed=1
fi
if [ "$changed" -eq 0 ]; then
say '未修改。'
return 0
fi
say '已写入配置。'
if confirm '是否立即重启面板?' y; then
service_action restart
sleep 1
service_is_active && say '✓ 已重启' || err '✗ 重启后未运行'
fi
}
reset_admin() {
if [ ! -x "$APP_BIN" ]; then
err "未找到程序: $APP_BIN"
return 1
fi
say '将随机生成新的管理员密码,并要求下次登录修改密码。'
if ! confirm '确认继续?' n; then
say '已取消。'
return 0
fi
run_app reset-admin || return 1
}
# --- BBR ---------------------------------------------------------------------
bbr_status() {
if [ "$(uname -s)" != Linux ]; then
err '仅支持 Linux。'
return 1
fi
sysctl net.ipv4.tcp_congestion_control 2>/dev/null || true
sysctl net.core.default_qdisc 2>/dev/null || true
}
enable_bbr() {
if [ "$(uname -s)" != Linux ]; then
err '仅支持 Linux。'
return 1
fi
if sysctl net.ipv4.tcp_congestion_control 2>/dev/null | grep -q bbr; then
say 'BBR 已启用。'
return 0
fi
modprobe tcp_bbr 2>/dev/null || true
if [ ! -f /etc/sysctl.d/99-3m-ui-bbr.conf ]; then
printf '%s\n' 'net.core.default_qdisc=fq' > /etc/sysctl.d/99-3m-ui-bbr.conf
printf '%s\n' 'net.ipv4.tcp_congestion_control=bbr' >> /etc/sysctl.d/99-3m-ui-bbr.conf
fi
sysctl --system >/dev/null 2>&1 || sysctl -p /etc/sysctl.d/99-3m-ui-bbr.conf 2>/dev/null || true
bbr_status
say '已尝试启用 BBR。'
}
disable_bbr() {
if [ "$(uname -s)" != Linux ]; then
err '仅支持 Linux。'
return 1
fi
rm -f /etc/sysctl.d/99-3m-ui-bbr.conf
sysctl -w net.ipv4.tcp_congestion_control=cubic >/dev/null 2>&1 || true
sysctl -w net.core.default_qdisc=fq_codel >/dev/null 2>&1 || true
bbr_status
say '已尝试恢复默认拥塞控制 (cubic)。重启后仍可能被其它 sysctl 覆盖。'
}
bbr_menu() {
say ''
say ' 1) 状态 / Status'
say ' 2) 启用 BBR / Enable'
say ' 3) 关闭 BBR / Disable'
say ' 0) 返回'
printf '选择: '
IFS= read -r c || return 0
case "$c" in
1) bbr_status ;;
2) enable_bbr ;;
3) disable_bbr ;;
*) ;;
esac
}
# --- Geo / firewall / SSL helpers --------------------------------------------
update_geo() {
# Geo databases influence routing. Do not download a moving "latest" asset
# from a root-owned management script without a release checksum.
say 'Geo 数据库请在面板「系统设置 → 网络与 Geo」中更新。'
say 'Use Settings → Network & Geo in the authenticated panel to update Geo files.'
say '该入口会显示每个文件的更新结果;管理脚本不会下载未校验的 latest 资产。'
}
open_firewall() {
say '尝试放行常用端口 (22, 80, 443, 面板端口)...'
port=$(yaml_get ' port:' 2>/dev/null || echo "8080")
if command -v ufw >/dev/null 2>&1; then
ufw allow 22/tcp || true
ufw allow 80/tcp || true
ufw allow 443/tcp || true
ufw allow "${port}/tcp" || true
say '已向 ufw 提交规则 (请自行 ufw enable / status)。'
elif command -v firewall-cmd >/dev/null 2>&1; then
firewall-cmd --permanent --add-port=80/tcp || true
firewall-cmd --permanent --add-port=443/tcp || true
firewall-cmd --permanent --add-port="${port}/tcp" || true
firewall-cmd --reload || true
say '已尝试 firewalld 放行。'
else
say '未检测到 ufw/firewalld。请在云安全组放行 80/443 与面板端口。'
fi
}
ssl_cert_menu() {
say ''
say '面板 HTTPS 推荐在「系统设置 → SSL」使用内置 ACME。'
say '以下为可选:用外部工具申请 PEM,供节点或反代使用。'
say ''
say ' 1) 打印 certbot 命令 (standalone)'
say ' 2) 打印 acme.sh 命令 (standalone)'
say ' 3) 打印 Cloudflare DNS (acme.sh) 示例'
say ' 0) 返回'
printf '选择: '
IFS= read -r c || return 0
printf '域名 (例 dzx.qzz.io): '
IFS= read -r dom || return 0
[ -z "$dom" ] && return 0
printf '邮箱 (可空): '
IFS= read -r em || true
case "$c" in
1)
say ''
say '# 申请前请确保 80 未被占用(可临时: systemctl stop 3m-ui)'
if [ -n "$em" ]; then
say "certbot certonly --standalone -d $dom --email $em --agree-tos --non-interactive"
else
say "certbot certonly --standalone -d $dom --register-unsafely-without-email --agree-tos --non-interactive"
fi
say "# 证书: /etc/letsencrypt/live/$dom/fullchain.pem"
say "# 私钥: /etc/letsencrypt/live/$dom/privkey.pem"
;;
2)
say ''
say 'curl https://get.acme.sh | sh'
say "~/.acme.sh/acme.sh --set-default-ca --server letsencrypt"
say "~/.acme.sh/acme.sh --issue -d $dom --standalone"
say "~/.acme.sh/acme.sh --install-cert -d $dom \\"
say " --key-file /etc/3m-ui/certs/${dom}.key \\"
say " --fullchain-file /etc/3m-ui/certs/${dom}.crt \\"
say " --reloadcmd \"systemctl restart 3m-ui\""
;;
3)
say ''
say 'export CF_Token="your_api_token"'
say "~/.acme.sh/acme.sh --issue -d $dom --dns dns_cf"
say "# 不占用 80,适合面板已占用 ACME 端口时给节点另签证书"
;;
*) ;;
esac
}
# --- update management script itself -----------------------------------------
update_self() {
# Lifecycle scripts and the panel binary are a single release unit. Routing
# this action through the installer keeps the checksum, provenance option,
# rollback snapshot, and health check that protect normal updates.
say '管理脚本会随已验证的面板版本一起更新。'
if ! confirm '通过已验证的发布更新 3m-ui(包括管理脚本)?' n; then
say '已取消。'
return 0
fi
run_script "$UPDATER" || return 1
}
update_to_version() {
printf '输入版本号 (如 v1.1.1,回车取消): '
IFS= read -r ver || return 0
[ -z "$ver" ] && return 0
case "$ver" in
v*) ;;
*) ver="v$ver" ;;
esac
say "将更新到 $ver ..."
run_script "$UPDATER" "$ver" || return 1
}
# --- menu --------------------------------------------------------------------
menu() {
while :; do
printf '\n'
printf '%s\n' '╭──────────────────────────────────────────────╮'
printf '%s\n' '│ 3m-ui 管理控制台 / Management Console │'
printf '%s\n' '│ Mihomo Core Management UI │'
printf '%s\n' '╰──────────────────────────────────────────────╯'
printf '\n'
printf '%s\n' ' 1. 安装/修复 | Install / Repair'
printf '%s\n' ' 2. 更新到最新 | Update latest'
printf '%s\n' ' 3. 更新到指定版本 | Update to version'
printf '%s\n' ' 4. 运行状态 | Status'
printf '%s\n' ' 5. 启动 | Start'
printf '%s\n' ' 6. 重启 | Restart'
printf '%s\n' ' 7. 停止 | Stop'
printf '%s\n' ' 8. 开机自启 开/关 | Enable/Disable'
printf '%s\n' ' 9. 日志 | Logs'
printf '%s\n' ' 10. 版本 | Version'
printf '%s\n' ' 11. 访问信息 / URI | Access info'
printf '%s\n' ' 12. 修改面板端口 | Change port'
printf '%s\n' ' 13. 设置 public_url | Public URL'
printf '%s\n' ' 14. 路径/订阅端口 | web_path sub_path sub_port'
printf '%s\n' ' 15. 重置管理员密码 | Reset admin'
printf '%s\n' ' 16. 健康检查 | Healthcheck'
printf '%s\n' ' 17. BBR | BBR menu'
printf '%s\n' ' 18. 更新 Geo | Update Geo'
printf '%s\n' ' 19. 防火墙放行提示 | Firewall'
printf '%s\n' ' 20. SSL 证书助手 | SSL cert helper'
printf '%s\n' ' 21. 备份 | Backup'
printf '%s\n' ' 22. 更新本管理脚本 | Update this script'
printf '%s\n' ' a. 卸载 | Uninstall'
printf '%s\n' ' 0. 退出 | Exit'
printf '\n请选择 / Select: '
IFS= read -r choice || exit 0
case "$choice" in
1) run_script "$INSTALLER" || true; pause ;;
2) run_script "$UPDATER" || true; pause ;;
3) update_to_version || true; pause ;;
4) status || true; pause ;;
5) service_action start || true; pause ;;
6) service_action restart || true; pause ;;
7) service_action stop || true; pause ;;
8)
printf 'e=启用 enable / d=禁用 disable: '
IFS= read -r ed || true
case "$ed" in
e|E) service_enable || true ;;
d|D) service_disable || true ;;
esac
pause
;;
9)
printf '跟随输出? [y/N]: '
IFS= read -r fl || true
case "$fl" in y|Y) logs -f || true ;; *) logs || true; pause ;; esac
;;
10) version || true; pause ;;
11) show_access || true; pause ;;
12) change_port || true; pause ;;
13) set_public_url || true; pause ;;
14) set_panel_paths || true; pause ;;
15) reset_admin || true; pause ;;
16) run_app healthcheck || true; pause ;;
17) bbr_menu || true; pause ;;
18) update_geo || true; pause ;;
19) open_firewall || true; pause ;;
20) ssl_cert_menu || true; pause ;;
21) run_script "$INSTALLER" --backup || true; pause ;;
22) update_self || true; pause ;;
a|A) run_script "$UNINSTALLER" || true; pause ;;
0) exit 0 ;;
*) say '无效选择,请重新输入。' ;;
esac
done
}
# --- main --------------------------------------------------------------------
channel_cmd() {
ch_file="$BASE/CHANNEL"
arg="${1:-}"
do_update=0
if [ "$#" -gt 0 ]; then
shift
fi
for a in "$@"; do
case "$a" in --update|update|-u) do_update=1 ;; esac
done
if [ -z "$arg" ]; then
cur=stable
if [ -s "$ch_file" ]; then
cur=$(tr -d '\n\r' < "$ch_file")
fi
case "$cur" in pre|prerelease) cur=pre ;; *) cur=stable ;; esac
say "Current update channel: $cur"
say "Switch: 3m-ui channel stable | 3m-ui channel pre"
say "Switch and upgrade now: 3m-ui channel pre --update"
return 0
fi
case "$arg" in
stable|pre) ;;
prerelease) arg=pre ;;
*)
err "Unknown channel: $arg (use stable or pre)"
return 1
;;
esac
mkdir -p "$BASE"
printf '%s\n' "$arg" > "$ch_file"
say "Update channel set to: $arg (saved in $ch_file)"
if [ "$do_update" -eq 1 ]; then
say "Running update on channel $arg ..."
THREE_M_UI_CHANNEL="$arg" run_script "$UPDATER" || return 1
else
say "Next: 3m-ui update # install latest $arg release"
say " or: 3m-ui channel $arg --update"
fi
}
main() {
cmd=${1:-}
case "$cmd" in
help|-h|--help) ;;
*) need_root ;;
esac
case "$cmd" in
status) status ;;
start) service_action start ;;
restart) service_action restart ;;
stop) service_action stop ;;
enable) service_enable ;;
disable) service_disable ;;
logs) shift; logs "$@" ;;
version) version ;;
channel) shift; channel_cmd "$@" ;;
config) shift; run_script "$MANAGER" config "$@" ;;
backup) shift; run_script "$INSTALLER" --backup "$@" ;;
restore) shift; run_script "$INSTALLER" --restore "$@" ;;
init|reset-admin|healthcheck) run_app "$cmd" ;;
update|upgrade)
shift
run_script "$UPDATER" "$@"
;;
install|fix)
shift
run_script "$INSTALLER" "$@"
;;
uninstall|remove)
shift
run_script "$UNINSTALLER" "$@"
;;
menu) menu ;;
help|-h|--help)
printf '%s\n' '用法: 3m-ui <命令> [参数]'
printf '%s\n' ''
printf '%s\n' '服务: status | start | restart | stop | enable | disable | logs | version | channel'
printf '%s\n' '配置: config show | config port <端口>'
printf '%s\n' '安装: update [vX.Y.Z|pre] | channel stable|pre [--update] | install | uninstall [--purge]'
printf '%s\n' '数据: backup | restore <snapshot.tar.gz> | reset-admin | healthcheck'
printf '%s\n' '其他: menu | help'
printf '%s\n' ''
printf '%s\n' '不带参数时进入交互式菜单。'
;;
'') menu ;;
*)
err "未知命令: $cmd"
err '运行: 3m-ui help'
exit 2
;;
esac
}
main "$@"