scripts/startup-guard.mjs + scripts/lib/startup-guard-core.mjs(新增): - 被控端启动连续失败 N 次(默认 3)自动从 LKG 指纹匹配的备份恢复入口脚本 - 30s 启动宽限期 + 端口探活,避免计划内重启与 supervisor 重启撞车被误计数 - 无 LKG / 无指纹匹配只记日志放行,绝不擦除 install-dir - 恢复成功或服务已就绪均归零失败计数;被控端启动成功后 resetStartupGuardState 归零 (apps/agent/src/index.ts 为本任务对 agent 的唯一改动点) 三端 supervisor 接入: - Windows install.ps1:计划任务包一层 guard,RestartCount 3→10(保证阈值可达) - Linux install-linux.sh:systemd unit 的 ExecStart 包一层 guard - macOS launchd:无安装器,docs/DEVELOPMENT.md §7.4 给出完整接法 docs:DELIVERY-PIPELINE §6 由「不做自动回滚」收窄为 「手动 rollback + 看门狗自动恢复 + 边界明示」,附录 C 同步; DEVELOPMENT.md 新增 §7.4 三端 supervisor 接入说明。
429 lines
17 KiB
Bash
Executable File
429 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# nodeagent 被控端 —— Linux 安装器(Debian 12 / Ubuntu / NAS 系统等 systemd 系统)
|
||
#
|
||
# 用法:
|
||
# sudo bash scripts/install-linux.sh # 系统级(/opt/nodeagent + 系统 systemd)
|
||
# bash scripts/install-linux.sh --user # 用户级(~/.local/share + user systemd + linger)
|
||
# sudo bash scripts/install-linux.sh --node-id nas-01 --port 8765
|
||
# sudo bash scripts/install-linux.sh --no-service # 只装程序+配置,不注册 systemd
|
||
# sudo bash scripts/install-linux.sh --uninstall # 卸载(保留配置)
|
||
# sudo bash scripts/install-linux.sh --uninstall --purge # 卸载并删配置
|
||
#
|
||
# 这套脚本要解决的实际问题(与 Windows 的 install.ps1 同语义):
|
||
# 1) 目标机未必有 Node → 自带运行时(下载 linux-<arch> 到安装目录)
|
||
# 2) 要开机自启 → 用 systemd(对齐 Windows 的「计划任务」角色)
|
||
# 3) 密钥要生成/复用 → 写进 <数据目录>/agent.json,并在屏幕上打印(供控制端连接)
|
||
# 4) 幂等 → 重复执行不丢配置、不产生重复服务
|
||
#
|
||
# ⚠️ 本平台没有 GUI 能力(截图/键鼠/窗口/剪贴板/音量均为 Windows/macOS 实现),
|
||
# 故 --allow-input 即使打开也只影响「是否允许输入类能力注册」,
|
||
# **在 Linux 上不会有实际输入能力可用**。默认关闭。
|
||
#
|
||
set -euo pipefail
|
||
|
||
# ---------------- 常量 ----------------
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||
|
||
NODE_VERSION="${NODEAGENT_NODE_VERSION:-v22.20.0}"
|
||
NODE_MIRROR="${NODEAGENT_NODE_MIRROR:-https://npmmirror.com/mirrors/node}"
|
||
DEFAULT_PORT=8765
|
||
DEFAULT_NODE_ID="$(hostname 2>/dev/null | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_-' || echo linux_01)"
|
||
[ -z "$DEFAULT_NODE_ID" ] && DEFAULT_NODE_ID="linux_01"
|
||
|
||
UNIT_NAME="nodeagent"
|
||
|
||
# ---------------- 输出 ----------------
|
||
|
||
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
||
C_OK=$'\033[32m'; C_WARN=$'\033[33m'; C_ERR=$'\033[31m'; C_DIM=$'\033[2m'; C_RST=$'\033[0m'
|
||
else
|
||
C_OK=''; C_WARN=''; C_ERR=''; C_DIM=''; C_RST=''
|
||
fi
|
||
ok() { printf '%s✓%s %s\n' "$C_OK" "$C_RST" "$*"; }
|
||
warn() { printf '%s!%s %s\n' "$C_WARN" "$C_RST" "$*"; }
|
||
err() { printf '%s✗%s %s\n' "$C_ERR" "$C_RST" "$*" >&2; }
|
||
info() { printf '%s·%s %s\n' "$C_DIM" "$C_RST" "$*"; }
|
||
|
||
die() { err "$*"; exit 1; }
|
||
|
||
# ---------------- 参数 ----------------
|
||
|
||
MODE="system" # system | user
|
||
PORT="$DEFAULT_PORT"
|
||
NODE_ID="$DEFAULT_NODE_ID"
|
||
ALLOW_INPUT=0
|
||
UNINSTALL=0
|
||
PURGE=0
|
||
NO_SERVICE=0
|
||
BIND_HOST="0.0.0.0"
|
||
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--user) MODE="user"; shift ;;
|
||
--system) MODE="system"; shift ;;
|
||
--port) PORT="${2:-}"; shift 2 ;;
|
||
--node-id) NODE_ID="${2:-}"; shift 2 ;;
|
||
--allow-input) ALLOW_INPUT=1; shift ;;
|
||
--host) BIND_HOST="${2:-}"; shift 2 ;;
|
||
--no-service) NO_SERVICE=1; shift ;;
|
||
--uninstall) UNINSTALL=1; shift ;;
|
||
--purge) PURGE=1; shift ;;
|
||
-h|--help)
|
||
sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
|
||
exit 0 ;;
|
||
*) die "未知参数:$1(用 --help 查看用法)" ;;
|
||
esac
|
||
done
|
||
|
||
case "$PORT" in
|
||
''|*[!0-9]*) die "--port 必须是数字,收到:$PORT" ;;
|
||
esac
|
||
[ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ] || die "--port 需在 1-65535,收到:$PORT"
|
||
|
||
# ---------------- 路径规划 ----------------
|
||
|
||
ARCH_RAW="$(uname -m)"
|
||
case "$ARCH_RAW" in
|
||
x86_64|amd64) NODE_ARCH="x64" ;;
|
||
aarch64|arm64) NODE_ARCH="arm64" ;;
|
||
*) die "不支持的架构:$ARCH_RAW(仅 x86_64 / aarch64)" ;;
|
||
esac
|
||
|
||
if [ "$MODE" = "user" ]; then
|
||
INSTALL_DIR="${NODEAGENT_INSTALL_DIR:-$HOME/.local/share/nodeagent}"
|
||
UNIT_DIR="$HOME/.config/systemd/user"
|
||
DATA_DIR="${NODEAGENT_HOME:-$HOME/.nodeagent}"
|
||
SUDO=""
|
||
else
|
||
INSTALL_DIR="${NODEAGENT_INSTALL_DIR:-/opt/nodeagent}"
|
||
UNIT_DIR="/etc/systemd/system"
|
||
DATA_DIR="${NODEAGENT_HOME:-$HOME/.nodeagent}" # 数据目录始终在「执行安装的用户」家目录下
|
||
[ "$(id -u)" -eq 0 ] || die "系统级安装需要 root:请用 sudo,或改用 --user"
|
||
SUDO=""
|
||
fi
|
||
|
||
AGENT_JS_SRC="$PROJECT_ROOT/release/nodeagent-linux/agent.mjs"
|
||
[ -f "$AGENT_JS_SRC" ] || AGENT_JS_SRC="$PROJECT_ROOT/release/agent.mjs"
|
||
[ -f "$AGENT_JS_SRC" ] || die "找不到 agent.mjs —— 请先执行:node scripts/pack-linux.mjs(或 pnpm pack:linux)"
|
||
|
||
# ---------------- 卸载 ----------------
|
||
|
||
if [ "$UNINSTALL" -eq 1 ]; then
|
||
echo "=== 卸载 nodeagent ==="
|
||
if [ "$MODE" = "user" ]; then
|
||
systemctl --user disable --now "$UNIT_NAME" >/dev/null 2>&1 || true
|
||
rm -f "$UNIT_DIR/$UNIT_NAME.service"
|
||
systemctl --user daemon-reload >/dev/null 2>&1 || true
|
||
else
|
||
systemctl disable --now "$UNIT_NAME" >/dev/null 2>&1 || true
|
||
rm -f "$UNIT_DIR/$UNIT_NAME.service"
|
||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||
fi
|
||
rm -rf "$INSTALL_DIR"
|
||
ok "已移除服务与程序目录:$INSTALL_DIR"
|
||
if [ "$PURGE" -eq 1 ]; then
|
||
rm -rf "$DATA_DIR"
|
||
ok "已删除数据目录(含密钥与审计日志):$DATA_DIR"
|
||
else
|
||
info "已保留数据目录:$DATA_DIR(含密钥与审计日志;加 --purge 可一并删除)"
|
||
fi
|
||
exit 0
|
||
fi
|
||
|
||
# ---------------- 环境检查 ----------------
|
||
|
||
echo "=== nodeagent 被控端安装(Linux · $MODE 模式)==="
|
||
echo ""
|
||
|
||
[ "$(uname -s)" = "Linux" ] || die "本脚本仅用于 Linux(当前:$(uname -s))"
|
||
|
||
if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then
|
||
ok "systemd 可用"
|
||
elif [ "$NO_SERVICE" -eq 1 ]; then
|
||
warn "未检测到 systemd,但已指定 --no-service → 只装程序与配置,不注册服务"
|
||
MODE_LABEL="no-service"
|
||
else
|
||
die "未检测到 systemd(本脚本用 systemd 做开机自启)。
|
||
可选项:
|
||
a) 加 --no-service 只装程序与配置,自启由你另行安排(手动/nohup/supervisor/cron @reboot)
|
||
b) 手工启动:NODEAGENT_HOME=$DATA_DIR $INSTALL_DIR/runtime/bin/node $INSTALL_DIR/agent.mjs"
|
||
fi
|
||
|
||
# 发行版信息(部分 NAS 系统基于 Debian 12,此处只作展示)
|
||
if [ -r /etc/os-release ]; then
|
||
# shellcheck disable=SC1091
|
||
. /etc/os-release
|
||
info "系统:${PRETTY_NAME:-unknown}(内核 $(uname -r))"
|
||
fi
|
||
ok "架构:$ARCH_RAW → node-$NODE_ARCH"
|
||
|
||
# ---------------- Node 运行时 ----------------
|
||
|
||
RUNTIME_DIR="$INSTALL_DIR/runtime"
|
||
NODE_BIN=""
|
||
|
||
# ① 系统 node ≥ 22 就直接用(省一次下载;NAS 上通常没有,走 ②)
|
||
if command -v node >/dev/null 2>&1; then
|
||
SYS_VER="$(node -v 2>/dev/null || echo '')"
|
||
SYS_MAJOR="${SYS_VER#v}"; SYS_MAJOR="${SYS_MAJOR%%.*}"
|
||
if [ -n "$SYS_MAJOR" ] && [ "$SYS_MAJOR" -ge 22 ] 2>/dev/null; then
|
||
NODE_BIN="$(command -v node)"
|
||
ok "使用系统 Node:$SYS_VER($NODE_BIN)"
|
||
else
|
||
info "系统 Node 版本过低(${SYS_VER:-无}),改用自带运行时"
|
||
fi
|
||
fi
|
||
|
||
if [ -z "$NODE_BIN" ]; then
|
||
if [ -x "$RUNTIME_DIR/bin/node" ]; then
|
||
NODE_BIN="$RUNTIME_DIR/bin/node"
|
||
ok "复用已下载的运行时:$("$NODE_BIN" -v)"
|
||
else
|
||
TARBALL="node-$NODE_VERSION-linux-$NODE_ARCH.tar.xz"
|
||
URL="$NODE_MIRROR/$NODE_VERSION/$TARBALL"
|
||
TMPDIR_DL="$(mktemp -d)"
|
||
trap 'rm -rf "$TMPDIR_DL"' EXIT
|
||
info "下载 Node 运行时:$URL"
|
||
if command -v curl >/dev/null 2>&1; then
|
||
curl -fsSL --retry 3 --connect-timeout 20 -o "$TMPDIR_DL/$TARBALL" "$URL" \
|
||
|| die "下载失败(可设 NODEAGENT_NODE_MIRROR 换源,或预先把 tar.xz 放到 $TMPDIR_DL)"
|
||
elif command -v wget >/dev/null 2>&1; then
|
||
wget -q -T 20 -t 3 -O "$TMPDIR_DL/$TARBALL" "$URL" || die "下载失败:$URL"
|
||
else
|
||
die "需要 curl 或 wget 之一来下载 Node 运行时"
|
||
fi
|
||
mkdir -p "$INSTALL_DIR"
|
||
info "解压到 $RUNTIME_DIR"
|
||
mkdir -p "$RUNTIME_DIR"
|
||
tar -xJf "$TMPDIR_DL/$TARBALL" -C "$RUNTIME_DIR" --strip-components=1 || die "解压失败:$TARBALL"
|
||
NODE_BIN="$RUNTIME_DIR/bin/node"
|
||
ok "运行时就绪:$("$NODE_BIN" -v)"
|
||
fi
|
||
fi
|
||
|
||
"$NODE_BIN" -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 22 ? 0 : 1)' \
|
||
|| die "Node 版本需 ≥ 22(当前 $("$NODE_BIN" -v))"
|
||
|
||
# ---------------- 安装程序文件 ----------------
|
||
|
||
mkdir -p "$INSTALL_DIR" "$DATA_DIR"
|
||
install -m 0644 "$AGENT_JS_SRC" "$INSTALL_DIR/agent.mjs"
|
||
ok "已安装:$INSTALL_DIR/agent.mjs($(wc -c < "$INSTALL_DIR/agent.mjs" | tr -d ' ') 字节)"
|
||
|
||
# ---------------- 看门狗(v2.1.0,决策 R-1) ----------------
|
||
#
|
||
# 启动连续失败 N 次自动从 LKG 恢复入口文件。
|
||
# 为什么需要:nodeagent rollback 是 RPC 命令,被控端「完全起不来」时无监听可连,
|
||
# CLI 救不了(ARCH A-P0-6);看门狗跑在 supervisor 链上,不要求被控端可达。
|
||
# ⚠️ 两个文件必须保持相对位置一起安装:guard 同目录的 lib/ 下是它的核心纯函数。
|
||
GUARD_MJS="$PROJECT_ROOT/scripts/startup-guard.mjs"
|
||
GUARD_LIB="$PROJECT_ROOT/scripts/lib/startup-guard-core.mjs"
|
||
if [ -f "$GUARD_MJS" ] && [ -f "$GUARD_LIB" ]; then
|
||
mkdir -p "$INSTALL_DIR/scripts/lib"
|
||
install -m 0644 "$GUARD_MJS" "$INSTALL_DIR/scripts/startup-guard.mjs"
|
||
install -m 0644 "$GUARD_LIB" "$INSTALL_DIR/scripts/lib/startup-guard-core.mjs"
|
||
# 被看守的是「完整启动命令」(-- 之后原样 spawn):node agent.mjs
|
||
AGENT_CMD="$NODE_BIN $INSTALL_DIR/scripts/startup-guard.mjs --data-dir $DATA_DIR --lkg-dir $INSTALL_DIR --install-dir $INSTALL_DIR --threshold 3 -- $NODE_BIN $INSTALL_DIR/agent.mjs"
|
||
ok "看门狗就绪:启动连续失败 3 次自动从 LKG 恢复(日志 $DATA_DIR/logs/startup-guard.log)"
|
||
else
|
||
AGENT_CMD="$NODE_BIN $INSTALL_DIR/agent.mjs"
|
||
warn "未找到看门狗脚本($GUARD_MJS)—— 按原样直启 agent(无自动恢复)"
|
||
fi
|
||
|
||
# ---------------- 配置:复用既有密钥,否则生成 ----------------
|
||
|
||
CFG="$DATA_DIR/agent.json"
|
||
KEY=""
|
||
if [ -f "$CFG" ]; then
|
||
# 备份一次(保留最近一次即可,避免堆积)
|
||
cp -f "$CFG" "$CFG.bak" 2>/dev/null || true
|
||
KEY="$(sed -n 's/.*"key"[[:space:]]*:[[:space:]]*"\([0-9a-fA-F]\{16,\}\)".*/\1/p' "$CFG" | head -1)"
|
||
if [ -n "$KEY" ]; then
|
||
ok "复用既有配置:$CFG(已备份为 agent.json.bak)"
|
||
else
|
||
info "既有配置里没有可用密钥,将生成新密钥"
|
||
fi
|
||
fi
|
||
|
||
if [ -z "$KEY" ]; then
|
||
if command -v openssl >/dev/null 2>&1; then
|
||
KEY="$(openssl rand -hex 32)"
|
||
else
|
||
KEY="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')"
|
||
fi
|
||
case "$KEY" in
|
||
*[!0-9a-fA-F]*|'') die "密钥生成异常(非 hex)" ;;
|
||
esac
|
||
[ ${#KEY} -ge 32 ] || die "密钥长度不足(${#KEY})"
|
||
fi
|
||
|
||
cat > "$CFG" <<JSON
|
||
{
|
||
"node_id": "$NODE_ID",
|
||
"host": "$BIND_HOST",
|
||
"port": $PORT,
|
||
"tls": true,
|
||
"key": "$KEY",
|
||
"log_level": "info",
|
||
"allow_input": $([ "$ALLOW_INPUT" -eq 1 ] && echo true || echo false),
|
||
"auth_mode": "psk"
|
||
}
|
||
JSON
|
||
chmod 600 "$CFG"
|
||
ok "配置已写入:$CFG(权限 600)"
|
||
[ "$ALLOW_INPUT" -eq 1 ] && warn "已开启 allow_input —— 但 **Linux 上并无输入类能力实现**,此项仅作配置留存"
|
||
|
||
# ---------------- systemd 服务 ----------------
|
||
|
||
if [ "$NO_SERVICE" -eq 1 ]; then
|
||
info "跳过 systemd 注册(--no-service)"
|
||
else
|
||
if [ "$MODE" = "user" ]; then
|
||
mkdir -p "$UNIT_DIR"
|
||
UNIT_PATH="$UNIT_DIR/$UNIT_NAME.service"
|
||
else
|
||
UNIT_PATH="$UNIT_DIR/$UNIT_NAME.service"
|
||
fi
|
||
|
||
cat > "$UNIT_PATH" <<UNIT
|
||
[Unit]
|
||
Description=nodeagent 被控端(跨机 AI 接管)
|
||
Documentation=https://github.com/jkooe/nodeagent
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
# 数据目录(含密钥与审计日志)—— 与 agent 侧 agentDir() 一致
|
||
Environment=NODEAGENT_HOME=$DATA_DIR
|
||
# v2.1.0:ExecStart 包一层看门狗($AGENT_CMD,见上文「看门狗」节)。
|
||
# 崩溃自拉起由 Restart=always 负责;看门狗负责「连续失败 N 次从 LKG 恢复」。
|
||
ExecStart=$AGENT_CMD
|
||
WorkingDirectory=$INSTALL_DIR
|
||
# 崩溃自拉起(对齐 Windows 计划任务的重启语义)
|
||
Restart=always
|
||
RestartSec=3
|
||
# 优雅停止:先 SIGTERM(agent 会关掉监听与审计写入),超时再 SIGKILL。
|
||
# 看门狗收到 SIGTERM 会转发给子进程后退出,不会被误记成「启动失败」。
|
||
KillSignal=SIGTERM
|
||
TimeoutStopSec=15
|
||
# 日志进 journald:journalctl -u $UNIT_NAME -f
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
# 退出码 0 视为「非异常」(如 --uninstall 后的正常停止)
|
||
SuccessExitStatus=0 143
|
||
|
||
[Install]
|
||
WantedBy=$([ "$MODE" = "user" ] && echo default.target || echo multi-user.target)
|
||
UNIT
|
||
|
||
ok "已写入服务单元:$UNIT_PATH"
|
||
|
||
if [ "$MODE" = "user" ]; then
|
||
systemctl --user daemon-reload
|
||
systemctl --user enable --now "$UNIT_NAME" || die "启动失败,查看:systemctl --user status $UNIT_NAME"
|
||
# 让用户级服务在「未登录」时也能跑(NAS 重启后无需登录即自启)
|
||
if command -v loginctl >/dev/null 2>&1; then
|
||
if loginctl enable-linger "$USER" 2>/dev/null; then
|
||
ok "已启用 linger(未登录也自启)"
|
||
else
|
||
warn "未能启用 linger(可能需要 root 执行一次:loginctl enable-linger $USER)"
|
||
warn " 未启用 linger 时,用户级服务只在登录会话中运行"
|
||
fi
|
||
fi
|
||
else
|
||
systemctl daemon-reload
|
||
systemctl enable --now "$UNIT_NAME" || die "启动失败,查看:systemctl status $UNIT_NAME -l"
|
||
fi
|
||
|
||
fi # end: NO_SERVICE
|
||
|
||
# ---------------- 启动确认 ----------------
|
||
|
||
if [ "$NO_SERVICE" -eq 1 ]; then
|
||
echo ""
|
||
warn "未注册服务 —— 手工启动:"
|
||
printf ' NODEAGENT_HOME=%s %s %s/agent.mjs\n' "$DATA_DIR" "$NODE_BIN" "$INSTALL_DIR"
|
||
MODE_LABEL="no-service"
|
||
else
|
||
sleep 2
|
||
STATE="$(systemctl is-active "$UNIT_NAME" 2>/dev/null || true)"
|
||
[ "$MODE" = "user" ] && STATE="$(systemctl --user is-active "$UNIT_NAME" 2>/dev/null || true)"
|
||
|
||
if [ "$STATE" = "active" ]; then
|
||
ok "服务运行中($UNIT_NAME)"
|
||
else
|
||
warn "服务状态:${STATE:-unknown}"
|
||
warn "查看日志:$([ "$MODE" = "user" ] && echo 'journalctl --user -u' || echo 'journalctl -u') $UNIT_NAME -n 50 --no-pager"
|
||
fi
|
||
fi # end: NO_SERVICE 启动确认
|
||
|
||
# 端口监听检查(不强依赖 ss/netstat)
|
||
if command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | grep -q ":$PORT "; then
|
||
ok "端口 $PORT 已监听"
|
||
else
|
||
info "端口 $PORT 监听状态未确认(可用 ss -ltnp | grep $PORT 自查)"
|
||
fi
|
||
|
||
# ---------------- 打印连接信息 ----------------
|
||
|
||
# ⚠️ 取本机 IP 要容错:`ip` 在精简系统(含部分 NAS 系统)上可能不存在,
|
||
# 而 `set -e` 下「命令替换失败」会**直接终止脚本**(且 2>/dev/null 把原因吞了,
|
||
# 表现为「脚本跑到这里就没了」——2026-10-13 在 Debian 容器里踩到)。
|
||
LAN_IP=""
|
||
if command -v ip >/dev/null 2>&1; then
|
||
LAN_IP="$(ip -4 route get 1.1.1.1 2>/dev/null | sed -n 's/.* src \([0-9.]*\).*/\1/p' | head -1)" || true
|
||
fi
|
||
if [ -z "$LAN_IP" ]; then
|
||
LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')" || true
|
||
fi
|
||
if [ -z "$LAN_IP" ] && command -v ifconfig >/dev/null 2>&1; then
|
||
LAN_IP="$(ifconfig 2>/dev/null | sed -n 's/.*inet \(addr:\)\?\([0-9.]*\).*/\2/p' | grep -v '^127\.' | head -1)" || true
|
||
fi
|
||
[ -z "$LAN_IP" ] && LAN_IP="<本机IP>"
|
||
|
||
echo ""
|
||
echo "══════════════════════════════════════════════════════"
|
||
echo " nodeagent 被控端已就绪"
|
||
echo "──────────────────────────────────────────────────────"
|
||
printf ' 节点 ID : %s\n' "$NODE_ID"
|
||
printf ' 监听 : %s:%s (wss / TLS)\n' "$BIND_HOST" "$PORT"
|
||
printf ' 本机 IP : %s\n' "$LAN_IP"
|
||
printf ' 认证模式 : psk\n'
|
||
printf ' 输入控制 : %s\n' "$([ "$ALLOW_INPUT" -eq 1 ] && echo 已开启 || echo 已禁用(Linux 无 GUI 能力))"
|
||
printf ' 安装目录 : %s\n' "$INSTALL_DIR"
|
||
printf ' 数据目录 : %s\n' "$DATA_DIR"
|
||
if [ "$NO_SERVICE" -eq 1 ]; then
|
||
printf ' 服务 : 未注册(--no-service,需自行安排自启)\n'
|
||
else
|
||
printf ' 服务名 : %s(%s systemd)\n' "$UNIT_NAME" "$MODE"
|
||
fi
|
||
echo ""
|
||
echo " 密钥(请妥善保存,控制端连接需要):"
|
||
echo ""
|
||
printf ' %s\n' "$KEY"
|
||
echo ""
|
||
echo " 控制端连接命令:"
|
||
printf ' nodeagent connect %s --port %s --key %s --insecure\n' "$LAN_IP" "$PORT" "$KEY"
|
||
echo "══════════════════════════════════════════════════════"
|
||
echo ""
|
||
echo " 常用管理命令($MODE 级):"
|
||
if [ "$MODE" = "user" ]; then
|
||
echo " systemctl --user status $UNIT_NAME"
|
||
echo " journalctl --user -u $UNIT_NAME -f # 实时日志"
|
||
echo " systemctl --user restart $UNIT_NAME"
|
||
echo " bash $0 --user --uninstall # 卸载(保留配置)"
|
||
else
|
||
echo " systemctl status $UNIT_NAME"
|
||
echo " journalctl -u $UNIT_NAME -f # 实时日志"
|
||
echo " systemctl restart $UNIT_NAME"
|
||
echo " sudo bash $0 --uninstall # 卸载(保留配置)"
|
||
fi
|
||
echo ""
|
||
echo " 注意:审计链外部锚定建议尽快启用(把链头存到链外)——"
|
||
echo " node scripts/anchor-audit.mjs collect --node $NODE_ID"
|
||
echo ""
|