#!/usr/bin/env bash # # 牛气面板 · 一句话安装器 # # curl -fsSL https://get.niuqi.net/install.sh | sudo bash # curl -fsSL https://get.niuqi.net/install.sh | sudo bash -s -- --domain panel.example.com # # 三种用法: # 1. 全新安装(默认):装二进制、生成密钥、写配置、起服务、配 nginx 入口; # 2. 升级(--upgrade):只替换二进制,库、密钥、配置、证书全部保留; # 3. 卸载(--uninstall):停服务删程序,数据目录默认保留(--purge 才删)。 # # 设计取舍: # · 不依赖 deb 包管理:一个脚本加两个静态二进制,装到任何 apt 系发行版都一样。 # 代价是没有 dpkg 的依赖解析与卸载记录,换来的是"内网机器也能装"。 # · 下载必须校验 sha256:安装脚本以 root 运行,下载链路被投毒的后果是整机沦陷。 # · 重复执行必须安全:所有写配置的地方都是"不存在才写",升级不会覆盖用户改过的配置。 set -Eeuo pipefail # ── 可调参数 ──────────────────────────────────────────── BASE_URL="${NIUQI_BASE_URL:-https://get.niuqi.net}" PREFIX="/opt/panel" CONF_DIR="/etc/panel" DATA_DIR="/var/lib/panel" RUN_DIR="/run/panel" SERVICE_USER="panel" PANEL_PORT="8443" PANEL_DOMAIN="" ALLOW_CIDRS="" USE_NGINX=1 FROM_DIR="" MODE="install" PURGE=0 REMOTE_VERSION="" REMOTE_RELEASED="" CHECK=0 BRAND_NAME="" BRAND_SUB="" BRAND_MARK="" # ── 输出 ──────────────────────────────────────────────── if [[ -t 1 ]]; then C_OK=$'\033[32m'; C_WARN=$'\033[33m'; C_ERR=$'\033[31m'; C_DIM=$'\033[2m'; C_B=$'\033[1m'; C_0=$'\033[0m' else C_OK=''; C_WARN=''; C_ERR=''; C_DIM=''; C_B=''; C_0='' fi ok() { printf ' %s✓%s %s\n' "$C_OK" "$C_0" "$*"; } warn() { printf ' %s!%s %s\n' "$C_WARN" "$C_0" "$*"; } die() { printf '\n%s✗ %s%s\n' "$C_ERR" "$*" "$C_0" >&2; exit 1; } step() { printf '\n%s▸ %s%s\n' "$C_B" "$*" "$C_0"; } dim() { printf ' %s%s%s\n' "$C_DIM" "$*" "$C_0"; } usage() { cat <<'USAGE' 牛气面板安装器 用法: curl -fsSL https://get.niuqi.net/install.sh | sudo bash sudo bash install.sh [选项] 选项: --domain <域名> 面板访问域名,用于 nginx server_name 与自签证书 --port <端口> 面板独立端口,默认 8443 --allow-cidrs <列表> 只允许这些来源访问面板,逗号分隔,如 1.2.3.0/24,10.0.0.0/8 --no-nginx 不配置 nginx 入口,只跑 127.0.0.1:8899(用 SSH 隧道访问) --from <目录> 从本地目录取二进制(离线 / 内网安装) --base-url <地址> 下载地址,默认 https://get.niuqi.net --brand-name <名字> 自定义品牌名(OEM 白标) --brand-mark <字符> 自定义侧边栏标记字符 --check 只做环境预检、下载与校验,不改动系统 --upgrade 升级:只替换二进制,保留数据与配置 --uninstall 卸载(保留数据) --purge 与 --uninstall 一起用:连数据一起删 -h, --help 显示本帮助 环境变量: NIUQI_BASE_URL 等价于 --base-url USAGE } # ── 参数解析 ──────────────────────────────────────────── while [[ $# -gt 0 ]]; do case "$1" in --domain) PANEL_DOMAIN="${2:-}"; shift 2 ;; --port) PANEL_PORT="${2:-}"; shift 2 ;; --allow-cidrs) ALLOW_CIDRS="${2:-}"; shift 2 ;; --no-nginx) USE_NGINX=0; shift ;; --from) FROM_DIR="${2:-}"; shift 2 ;; --base-url) BASE_URL="${2:-}"; shift 2 ;; --brand-name) BRAND_NAME="${2:-}"; shift 2 ;; --brand-sub) BRAND_SUB="${2:-}"; shift 2 ;; --brand-mark) BRAND_MARK="${2:-}"; shift 2 ;; --check) CHECK=1; shift ;; --upgrade) MODE="upgrade"; shift ;; --uninstall) MODE="uninstall"; shift ;; --purge) PURGE=1; shift ;; -h|--help) usage; exit 0 ;; *) die "未知参数: $1(--help 查看用法)" ;; esac done [[ ${EUID} -eq 0 ]] || die "需要 root 权限运行(请用 sudo)" # ── 卸载 ──────────────────────────────────────────────── do_uninstall() { step "停止服务" for unit in panel-api panel-agent; do if systemctl list-unit-files | grep -q "^${unit}.service"; then systemctl disable --now "${unit}" 2>/dev/null || true rm -f "/etc/systemd/system/${unit}.service" ok "已移除 ${unit}" fi done systemctl daemon-reload || true step "删除程序" rm -rf "${PREFIX}" rm -f /etc/nginx/sites-enabled/panel.conf /etc/nginx/sites-available/panel.conf rm -f "${CONF_DIR}/agent.conf" "${CONF_DIR}/api.conf" "${CONF_DIR}/brand.conf" \ "${CONF_DIR}/mysql-admin.cnf" "${CONF_DIR}/alert.conf" rmdir "${CONF_DIR}" 2>/dev/null || true if nginx -t >/dev/null 2>&1; then systemctl reload nginx 2>/dev/null || true ok "已移除 nginx 入口" fi if [[ ${PURGE} -eq 1 ]]; then step "删除数据(--purge)" warn "即将删除 ${DATA_DIR}:数据库、站点备份、上传中转、初始密码文件" rm -rf "${DATA_DIR}" ok "数据已删除" else dim "数据保留在 ${DATA_DIR}(含面板数据库与备份)" dim "确认不再需要时执行:sudo bash $0 --uninstall --purge" fi getent passwd "${SERVICE_USER}" >/dev/null && userdel "${SERVICE_USER}" 2>/dev/null || true getent group "${SERVICE_USER}" >/dev/null && groupdel "${SERVICE_USER}" 2>/dev/null || true printf '\n%s卸载完成%s\n' "$C_OK" "$C_0" } if [[ "${MODE}" == "uninstall" ]]; then do_uninstall exit 0 fi # ── 环境预检 ──────────────────────────────────────────── step "环境预检" [[ -f /etc/os-release ]] || die "无法识别发行版(缺 /etc/os-release)" . /etc/os-release OS_ID="${ID:-unknown}" case "${OS_ID}" in ubuntu|debian) ok "发行版 ${PRETTY_NAME:-$OS_ID}" ;; *) warn "只在 Ubuntu / Debian 上验证过,当前是 ${PRETTY_NAME:-$OS_ID},继续但可能遇到路径差异" ;; esac ARCH="$(uname -m)" case "${ARCH}" in x86_64|amd64) ARCH_TAG="amd64" ;; aarch64|arm64) ARCH_TAG="arm64" ;; *) die "不支持的架构 ${ARCH}(只提供 amd64 / arm64)" ;; esac ok "架构 ${ARCH} → ${ARCH_TAG}" for cmd in curl tar systemctl; do command -v "${cmd}" >/dev/null || die "缺少命令 ${cmd}(apt-get install -y ${cmd})" done ok "基础命令齐备" # web 用户:Debian/Ubuntu 是 www-data,RHEL 系是 nginx if id www-data >/dev/null 2>&1; then WEB_USER="www-data" elif id nginx >/dev/null 2>&1; then WEB_USER="nginx" else WEB_USER="www-data" warn "没找到 www-data / nginx 用户,先按 www-data 写配置;装完 nginx 后请检查 agent.conf 的 web_user" fi ok "Web 用户 ${WEB_USER}" # 端口占用检查(ss 不一定装了,缺了就跳过,别让预检自己先失败) # # 已装机器上这个端口本来就该被自己的 nginx 占着 —— 升级时占着它的正是它。 # 不区分的话,每个老用户升级都会看到「端口已被占用」然后停下来问, # 而答案永远是「那是你自己」。判据取「已装 + 占用者是 nginx」: # 新机器上前半条不成立,不会把别人的 nginx 误判成自己的。 if command -v ss >/dev/null && ss -lntp 2>/dev/null | grep -qE ":${PANEL_PORT}\b"; then _holder="$(ss -lntp 2>/dev/null | grep -E ":${PANEL_PORT}\b" | head -1)" if [[ -x "${PREFIX}/bin/panel-api" && "${_holder}" == *nginx* ]]; then ok "端口 ${PANEL_PORT} 由现有面板的 nginx 占用(升级时会平滑替换)" else warn "端口 ${PANEL_PORT} 已被占用,nginx 入口可能起不来:" ss -lntp 2>/dev/null | grep -E ":${PANEL_PORT}\b" | sed 's/^/ /' fi fi # ── 下载与校验 ────────────────────────────────────────── # # 抽成函数是为了让 --check 走同一份代码:「先验一遍再装」与「直接装」 # 各写一套的话,两边迟早对不上,而对不上的那次一定是发版当天。 TMP="$(mktemp -d)" trap 'rm -rf "${TMP}"' EXIT fetch() { local name="$1" dest="$2" if [[ -n "${FROM_DIR}" ]]; then [[ -f "${FROM_DIR}/${name}" ]] || die "本地目录里没有 ${name}" cp "${FROM_DIR}/${name}" "${dest}" return 0 fi curl -fsSL --connect-timeout 15 --max-time 300 -o "${dest}" "${BASE_URL}/${name}" \ || die "下载失败: ${BASE_URL}/${name}" } # 版本信息只是"好看有用",不是安装前提:拿不到就明说,不阻断。 # 但它决定了升级时能不能告诉用户"从哪个版本到哪个版本"。 read_remote_version() { OLD_VERSION="$(cat "${PREFIX}/VERSION" 2>/dev/null || true)" if [[ -n "${FROM_DIR}" ]]; then [[ -f "${FROM_DIR}/version.json" ]] && \ cp "${FROM_DIR}/version.json" "${TMP}/version.json" 2>/dev/null || true else curl -fsSL --connect-timeout 10 --max-time 20 -o "${TMP}/version.json" \ "${BASE_URL}/version.json" 2>/dev/null || true fi if [[ -s "${TMP}/version.json" ]]; then REMOTE_VERSION="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "${TMP}/version.json" | head -1)" REMOTE_RELEASED="$(sed -n 's/.*"released"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "${TMP}/version.json" | head -1)" if [[ -n "${REMOTE_VERSION}" ]]; then ok "版本 ${REMOTE_VERSION}${REMOTE_RELEASED:+(发布于 ${REMOTE_RELEASED})}" fi else warn "分发源没有 version.json,按未知版本安装(不影响功能)" fi } fetch_binaries() { fetch "panel-agent-${ARCH_TAG}" "${TMP}/panel-agent" fetch "panel-api-${ARCH_TAG}" "${TMP}/panel-api" # 校验和:有 SHA256SUMS 就强制比对,没有则明确提示(离线目录常见) if [[ -n "${FROM_DIR}" ]]; then SUMS="${FROM_DIR}/SHA256SUMS" else curl -fsSL --connect-timeout 15 --max-time 60 -o "${TMP}/SHA256SUMS" \ "${BASE_URL}/SHA256SUMS" 2>/dev/null || true SUMS="${TMP}/SHA256SUMS" fi if [[ -s "${SUMS}" ]]; then for pair in "panel-agent:panel-agent-${ARCH_TAG}" "panel-api:panel-api-${ARCH_TAG}"; do local_name="${pair%%:*}"; remote_name="${pair##*:}" want="$(awk -v f="${remote_name}" '$2==f || $2=="*"f {print $1}' "${SUMS}" | head -1)" [[ -n "${want}" ]] || die "SHA256SUMS 里没有 ${remote_name} 的记录" got="$(sha256sum "${TMP}/${local_name}" | awk '{print $1}')" [[ "${want}" == "${got}" ]] || die "${local_name} 校验失败:期望 ${want},实际 ${got}" ok "校验 ${local_name} 通过" done else warn "没有 SHA256SUMS,跳过完整性校验(内网离线安装可接受,公网安装请修好分发源)" fi } # ── --check:只验不改 ─────────────────────────────────── # # 「装上去了但版本不对」「下载到一半断了」这类问题,在动 systemd 之前 # 就应该被发现。这个模式跑完环境预检、下载、校验就退出,不建用户、 # 不写 /opt、不碰 nginx。 if [[ ${CHECK} -eq 1 ]]; then step "检查分发源(--check:不改动系统)" read_remote_version fetch_binaries printf '\n%s检查通过%s\n' "$C_OK" "$C_0" printf ' 版本 %s\n' "${REMOTE_VERSION:-未知}" printf ' 架构 %s → %s\n' "${ARCH}" "${ARCH_TAG}" printf ' 来源 %s\n' "${FROM_DIR:-${BASE_URL}}" printf ' 本机状态 %s\n' "$(if [[ -d "${PREFIX}/bin" ]]; then echo "已安装(可用 --upgrade 升级)"; else echo "未安装"; fi)" printf '\n正式安装就去掉 --check 重跑一次。\n\n' exit 0 fi # ── 账号与目录 ────────────────────────────────────────── step "创建运行账号与目录" getent group "${SERVICE_USER}" >/dev/null || groupadd --system "${SERVICE_USER}" id "${SERVICE_USER}" >/dev/null 2>&1 || \ useradd --system --gid "${SERVICE_USER}" --no-create-home --shell /usr/sbin/nologin "${SERVICE_USER}" ok "账号 ${SERVICE_USER}" # /opt/panel 必须 0755:panel-api 以 panel 用户运行, # 目录 0750 root:root 时连 exec 都进不去,systemd 报 203/EXEC。 install -d -m 0755 "${PREFIX}" "${PREFIX}/bin" install -d -m 0750 -o "${SERVICE_USER}" -g "${SERVICE_USER}" "${DATA_DIR}" install -d -m 0750 -o root -g "${SERVICE_USER}" "${CONF_DIR}" install -d -m 0750 -o root -g "${SERVICE_USER}" "${RUN_DIR}" install -d -m 0755 "${PREFIX}/certs" "${PREFIX}/snapshots" "${PREFIX}/acme" install -d -m 0750 -o "${SERVICE_USER}" -g "${SERVICE_USER}" "${DATA_DIR}/stage" "${DATA_DIR}/backups" ok "目录就绪" # ── 安装程序文件 ──────────────────────────────────────── step "安装程序文件" read_remote_version fetch_binaries install -m 0755 "${TMP}/panel-agent" "${PREFIX}/bin/panel-agent" install -m 0755 "${TMP}/panel-api" "${PREFIX}/bin/panel-api" ok "已安装到 ${PREFIX}/bin" # 版本落盘:下次 --upgrade 时就是靠这个文件说出"从哪个版本到哪个版本"。 # 拿不到远端版本时保留旧的,别把已知信息覆盖成空。 if [[ -n "${REMOTE_VERSION}" ]]; then printf '%s\n' "${REMOTE_VERSION}" > "${PREFIX}/VERSION" if [[ "${MODE}" == "upgrade" ]]; then if [[ -z "${OLD_VERSION}" ]]; then ok "升级到 ${REMOTE_VERSION}(此前版本未知)" elif [[ "${OLD_VERSION}" == "${REMOTE_VERSION}" ]]; then ok "已是 ${REMOTE_VERSION},重新安装一遍(用于修复损坏的二进制)" else ok "从 ${OLD_VERSION} 升级到 ${REMOTE_VERSION}" fi fi fi # ── 签名密钥 ──────────────────────────────────────────── step "签名密钥" # 密钥要在升级时保留:换了密钥,所有已签发的会话与 IPC 请求全部失效, # 用户会被强制踢下线,而且 agent 与 api 会对不上。 if [[ ! -f "${PREFIX}/secret.key" ]]; then umask 077 head -c 32 /dev/urandom | base64 | tr -d '\n' > "${PREFIX}/secret.key" chown root:"${SERVICE_USER}" "${PREFIX}/secret.key" chmod 640 "${PREFIX}/secret.key" ok "已生成 ${PREFIX}/secret.key" else ok "密钥已存在,保留" fi # ── 配置文件 ──────────────────────────────────────────── step "写入配置" if [[ -f "${CONF_DIR}/agent.conf" && "${MODE}" == "upgrade" ]]; then ok "保留现有 agent.conf" else cat > "${CONF_DIR}/agent.conf" < "${CONF_DIR}/api.conf" < "${CONF_DIR}/brand.conf" < "/etc/systemd/system/${name}.service" </dev/null 2>&1 || true systemctl restart panel-agent.service || die "panel-agent 启动失败,查看 journalctl -u panel-agent -n 50" sleep 1 systemctl enable panel-api.service >/dev/null 2>&1 || true systemctl restart panel-api.service || die "panel-api 启动失败,查看 journalctl -u panel-api -n 50" # 等健康检查:二进制能起但接口不通(库迁移失败、密钥读不到)是最常见的"装上了打不开" HEALTH=0 for _ in $(seq 1 30); do if curl -fsS --max-time 2 http://127.0.0.1:8899/api/health >/dev/null 2>&1; then HEALTH=1 break fi sleep 1 done [[ ${HEALTH} -eq 1 ]] || die "服务已启动但健康检查未通过,查看 journalctl -u panel-api -n 50" ok "panel-agent / panel-api 运行中" # ── nginx 入口 ────────────────────────────────────────── step "配置 nginx 入口" PANEL_SCHEME="http" PANEL_HOST="127.0.0.1" if [[ ${USE_NGINX} -eq 0 ]]; then dim "已跳过(--no-nginx)。用 SSH 隧道访问:ssh -L 8899:127.0.0.1:8899 用户@本机" elif ! command -v nginx >/dev/null; then warn "没装 nginx,跳过入口配置。装好后重跑本脚本即可补上" else ENTRY="/etc/nginx/sites-available/panel.conf" if [[ -f "${ENTRY}" && "${MODE}" == "upgrade" ]]; then # 从现有配置推断 scheme:保留下来的入口可能已经是 https, # 打印成 http 会让用户以为地址写错了 if grep -qE '^[[:space:]]*listen[^#]*ssl' "${ENTRY}"; then PANEL_SCHEME="https" fi ok "保留现有 panel.conf" # 老版本的 panel.conf 里没有 WebSocket 转发。终端(0.2.0 起)依赖它, # 缺了的表现是「终端一直显示连接中」,而 nginx 的 access_log 里 # 全是 200 —— 看起来完全不像是配置问题。 # # 就地补上,而不是让用户自己去改 nginx:升级本来就不该要求 # 用户做运维动作,而且绝大多数人升级完根本不会想到去看配置。 # 先定位真正放着 proxy 指令的文件:它可能在主配置里,也可能被 # include 进了一个 snippet(location 里只剩一行 include)。 # # 这一步必须先做。判定「补过没有」一定要落在真正要改的那个文件上 —— # 拿主配置判断、往 snippet 里写,结果是每次升级都多插一遍, # 而 nginx 对同名 proxy_set_header 是后者覆盖前者,功能看起来 # 完全正常,配置却越升越长。 _target="" if grep -q 'proxy_buffering off;' "${ENTRY}"; then _target="${ENTRY}" else _inc="$(sed -n 's/^[[:space:]]*include[[:space:]]\{1,\}\([^;]*\);.*/\1/p' "${ENTRY}" | head -1)" [[ -n "${_inc}" && "${_inc}" != /* ]] && _inc="/etc/nginx/${_inc}" if [[ -n "${_inc}" && -f "${_inc}" ]] && grep -q 'proxy_buffering off;' "${_inc}"; then _target="${_inc}" fi fi if [[ -n "${_target}" ]] && ! grep -q 'proxy_set_header Upgrade' "${_target}"; then cp -a "${_target}" "${_target}.bak-$(date +%Y%m%d%H%M%S)" awk ' !done && /proxy_buffering off;/ { print "proxy_set_header Upgrade $http_upgrade;" print "proxy_set_header Connection \"upgrade\";" done = 1 } { print } ' "${_target}" > "${_target}.new" && mv "${_target}.new" "${_target}" # 终端长时间没有输出是常态(看日志、开 vim), # 300 秒的空闲超时会把好好的会话掐断 sed -i 's/proxy_read_timeout 300s;/proxy_read_timeout 3600s;/' "${_target}" ok "已为 ${_target} 补上 WebSocket 转发(终端需要)" elif [[ -z "${_target}" ]]; then warn "现有 panel.conf 结构不像本脚本生成的,未自动补 WebSocket 转发。" warn "若终端一直停在「连接中」,请手动在 location / 里加:" warn " proxy_set_header Upgrade \$http_upgrade;" warn " proxy_set_header Connection \"upgrade\";" fi else # 自签证书:面板是管理入口,明文 HTTP 会把口令暴露在链路上。 # 有域名就让用户后续用面板自己的 ACME 换正式证书。 CERT_DIR="${PREFIX}/certs/_self" if [[ ! -f "${CERT_DIR}/fullchain.pem" ]]; then install -d -m 0750 "${CERT_DIR}" CN="${PANEL_DOMAIN:-$(hostname -f 2>/dev/null || hostname)}" openssl req -x509 -nodes -newkey rsa:2048 -days 3650 \ -keyout "${CERT_DIR}/privkey.pem" -out "${CERT_DIR}/fullchain.pem" \ -subj "/CN=${CN}" \ -addext "subjectAltName=DNS:${CN},IP:$(hostname -I | awk '{print $1}')" >/dev/null 2>&1 \ || warn "自签证书生成失败,将退回 HTTP 入口" chmod 0640 "${CERT_DIR}/privkey.pem" fi ACL="" if [[ -n "${ALLOW_CIDRS}" ]]; then ACL=$'\n # 来源白名单(--allow-cidrs)。nginx 这一层挡在前面,\n # 面板里还配了同一份 allow_cidrs,两层都生效。' IFS=',' read -ra _cidrs <<< "${ALLOW_CIDRS}" for c in "${_cidrs[@]}"; do c="$(echo "${c}" | xargs)" [[ -n "${c}" ]] && ACL+=$'\n'" allow ${c};" done ACL+=$'\n deny all;' fi SERVER_NAME="${PANEL_DOMAIN:-_}" if [[ -f "${CERT_DIR}/fullchain.pem" ]]; then PANEL_SCHEME="https" cat > "${ENTRY}" < "${ENTRY}" </dev/null 2>&1; then systemctl reload nginx 2>/dev/null || systemctl start nginx ok "nginx 已重载" else warn "nginx -t 未通过,入口未生效。手动检查:nginx -t" fi fi # ── 收尾 ──────────────────────────────────────────────── IP="$(hostname -I 2>/dev/null | awk '{print $1}')" [[ -n "${IP}" ]] || IP="<服务器IP>" printf '\n%s━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━%s\n' "$C_OK" "$C_0" printf '%s安装完成%s\n\n' "$C_B" "$C_0" if [[ ${USE_NGINX} -eq 1 ]] && command -v nginx >/dev/null; then if [[ -n "${PANEL_DOMAIN}" ]]; then printf ' 访问地址 %s://%s:%s\n' "${PANEL_SCHEME}" "${PANEL_DOMAIN}" "${PANEL_PORT}" else printf ' 访问地址 %s://%s:%s\n' "${PANEL_SCHEME}" "${IP}" "${PANEL_PORT}" fi if [[ "${PANEL_SCHEME}" == "https" ]]; then dim "证书是自签的,浏览器会警告一次;绑定域名后可在面板里申请正式证书" fi # 内网地址这条提示与 scheme 无关:http 入口同样会遇到 if [[ "${IP}" == 10.* || "${IP}" == 192.168.* || "${IP}" == 172.1[6-9].* || "${IP}" == 172.2[0-9].* || "${IP}" == 172.3[01].* ]]; then dim "上面是内网地址;云服务器请用公网 IP 或绑定域名访问" fi else printf ' 访问方式 ssh -L 8899:127.0.0.1:8899 用户@%s\n' "${IP}" dim "然后本地浏览器打开 http://127.0.0.1:8899" fi printf ' 初始密码 ' PW_FILE="${DATA_DIR}/initial.password" if [[ "${MODE}" == "upgrade" ]]; then # 升级不展示密码:文件可能是几天前生成的,用户多半已经改过。 # 打出来只会让人以为「这次升级把我的密码重置成了这个」。 printf '未改动(沿用原账号)\n' dim "忘记密码时:见 ${PW_FILE};该文件已不存在则说明已改过密码" elif [[ -f "${PW_FILE}" ]]; then # 文件是两行(username: xxx / password: xxx),只取密码那行; # 取 head -1 会把 "username: admin" 当成密码打出来 PW_VALUE="$(grep -iE '^[[:space:]]*password[[:space:]]*:' "${PW_FILE}" | head -1 | sed 's/^[^:]*:[[:space:]]*//')" [[ -n "${PW_VALUE}" ]] || PW_VALUE="$(head -1 "${PW_FILE}")" printf '%s(账号 admin)\n' "${PW_VALUE}" dim "首次登录后请立即改密,然后删除 ${PW_FILE}" else printf '已初始化过(没有新的初始密码文件)\n' dim "忘记密码时:停掉 panel-api,删除 ${DATA_DIR}/panel.db 再启动会重新生成 admin 与初始密码(会丢面板配置,站点不受影响)" fi printf ' 查看日志 journalctl -u panel-api -f\n' # 管道执行时 $0 就是 "bash",直接拼进去会变成 "sudo bash bash --uninstall" case "$(basename "${0:-}")" in bash|sh|dash) printf ' 卸载 curl -fsSL %s/install.sh | sudo bash -s -- --uninstall\n' "${BASE_URL}" ;; *) printf ' 卸载 sudo bash %s --uninstall\n' "$0" ;; esac printf '%s━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━%s\n\n' "$C_OK" "$C_0"