From 35bd340d02a8c31e6d62ce6017ed4f0c67bf31f5 Mon Sep 17 00:00:00 2001 From: yanyu Date: Wed, 8 Jul 2026 16:13:18 +0800 Subject: [PATCH] add tunnel-agent/install.sh (tunnel-agent-v0.1.0) --- tunnel-agent/install.sh | 152 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 tunnel-agent/install.sh diff --git a/tunnel-agent/install.sh b/tunnel-agent/install.sh new file mode 100644 index 0000000..9708a8b --- /dev/null +++ b/tunnel-agent/install.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# tunnel-agent 一键安装/升级(用户侧,Linux)。 +# +# curl -fsSL https://git.lingyu.org.cn/lingyu-note/releases/raw/branch/main/tunnel-agent/install.sh \ +# | sudo TUNNEL_TOKEN=<控制台生成的令牌> bash +# +# 行为:下载二进制(amd64/arm64,SHA256 校验)→ /opt/tunnel-agent;首次生成 +# config.yaml —— 自动探测本机已装的 miiot(:8677)/ cloud-mcp(:8550)并读取其 +# .env 里的 MCP token 一并声明;systemd 常驻,断线自动重连。 +# 再次运行 = 升级(config 保留;旧二进制备份,健康失败回滚)。 +# +# 可选:TUNNEL_SERVER 覆盖服务器地址(默认 wss://penna.pro/tunnel);VERSION 钉版本。 +set -euo pipefail + +APP="tunnel-agent" +GITEA="https://git.lingyu.org.cn" +REL_REPO="lingyu-note/releases" +APP_DIR="/opt/${APP}" +SERVICE="${APP}" +TUNNEL_SERVER="${TUNNEL_SERVER:-wss://penna.pro/tunnel}" +KEEP_BACKUPS=5 + +[[ "$(id -u)" == "0" ]] || { echo "❌ 请用 root / sudo 运行" >&2; exit 1; } +[[ "$(uname -s)" == "Linux" ]] || { echo "❌ 仅支持 Linux" >&2; exit 1; } +case "$(uname -m)" in + x86_64) ARCH=amd64 ;; + aarch64|arm64) ARCH=arm64 ;; + *) echo "❌ 不支持的架构: $(uname -m)" >&2; exit 1 ;; +esac +command -v curl >/dev/null || { echo "❌ 需要 curl" >&2; exit 1; } + +if [[ "${1:-}" == "--uninstall" ]]; then + systemctl disable --now "${SERVICE}" 2>/dev/null || true + rm -f "/etc/systemd/system/${SERVICE}.service"; systemctl daemon-reload + rm -rf "${APP_DIR}/bin" + echo "✓ 已卸载(${APP_DIR}/config.yaml 保留)" + exit 0 +fi + +# ---- 版本解析 ---- +if [[ -n "${VERSION:-}" ]]; then + VER="${VERSION#v}" +else + VER=$(curl -fsSL "${GITEA}/api/v1/repos/${REL_REPO}/releases?limit=50" \ + | python3 -c " +import json,sys +rels=[r for r in json.load(sys.stdin) if r['tag_name'].startswith('${APP}-v')] +print(rels[0]['tag_name'].removeprefix('${APP}-v') if rels else '')" 2>/dev/null) || VER="" + [[ -n "$VER" ]] || { echo "❌ 查询最新版本失败" >&2; exit 1; } +fi +TAG="${APP}-v${VER}" +PKG="${APP}_${VER}_linux_${ARCH}.tar.gz" +BASE="${GITEA}/${REL_REPO}/releases/download/${TAG}" + +echo "▶ 下载 ${PKG} ..." +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT +curl -fsSL -o "${TMP}/${PKG}" "${BASE}/${PKG}" +curl -fsSL -o "${TMP}/SHA256SUMS" "${BASE}/SHA256SUMS" +(cd "${TMP}" && grep " ${PKG}\$" SHA256SUMS | sha256sum -c - >/dev/null) \ + || { echo "❌ SHA256 校验失败" >&2; exit 1; } +tar -xzf "${TMP}/${PKG}" -C "${TMP}" + +mkdir -p "${APP_DIR}/bin/backup" + +# ---- 首次:生成 config.yaml(自动探测本机服务)---- +if [[ ! -f "${APP_DIR}/config.yaml" ]]; then + [[ -n "${TUNNEL_TOKEN:-}" ]] || { echo "❌ 首次安装需要 TUNNEL_TOKEN(控制台「自托管节点」页生成)" >&2; exit 1; } + echo "▶ 生成 ${APP_DIR}/config.yaml(自动探测本机服务)..." + { + echo "server: ${TUNNEL_SERVER}" + echo "token: ${TUNNEL_TOKEN}" + echo "services:" + found=0 + # miiot(:8677) + if curl -sf -o /dev/null --max-time 2 http://127.0.0.1:8677/healthz 2>/dev/null; then + tok=$(grep -E '^MIOT_MCP_AUTH_TOKEN=' /opt/miiot/.env 2>/dev/null | cut -d= -f2- || true) + echo " - svc: miiot" + echo " port: 8677" + [[ -n "$tok" ]] && echo " mcp_token: ${tok}" + echo " mcp_endpoints:" + echo " - { name: 小米设备, path: /mcp/miot-devices }" + echo " - { name: 小米场景, path: /mcp/miot-scenes }" + found=1 + echo " ✓ 探测到 miiot(:8677)" >&2 + fi + # cloud-mcp(:8550) + if curl -sf -o /dev/null --max-time 2 http://127.0.0.1:8550/healthz 2>/dev/null; then + tok=$(grep -E '^LYMCP_MCP_AUTH_TOKEN=' /opt/cloud-mcp/.env 2>/dev/null | cut -d= -f2- || true) + echo " - svc: cloud-mcp" + echo " port: 8550" + [[ -n "$tok" ]] && echo " mcp_token: ${tok}" + echo " mcp_endpoints:" + echo " - { name: 浏览器, path: /mcp/browser }" + echo " - { name: 天气, path: /mcp/weather }" + echo " - { name: 火车票, path: /mcp/train }" + echo " - { name: 地图, path: /mcp/maps }" + found=1 + echo " ✓ 探测到 cloud-mcp(:8550)" >&2 + fi + if [[ "$found" == "0" ]]; then + echo " # 未探测到本机服务 —— 按需手工声明后 systemctl restart ${SERVICE}" + echo " # - svc: miiot" + echo " # port: 8677" + echo " # mcp_token: " + echo " # mcp_endpoints:" + echo " # - { name: 小米设备, path: /mcp/miot-devices }" + echo " ⚠️ 未探测到 miiot/cloud-mcp,请编辑 ${APP_DIR}/config.yaml 手工声明" >&2 + fi + } > "${APP_DIR}/config.yaml" + chmod 600 "${APP_DIR}/config.yaml" +fi +if [[ ! -f "/etc/systemd/system/${SERVICE}.service" ]]; then + cat > "/etc/systemd/system/${SERVICE}.service" </dev/null +fi + +# ---- 替换二进制 + 重启(agent 无本地 HTTP,健康 = systemd active + 日志无 crash)---- +if [[ -f "${APP_DIR}/bin/${APP}" ]]; then + cp "${APP_DIR}/bin/${APP}" "${APP_DIR}/bin/backup/${APP}.$(date +%Y%m%d%H%M%S)" + ls -t "${APP_DIR}/bin/backup/" | tail -n +$((KEEP_BACKUPS+1)) | xargs -r -I{} rm -f "${APP_DIR}/bin/backup/{}" +fi +install -m 755 "${TMP}/${APP}" "${APP_DIR}/bin/${APP}" +echo "${VER}" > "${APP_DIR}/.version" + +systemctl restart "${SERVICE}" +sleep 3 +if ! systemctl is-active --quiet "${SERVICE}"; then + echo "✗ 服务未能启动,回滚" >&2 + journalctl -u "${SERVICE}" -n 20 --no-pager || true + latest=$(ls -t "${APP_DIR}/bin/backup/" 2>/dev/null | head -1 || true) + [[ -n "$latest" ]] && { cp "${APP_DIR}/bin/backup/${latest}" "${APP_DIR}/bin/${APP}"; systemctl restart "${SERVICE}"; } + exit 1 +fi +echo "✅ ${APP} v${VER} 运行中 — 稍后到控制台「自托管节点」查看在线状态与已同步工具" +echo " 日志:journalctl -u ${SERVICE} -f 配置:${APP_DIR}/config.yaml"