feat(scripts/3m-ui): expand management console with ops features

Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
feat(scripts/3m-ui): expand management console with ops features
Reimplemented for 3m-ui (not a copy of third-party scripts):
- enable/disable on boot, follow logs, access URI view
- port / public_url / web_path / sub_path / sub_port editors
- update to version, update this script from repo
- BBR status/enable/disable, Geo, firewall hints
- SSL cert helper (certbot/acme.sh command templates)
- backup entry, healthcheck, confirm + pause menu flow
This commit is contained in:
風雪 committed 2026-09-14 11:27:17 +08:00
1 parent eadd5255b0
commit aa08f34be4
1 file changed
+531 -207
+531 -207
View File
@@ -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://<IP>:${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:-<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
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:-<empty>}"
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 <snapshot.tar.gz> | 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