diff --git a/scripts/3m-ui b/scripts/3m-ui index 8ed5089..f013356 100755 --- a/scripts/3m-ui +++ b/scripts/3m-ui @@ -1,7 +1,8 @@ #!/bin/sh -# 3m-ui unified management entrypoint. -# Installed as /usr/local/bin/3m-ui. -# The actual Go application binary is /usr/local/lib/3m-ui/3m-ui-bin. +# 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 @@ -13,68 +14,54 @@ UPDATER="$BASE/update.sh" UNINSTALLER="$BASE/uninstall.sh" APP_BIN="$BASE/3m-ui-bin" CONFIG_FILE="$ROOT/etc/3m-ui/config.yaml" +REPO_RAW="${THREE_M_UI_REPO_RAW:-https://raw.githubusercontent.com/kazeyukiro/3m-ui/main}" DATA_DIR="${THREE_M_UI_DATA_DIR:-}" -if [ -z "$DATA_DIR" ] && [ -s "$BASE/DATA_DIRECTORY" ]; then DATA_DIR="$(cat "$BASE/DATA_DIRECTORY")"; fi +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" ] || { - printf '%s\n' "3m-ui management script is missing: $script" >&2 + err "管理脚本不存在: $script" return 1 } - # Keep menu alive after child scripts finish. "$script" "$@" } -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' - printf '%s\n' ' 3. 运行状态 | Status' - printf '%s\n' ' 4. 启动 | Start' - printf '%s\n' ' 5. 重启 | Restart' - printf '%s\n' ' 6. 停止 | Stop' - printf '%s\n' ' 7. 日志 | Logs' - printf '%s\n' ' 8. 版本 | Version' - printf '%s\n' ' 9. 修改面板端口 | Change panel port' - printf '%s\n' ' 10. 端口与 public_url | Port & public_url' - printf '%s\n' ' 11. 重置管理员密码 | Reset admin password' - printf '%s\n' ' 12. 启用 BBR | Enable BBR (Linux)' - printf '%s\n' ' 13. 更新 Geo | Update Geo rules' - printf '%s\n' ' 14. 设置 public_url | Set public_url' - printf '%s\n' ' a. 卸载 | Uninstall' - printf '%s\n' ' 0. 退出 | Exit' - printf '\n请选择 / Select: ' - IFS= read -r choice || exit 0 - case "$choice" in - # || true: set -e must not tear down the whole menu on a failed action - 1) run_script "$INSTALLER" || true ;; - 2) run_script "$UPDATER" || true ;; - 3) status || true ;; - 4) service_action start || true ;; - 5) service_action restart || true ;; - 6) service_action stop || true ;; - 7) logs || true ;; - 8) version || true ;; - 9) change_port || true ;; - 10) show_access || true ;; - 11) reset_admin || true ;; - 12) enable_bbr || true ;; - 13) update_geo || true ;; - 14) set_public_url || true ;; - a|A) run_script "$UNINSTALLER" || true ;; - 0) exit 0 ;; - *) printf '%s\n' '无效选择,请重新输入。' ;; - esac - done +run_app() { + cmd="$1" + if [ ! -x "$APP_BIN" ]; then + err "未找到程序: $APP_BIN" + return 1 + fi + (cd "$DATA_DIR" && THREE_M_UI_CONFIG="$CONFIG_FILE" THREE_M_UI_DATA_DIR="$DATA_DIR" "$APP_BIN" "$cmd") } init_system() { @@ -92,7 +79,7 @@ service_action() { case "$(init_system)" in systemd) systemctl "$action" 3m-ui ;; openrc) rc-service 3m-ui "$action" ;; - *) printf '%s\n' '未检测到 systemd 或 OpenRC。' >&2; return 1 ;; + *) err '未检测到 systemd 或 OpenRC。'; return 1 ;; esac } @@ -104,26 +91,105 @@ service_is_active() { esac } +service_enable() { + case "$(init_system)" in + systemd) systemctl enable 3m-ui && say '已设置开机自启 / Enabled on boot.' ;; + openrc) rc-update add 3m-ui default && say '已设置开机自启 / Enabled on boot.' ;; + *) err '不支持的 init'; return 1 ;; + esac +} + +service_disable() { + case "$(init_system)" in + systemd) systemctl disable 3m-ui && say '已取消开机自启 / Disabled on boot.' ;; + 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 ;; - *) printf '%s\n' 'init: unsupported' ;; + *) 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 - printf '%s\n' "应用程序: $APP_BIN" + say "应用程序: $APP_BIN" "$APP_BIN" --version 2>/dev/null || true else - printf '%s\n' '应用程序二进制不存在。' + say '应用程序二进制不存在。' fi } logs() { case "$(init_system)" in - systemd) journalctl -u 3m-ui -n 100 --no-pager ;; - openrc) tail -n 100 /var/log/3m-ui/3m-ui.log 2>/dev/null || printf '%s\n' '未找到日志文件。' ;; - *) printf '%s\n' '未找到可用的日志系统。' ;; + systemd) + if [ "${1:-}" = "-f" ] || [ "${1:-}" = "follow" ]; then + journalctl -u 3m-ui -f --no-pager + else + journalctl -u 3m-ui -n 100 --no-pager + 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 + else + tail -n 100 /var/log/3m-ui/3m-ui.log + fi + else + say '未找到日志文件 /var/log/3m-ui/3m-ui.log' + fi + ;; + *) say '未找到可用的日志系统。' ;; esac } @@ -131,85 +197,208 @@ version() { printed=0 if [ -x "$APP_BIN" ]; then out="$("$APP_BIN" --version 2>/dev/null || true)" - if [ -z "$out" ]; then - out="$("$APP_BIN" version 2>/dev/null || true)" - fi + [ -z "$out" ] && out="$("$APP_BIN" version 2>/dev/null || true)" if [ -n "$out" ]; then printf '%s\n' "$out" printed=1 fi fi - if [ -s "$BASE/VERSION" ]; then - printf 'Installed release: %s -' "$(cat "$BASE/VERSION")" - printed=1 - fi - if [ -s "$BASE/CHANNEL" ]; then - printf 'Update channel: %s -' "$(cat "$BASE/CHANNEL")" - printed=1 - fi - if [ -s "$BASE/MIHOMO_VERSION" ]; then - printf 'Bundled Mihomo: %s -' "$(cat "$BASE/MIHOMO_VERSION")" - printed=1 - fi - if [ -s "$BASE/REPOSITORY" ]; then - printf 'Release source: %s -' "$(cat "$BASE/REPOSITORY")" - printed=1 - 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 -' "$APP_BIN" + printf 'Binary: %s\n' "$APP_BIN" printed=1 fi if [ "$printed" -eq 0 ]; then - printf '%s -' '3m-ui 未安装或无法读取版本信息。' >&2 - exit 1 + err '3m-ui 未安装或无法读取版本信息。' + return 1 fi } -need_root() { - [ "$(id -u)" -eq 0 ] || { printf '%s\n' '请使用 root 运行 3m-ui 管理命令。' >&2; exit 1; } -} - +# --- config / access --------------------------------------------------------- show_access() { - port=$(awk '/^ port:/ {print $2; exit}' "$CONFIG_FILE" 2>/dev/null || echo "8080") - pub=$(awk '/^ public_url:/ {print $2; exit}' "$CONFIG_FILE" 2>/dev/null | tr -d '"' || true) - printf '%s\n' "配置端口: $port" + 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 - printf '%s\n' "public_url: $pub" + # strip trailing slash for display + base=$(printf '%s' "$pub" | sed 's|/*$||') + say "面板访问 / Panel URI: ${base}${path_suffix}/" + else + say "面板访问示例 (请换成公网 IP/域名):" + say " http://:${port}${path_suffix}/" + say " https://<域名>/ (若已启用面板 SSL 且监听 443)" fi - printf '%s\n' '面板启用 HTTPS 后,请使用面板中配置的域名和 HTTPS 端口。' - if (cd "$DATA_DIR" && THREE_M_UI_CONFIG="$CONFIG_FILE" "$APP_BIN" healthcheck) >/dev/null 2>&1; then - printf '%s\n' '面板健康检查通过。' + 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:-}" + 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:-}" + IFS= read -r web || true + printf 'sub_path (订阅路径前缀, 现: %s): ' "${cur_sub:-}" + IFS= read -r sub || true + printf 'sub_port (0=与面板同端口, 现: %s): ' "${cur_sp:-0}" + IFS= read -r sp || true + printf 'listen (空=全部接口, 现: %s): ' "${cur_listen:-}" + 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 - printf '%s\n' '管理命令: 3m-ui' - printf '%s\n' '初始密码在首次安装时随机生成;若遗失,可用 3m-ui reset-admin 生成新密码。' } reset_admin() { if [ ! -x "$APP_BIN" ]; then - printf '%s\n' "未找到程序: $APP_BIN" >&2 + err "未找到程序: $APP_BIN" return 1 fi - printf '%s\n' '将随机生成新的管理员密码,并要求下次登录修改密码。' - printf '确认? [y/N] ' - IFS= read -r ans || return 0 - case "$ans" in y|Y|yes|YES) ;; *) printf '%s\n' '已取消。'; return 0 ;; esac - (cd "$DATA_DIR" && THREE_M_UI_CONFIG="$CONFIG_FILE" "$APP_BIN" reset-admin) || return 1 + 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 - printf '%s\n' '仅支持 Linux。' >&2 + err '仅支持 Linux。' return 1 fi if sysctl net.ipv4.tcp_congestion_control 2>/dev/null | grep -q bbr; then - printf '%s\n' 'BBR 已启用。' + say 'BBR 已启用。' return 0 fi modprobe tcp_bbr 2>/dev/null || true @@ -218,103 +407,250 @@ enable_bbr() { 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 - sysctl net.ipv4.tcp_congestion_control 2>/dev/null || true - printf '%s\n' '已尝试启用 BBR。' + 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() { dir="$DATA_DIR/mihomo" mkdir -p "$dir" base="https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest" - printf '%s\n' "下载 Geo 到 $dir ..." + say "下载 Geo 到 $dir ..." if command -v curl >/dev/null 2>&1; then - curl -fsSL -o "$dir/geoip.metadb" "$base/geoip.metadb" || curl -fsSL -o "$dir/geoip.dat" "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat" || true + curl -fsSL -o "$dir/geoip.metadb" "$base/geoip.metadb" || \ + curl -fsSL -o "$dir/geoip.dat" "$base/geoip.dat" || true curl -fsSL -o "$dir/geosite.dat" "$base/geosite.dat" || true elif command -v wget >/dev/null 2>&1; then wget -q -O "$dir/geoip.metadb" "$base/geoip.metadb" || true wget -q -O "$dir/geosite.dat" "$base/geosite.dat" || true else - printf '%s\n' '需要 curl 或 wget' >&2 + err '需要 curl 或 wget' return 1 fi ls -la "$dir"/geoip* "$dir"/geosite* 2>/dev/null || true - printf '%s\n' '完成。如内核已在运行,可在面板重启 Mihomo 使规则生效。' + say '完成。可在面板重启 Mihomo 使规则生效。' } -set_public_url() { - if [ ! -f "$CONFIG_FILE" ]; then - printf '%s\n' "配置不存在: $CONFIG_FILE" >&2 - return 1 - fi - cur=$(awk '/^ public_url:/ {print $2; exit}' "$CONFIG_FILE" 2>/dev/null | tr -d '"' || true) - printf '当前 public_url: %s\n' "${cur:-}" - printf '输入新的 public_url (例如 https://example.com:8443 ,回车取消): ' - IFS= read -r url || return 0 - [ -z "$url" ] && return 0 - # Escape for sed replacement (|, &, \) - esc=$(printf '%s' "$url" | sed -e 's/[\\|&]/\\\\&/g') - if grep -q '^ public_url:' "$CONFIG_FILE" 2>/dev/null; then - sed -i "s|^ public_url:.*| public_url: \"$esc\"|" "$CONFIG_FILE" - elif grep -q '^server:' "$CONFIG_FILE" 2>/dev/null; then - # Portable insert: rewrite via temp file (works on GNU and BusyBox sed) - tmp="${CONFIG_FILE}.tmp.$$" - awk -v u="$url" ' - BEGIN { done=0 } - { print } - /^server:/ && !done { print " public_url: \"" u "\""; done=1 } - ' "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE" +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 - printf '%s\n' "配置中未找到 server: 段,请手动编辑 $CONFIG_FILE" >&2 - return 1 - fi - printf '%s\n' "已写入 public_url。重启面板后订阅外链将使用该地址。" - service_action restart - if service_is_active; then - printf '%s\n' '✓ 服务已重启。' - else - printf '%s\n' '✗ 服务重启后未处于运行状态,请检查: 3m-ui logs' >&2 + say '未检测到 ufw/firewalld。请在云安全组放行 80/443 与面板端口。' fi } - -change_port() { - if [ ! -f "$CONFIG_FILE" ]; then - printf '%s\n' "配置文件不存在: $CONFIG_FILE" >&2 - return 1 - fi - current=$(awk '/^ port:/ {print $2; exit}' "$CONFIG_FILE" 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]*) printf '%s\n' '无效端口。' >&2; return 1 ;; +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 - [ "$new_port" -ge 1 ] 2>/dev/null && [ "$new_port" -le 65535 ] 2>/dev/null || { - printf '%s\n' '端口必须在 1-65535 范围内。' >&2 - return 1 - } - [ "$new_port" = "$current" ] && { - printf '%s\n' '端口未变更。' - return 0 - } - if grep -q '^ port:' "$CONFIG_FILE" 2>/dev/null; then - sed -i "s/^ port: .*/ port: $new_port/" "$CONFIG_FILE" - else - printf '%s\n' "配置中未找到 ' port:' 字段,请手动编辑 $CONFIG_FILE" >&2 - return 1 - fi - printf '%s\n' "端口已更改: $current → $new_port" - printf '%s\n' '正在重启 3m-ui...' - service_action restart - sleep 1 - if service_is_active; then - printf '%s\n' "✓ 3m-ui 已重启。配置端口: $new_port(独立 HTTPS 端口以面板设置为准)" - else - printf '%s\n' '✗ 3m-ui 启动失败,请检查日志: 3m-ui logs' >&2 - fi } +# --- update management script itself ----------------------------------------- + +update_self() { + say "从仓库更新管理脚本: $REPO_RAW/scripts/3m-ui" + if ! confirm '确认下载并覆盖 /usr/local/bin/3m-ui 与 $BASE/3m-ui ?' n; then + say '已取消。' + return 0 + fi + tmp=$(mktemp) + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$REPO_RAW/scripts/3m-ui" -o "$tmp" || { err '下载失败'; rm -f "$tmp"; return 1; } + elif command -v wget >/dev/null 2>&1; then + wget -q -O "$tmp" "$REPO_RAW/scripts/3m-ui" || { err '下载失败'; rm -f "$tmp"; return 1; } + else + err '需要 curl 或 wget' + return 1 + fi + if ! head -1 "$tmp" | grep -q '^#!'; then + err '下载内容不像 shell 脚本' + rm -f "$tmp" + return 1 + fi + install -m 0755 "$tmp" /usr/local/bin/3m-ui + if [ -d "$BASE" ]; then + install -m 0755 "$tmp" "$BASE/3m-ui" + fi + rm -f "$tmp" + say '管理脚本已更新。请重新运行: 3m-ui' +} + +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 -------------------------------------------------------------------- + main() { cmd=${1:-} case "$cmd" in @@ -326,12 +662,14 @@ main() { start) service_action start ;; restart) service_action restart ;; stop) service_action stop ;; - logs) logs ;; + enable) service_enable ;; + disable) service_disable ;; + logs) shift; logs "$@" ;; version) version ;; config) shift; run_script "$MANAGER" config "$@" ;; backup) shift; run_script "$INSTALLER" --backup "$@" ;; restore) shift; run_script "$INSTALLER" --restore "$@" ;; - init|reset-admin|healthcheck) (cd "$DATA_DIR" && THREE_M_UI_CONFIG="$CONFIG_FILE" THREE_M_UI_DATA_DIR="$DATA_DIR" "$APP_BIN" "$cmd") ;; + init|reset-admin|healthcheck) run_app "$cmd" ;; update|upgrade) shift run_script "$UPDATER" "$@" @@ -344,36 +682,22 @@ main() { shift run_script "$UNINSTALLER" "$@" ;; - menu) - menu - ;; + menu) menu ;; help|-h|--help) printf '%s\n' '用法: 3m-ui <命令> [参数]' printf '%s\n' '' - printf '%s\n' '服务:' - printf '%s\n' ' status | start | restart | stop | logs | version' - printf '%s\n' '配置:' - printf '%s\n' ' config show 查看配置' - printf '%s\n' ' config port <端口> 修改端口并重启' - printf '%s\n' '安装与更新:' - printf '%s\n' ' update [vX.Y.Z] [--yes] 更新到指定版本或所选通道最新版本' - printf '%s\n' ' install 安装 / 修复' - printf '%s\n' ' uninstall [--purge] 卸载,默认保留配置、密钥和数据' - printf '%s\n' '备份与恢复:' - printf '%s\n' ' backup 停服创建完整快照,然后恢复运行' - printf '%s\n' ' restore /path/snapshot.tar.gz [--yes] 恢复快照' - printf '%s\n' ' reset-admin 生成并显示新的随机管理员密码' - printf '%s\n' ' healthcheck 检查实际 HTTP/HTTPS 面板健康状态' - printf '%s\n' '其他:' - printf '%s\n' ' menu 交互式管理菜单' - printf '%s\n' ' help 显示本帮助' + printf '%s\n' '服务: status | start | restart | stop | enable | disable | logs | version' + printf '%s\n' '配置: config show | config port <端口>' + printf '%s\n' '安装: update [vX.Y.Z] | install | uninstall [--purge]' + printf '%s\n' '数据: backup | restore | reset-admin | healthcheck' + printf '%s\n' '其他: menu | help' printf '%s\n' '' - printf '%s\n' '不带参数时进入交互式菜单(等同于 menu)。' + printf '%s\n' '不带参数时进入交互式菜单。' ;; '') menu ;; *) - printf '%s\n' "未知命令: $cmd" >&2 - printf '%s\n' '运行: 3m-ui help' >&2 + err "未知命令: $cmd" + err '运行: 3m-ui help' exit 2 ;; esac