Files
3m-ui/scripts/3m-ui
T
Z User bffb21e415 feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
feat(scripts): add reset-config to interactive menu + CLI entrypoint
The 3m-ui management entrypoint (/usr/local/bin/3m-ui) had an
interactive menu but no reset-config option — operators had to know
the CLI flag syntax to SSH rescue a panel. Now both paths work:
INTERACTIVE MENU (3m-ui with no args)
Select option 16 → SSH 救援 / SSH Rescue sub-menu:
1) 仅面板 (listen + SSL)              [most common]
2) 面板 + 订阅地址 (public_url)
3) 全量重置 (panel + public + access_profile)
0) 返回
→ y/N 确认 → 自动 stop/run binary/start
CLI ENTRYPOINT
3m-ui reset-config --panel --yes     # scripted rescue
3m-ui reset-config --all --yes        # full reset
3m-ui reset-config --help             # help (no service touched)
3m-ui help                            # shows SSH rescue examples
CHANGES
scripts/3m-ui:
- New reset_config() function with interactive scope picker
- run_app() extended to pass-through extra args (was single-arg only)
- Menu: insert option 16 'SSH 救援重置面板配置', renumber 17-23
- main() case: add 'reset-config' subcommand with stop/run/start
cycle; --help short-circuits (read-only, no service touched)
- help text: add SSH rescue examples
Smoke-tested: dispatch + flag passthrough + scope picker + y/N
confirmation all work via fake binary; --help does not stop service.
2026-09-25 06:26:55 +00:00

822 lines
28 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"
shift
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
}
reset_config() {
if [ ! -x "$APP_BIN" ]; then
err "未找到程序: $APP_BIN"
return 1
fi
say '=== SSH 救援 / SSH Rescue ==='
say '当面板配置错误(端口/SSL/监听地址)导致 Web 无法访问时,'
say '可通过此菜单重置为安全默认值,重启后面板以 HTTP 模式启动。'
say ''
say '重置范围 / Scope:'
say ' 1) 仅面板 (listen + SSL) — 最常用,仅 Web 打不开时'
say ' 2) 面板 + 订阅地址 — 同时清除 public_url'
say ' 3) 面板 + 订阅 + 节点公网地址 — 全量重置(含 access_profile)'
say ' 0) 返回 / Cancel'
printf '选择 [0-3]: '
IFS= read -r scope || return 0
case "$scope" in
1) flags='--panel --yes' ;;
2) flags='--panel --public --yes' ;;
3) flags='--all --yes' ;;
*) say '已取消。'; return 0 ;;
esac
say ''
say "将以 ${flags} 重置。该操作会停止面板、修改 config.yaml + DB,然后重启面板。"
say '不会删除用户 / 节点 / 流量记录 / 凭证。'
if ! confirm '确认继续?' n; then
say '已取消。'
return 0
fi
say ''
say '停止 3m-ui 服务...'
service_action stop >/dev/null 2>&1 || true
if ! run_app reset-config $flags; then
err '✗ reset-config 失败,请查看上方错误。面板将以旧配置重启。'
service_action start || true
return 1
fi
say ''
say '重启 3m-ui 服务...'
service_action start || true
sleep 1
if service_is_active; then
say '✓ 重置完成。请用 HTTP(不是 HTTPS)访问面板:'
port=$(yaml_get ' port:' 2>/dev/null || echo "8080")
say " http://<server-ip>:${port}/"
say '在 Web UI 重新应用 SSL / 域名 / 订阅地址设置。'
else
err '✗ 面板启动失败,请检查: 3m-ui logs'
fi
}
# --- 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. SSH 救援重置面板配置 | Reset-config (SSH rescue)'
printf '%s\n' ' 17. 健康检查 | Healthcheck'
printf '%s\n' ' 18. BBR | BBR menu'
printf '%s\n' ' 19. 更新 Geo | Update Geo'
printf '%s\n' ' 20. 防火墙放行提示 | Firewall'
printf '%s\n' ' 21. SSL 证书助手 | SSL cert helper'
printf '%s\n' ' 22. 备份 | Backup'
printf '%s\n' ' 23. 更新本管理脚本 | 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) reset_config || true; pause ;;
17) run_app healthcheck || true; pause ;;
18) bbr_menu || true; pause ;;
19) update_geo || true; pause ;;
20) open_firewall || true; pause ;;
21) ssl_cert_menu || true; pause ;;
22) run_script "$INSTALLER" --backup || true; pause ;;
23) 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" ;;
reset-config)
# reset-config needs the service stopped before the Go binary
# runs (it writes config.yaml + DB rows the running panel caches).
# --help / -h short-circuits — no stop/restart needed.
shift # drop 'reset-config' so $@ holds the flags
help_only=0
for a in "$@"; do
case "$a" in --help|-h) help_only=1 ;; esac
done
if [ "$help_only" = 1 ]; then
run_app reset-config "$@"
else
say '停止 3m-ui 服务...'
service_action stop >/dev/null 2>&1 || true
if ! run_app reset-config "$@"; then
err '✗ reset-config 失败,请查看上方错误。'
service_action start || true
exit 1
fi
say '重启 3m-ui 服务...'
service_action start || true
fi
;;
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 | reset-config | healthcheck'
printf '%s\n' '其他: menu | help'
printf '%s\n' ''
printf '%s\n' 'SSH 救援 / SSH rescue:'
printf '%s\n' ' 3m-ui reset-config --panel --yes # 重置 listen/SSL,重启后 HTTP 访问'
printf '%s\n' ' 3m-ui reset-config --all --yes # 全量重置(含 public_url + access_profile)'
printf '%s\n' ''
printf '%s\n' '不带参数时进入交互式菜单。'
;;
'') menu ;;
*)
err "未知命令: $cmd"
err '运行: 3m-ui help'
exit 2
;;
esac
}
main "$@"