refactor: Hub二进制改为Docker构建时下载,优化部署流程与worker初始化

- Dockerfile: 移除COPY预编译二进制,改为构建时通过HUB_TAG从GitHub Release下载
- CI: 移除artifact上传/下载步骤,通过build-args传递Hub tag
- deploy.sh: 重构参数解析,支持--hub-tag指定版本,跟踪tag变化触发重建
- build.sh: 新增--image模式支持构建并推送Hub Docker镜像
- hub.rs: worker连接时同步所有节点在线状态,避免状态不一致
- proxy_nodes: 启动时主动建立Hub worker连接,消除懒连接窗口期
- docker-compose.yml: app镜像支持APP_IMAGE环境变量配置
- README: 更新部署文档,推荐本地构建方式
This commit is contained in:
fawney19
2026-03-02 11:24:14 +08:00
parent c9dbe7936b
commit 0564893c4f
12 changed files with 540 additions and 231 deletions

View File

@@ -32,6 +32,9 @@ ADMIN_PASSWORD=admin123456
# 应用端口(默认 8084 # 应用端口(默认 8084
# APP_PORT=8084 # APP_PORT=8084
# 生产部署镜像deploy.sh 会读取)
# APP_IMAGE=ghcr.io/fawney19/aether:latest
# Gunicorn Worker 数量(默认 2 # Gunicorn Worker 数量(默认 2
# Docker 部署下 Tunnel Hub 为容器内部固定服务,可安全使用多 worker。 # Docker 部署下 Tunnel Hub 为容器内部固定服务,可安全使用多 worker。
# 非 Docker 运行时若使用 ProxyNode tunnel建议设置为 1。 # 非 Docker 运行时若使用 ProxyNode tunnel建议设置为 1。

View File

@@ -179,20 +179,24 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
outputs:
hub_tag: ${{ steps.hub-tag.outputs.tag }}
steps: steps:
- name: Get latest hub release tag - name: Get latest hub release tag
id: hub-tag id: hub-tag
run: | run: |
TAG=$(curl -sL "https://api.github.com/repos/${{ env.GITHUB_REPO }}/releases" | \ TAG=$(
python3 -c " curl -sL "https://api.github.com/repos/${{ env.GITHUB_REPO }}/releases" | \
import json, sys python3 - <<'PY'
releases = json.load(sys.stdin) import json, sys
for r in releases: releases = json.load(sys.stdin)
tag = r.get('tag_name', '') for r in releases:
if tag.startswith('hub-v') and not r.get('draft') and not r.get('prerelease'): tag = r.get('tag_name', '')
print(tag) if tag.startswith('hub-v') and not r.get('draft') and not r.get('prerelease'):
break print(tag)
") break
PY
)
if [ -z "$TAG" ]; then if [ -z "$TAG" ]; then
echo "No hub release found" echo "No hub release found"
exit 1 exit 1
@@ -200,28 +204,6 @@ for r in releases:
echo "tag=$TAG" >> $GITHUB_OUTPUT echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "Hub release tag: $TAG" echo "Hub release tag: $TAG"
- name: Download hub binaries
run: |
TAG="${{ steps.hub-tag.outputs.tag }}"
BASE_URL="https://github.com/${{ env.GITHUB_REPO }}/releases/download/$TAG"
mkdir -p hub-binaries/amd64 hub-binaries/arm64
curl -L --fail -o hub-binaries/amd64/aether-hub-linux-amd64.tar.gz "$BASE_URL/aether-hub-linux-amd64.tar.gz"
curl -L --fail -o hub-binaries/arm64/aether-hub-linux-arm64.tar.gz "$BASE_URL/aether-hub-linux-arm64.tar.gz"
tar xzf hub-binaries/amd64/aether-hub-linux-amd64.tar.gz -C hub-binaries/amd64
tar xzf hub-binaries/arm64/aether-hub-linux-arm64.tar.gz -C hub-binaries/arm64
- name: Upload amd64 binary
uses: actions/upload-artifact@v4
with:
name: hub-binary-amd64
path: hub-binaries/amd64/aether-hub
- name: Upload arm64 binary
uses: actions/upload-artifact@v4
with:
name: hub-binary-arm64
path: hub-binaries/arm64/aether-hub
build-app: build-app:
needs: [check-base-changes, build-base, download-hub] needs: [check-base-changes, build-base, download-hub]
if: always() && (needs.build-base.result == 'success' || needs.build-base.result == 'skipped') && needs.download-hub.result == 'success' if: always() && (needs.build-base.result == 'success' || needs.build-base.result == 'skipped') && needs.download-hub.result == 'success'
@@ -291,25 +273,9 @@ for r in releases:
version_tuple = __version_tuple__ version_tuple = __version_tuple__
EOF EOF
- name: Download hub binary for amd64 - name: Resolve hub release for build args
uses: actions/download-artifact@v4
with:
name: hub-binary-amd64
path: aether-hub/dist-amd64
- name: Download hub binary for arm64
uses: actions/download-artifact@v4
with:
name: hub-binary-arm64
path: aether-hub/dist-arm64
- name: Prepare hub binary for build
run: | run: |
# 为当前构建平台准备二进制(多架构构建每个平台分别运行) echo "Hub release tag: ${{ needs.download-hub.outputs.hub_tag }}"
# 使用 amd64 作为默认Buildx 会处理多架构
mkdir -p aether-hub/dist
cp aether-hub/dist-amd64/aether-hub aether-hub/dist/aether-hub
chmod +x aether-hub/dist/aether-hub
- name: Build and push app image (amd64) - name: Build and push app image (amd64)
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
@@ -322,13 +288,11 @@ for r in releases:
no-cache-filters: builder no-cache-filters: builder
cache-from: type=gha,scope=app-amd64 cache-from: type=gha,scope=app-amd64
cache-to: type=gha,mode=min,scope=app-amd64 cache-to: type=gha,mode=min,scope=app-amd64
build-args: |
HUB_RELEASE_REPO=${{ env.GITHUB_REPO }}
HUB_TAG=${{ needs.download-hub.outputs.hub_tag }}
platforms: linux/amd64 platforms: linux/amd64
- name: Swap hub binary for arm64
run: |
cp aether-hub/dist-arm64/aether-hub aether-hub/dist/aether-hub
chmod +x aether-hub/dist/aether-hub
- name: Build and push app image (arm64) - name: Build and push app image (arm64)
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
@@ -340,4 +304,7 @@ for r in releases:
no-cache-filters: builder no-cache-filters: builder
cache-from: type=gha,scope=app-arm64 cache-from: type=gha,scope=app-arm64
cache-to: type=gha,mode=min,scope=app-arm64 cache-to: type=gha,mode=min,scope=app-arm64
build-args: |
HUB_RELEASE_REPO=${{ env.GITHUB_REPO }}
HUB_TAG=${{ needs.download-hub.outputs.hub_tag }}
platforms: linux/arm64 platforms: linux/arm64

View File

@@ -12,6 +12,11 @@ RUN cd frontend && npm run build
# ==================== 运行时镜像 ==================== # ==================== 运行时镜像 ====================
FROM python:3.13-slim FROM python:3.13-slim
WORKDIR /app WORKDIR /app
ARG HUB_RELEASE_REPO=fawney19/Aether
ARG HUB_TAG
ARG TARGETARCH
# 运行时依赖(无 gcc/nodejs/npm使用 BuildKit 缓存加速) # 运行时依赖(无 gcc/nodejs/npm使用 BuildKit 缓存加速)
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \
@@ -26,9 +31,32 @@ COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/pytho
COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/ COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/
COPY --from=builder /usr/local/bin/uvicorn /usr/local/bin/ COPY --from=builder /usr/local/bin/uvicorn /usr/local/bin/
COPY --from=builder /usr/local/bin/alembic /usr/local/bin/ COPY --from=builder /usr/local/bin/alembic /usr/local/bin/
# Hub 预编译二进制(从 GitHub Release 下载) # Hub 预编译二进制(构建时从 GitHub Release 下载)
COPY aether-hub/dist/aether-hub /usr/local/bin/aether-hub RUN set -eux; \
RUN chmod +x /usr/local/bin/aether-hub tag="${HUB_TAG:-}"; \
if [ -z "$tag" ]; then \
tag="$(curl -sL "https://api.github.com/repos/${HUB_RELEASE_REPO}/releases" | python3 -c "import json,sys;print(next((r['tag_name'] for r in json.load(sys.stdin) if r.get('tag_name','').startswith('hub-v') and not r.get('draft') and not r.get('prerelease')),''))")"; \
fi; \
if [ -z "$tag" ]; then \
echo "Failed to resolve hub release tag"; \
exit 1; \
fi; \
arch="${TARGETARCH:-}"; \
if [ -z "$arch" ]; then \
arch="$(dpkg --print-architecture)"; \
fi; \
case "$arch" in \
amd64|arm64) ;; \
x86_64) arch="amd64" ;; \
aarch64) arch="arm64" ;; \
*) echo "Unsupported architecture: $arch"; exit 1 ;; \
esac; \
echo "Using Hub release tag: $tag"; \
url="https://github.com/${HUB_RELEASE_REPO}/releases/download/${tag}/aether-hub-linux-${arch}.tar.gz"; \
curl -L --fail -o /tmp/aether-hub.tar.gz "$url"; \
tar xzf /tmp/aether-hub.tar.gz -C /usr/local/bin; \
chmod +x /usr/local/bin/aether-hub; \
rm -f /tmp/aether-hub.tar.gz
# 从 builder 阶段复制前端构建产物 # 从 builder 阶段复制前端构建产物
COPY --from=builder /app/frontend/dist /usr/share/nginx/html COPY --from=builder /app/frontend/dist /usr/share/nginx/html
RUN chmod -R 755 /usr/share/nginx/html RUN chmod -R 755 /usr/share/nginx/html

View File

@@ -16,6 +16,10 @@ FROM python:3.13-slim
WORKDIR /app WORKDIR /app
ARG HUB_RELEASE_REPO=fawney19/Aether
ARG HUB_TAG
ARG TARGETARCH
# 运行时依赖(使用清华镜像源 + BuildKit 缓存加速) # 运行时依赖(使用清华镜像源 + BuildKit 缓存加速)
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \
@@ -34,9 +38,32 @@ COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/
COPY --from=builder /usr/local/bin/uvicorn /usr/local/bin/ COPY --from=builder /usr/local/bin/uvicorn /usr/local/bin/
COPY --from=builder /usr/local/bin/alembic /usr/local/bin/ COPY --from=builder /usr/local/bin/alembic /usr/local/bin/
# Hub 预编译二进制(从 GitHub Release 下载) # Hub 预编译二进制(构建时从 GitHub Release 下载)
COPY aether-hub/dist/aether-hub /usr/local/bin/aether-hub RUN set -eux; \
RUN chmod +x /usr/local/bin/aether-hub tag="${HUB_TAG:-}"; \
if [ -z "$tag" ]; then \
tag="$(curl -sL "https://api.github.com/repos/${HUB_RELEASE_REPO}/releases" | python3 -c "import json,sys;print(next((r['tag_name'] for r in json.load(sys.stdin) if r.get('tag_name','').startswith('hub-v') and not r.get('draft') and not r.get('prerelease')),''))")"; \
fi; \
if [ -z "$tag" ]; then \
echo "Failed to resolve hub release tag"; \
exit 1; \
fi; \
arch="${TARGETARCH:-}"; \
if [ -z "$arch" ]; then \
arch="$(dpkg --print-architecture)"; \
fi; \
case "$arch" in \
amd64|arm64) ;; \
x86_64) arch="amd64" ;; \
aarch64) arch="arm64" ;; \
*) echo "Unsupported architecture: $arch"; exit 1 ;; \
esac; \
echo "Using Hub release tag: $tag"; \
url="https://github.com/${HUB_RELEASE_REPO}/releases/download/${tag}/aether-hub-linux-${arch}.tar.gz"; \
curl -L --fail -o /tmp/aether-hub.tar.gz "$url"; \
tar xzf /tmp/aether-hub.tar.gz -C /usr/local/bin; \
chmod +x /usr/local/bin/aether-hub; \
rm -f /tmp/aether-hub.tar.gz
# 从 builder 阶段复制前端构建产物 # 从 builder 阶段复制前端构建产物
COPY --from=builder /app/frontend/dist /usr/share/nginx/html COPY --from=builder /app/frontend/dist /usr/share/nginx/html

View File

@@ -34,7 +34,7 @@ Aether 是一个自托管的 AI API 网关,为团队和个人提供多租户
## 部署 ## 部署
### Docker Compose推荐:预构建镜像) ### Docker Compose预构建镜像,已废弃
```bash ```bash
# 1. 克隆代码 # 1. 克隆代码
@@ -45,14 +45,14 @@ cd Aether
cp .env.example .env cp .env.example .env
python generate_keys.py # 生成密钥, 并将生成的密钥填入 .env python generate_keys.py # 生成密钥, 并将生成的密钥填入 .env
# 3. 部署 / 更新(自动执行数据库迁移) # 3. 该模式已废弃,不再作为推荐部署方式
docker compose pull && docker compose up -d # 现在统一使用下面的“本地构建部署”方式
# 4. 升级前备份 # 4. 升级前备份
docker compose exec postgres pg_dump -U postgres aether | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz docker compose exec postgres pg_dump -U postgres aether | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz
``` ```
### Docker Compose本地构建镜像 ### Docker Compose推荐:本地构建镜像)
```bash ```bash
# 1. 克隆代码 # 1. 克隆代码
@@ -65,6 +65,9 @@ python generate_keys.py # 生成密钥, 并将生成的密钥填入 .env
# 3. 部署 / 更新(自动构建、启动、迁移) # 3. 部署 / 更新(自动构建、启动、迁移)
./deploy.sh ./deploy.sh
# 可选:固定 Hub release 版本
./deploy.sh --hub-tag hub-v0.1.0
``` ```
### 本地开发 ### 本地开发
@@ -180,4 +183,3 @@ docker compose up -d app
## Star History ## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=fawney19/Aether&type=Date)](https://star-history.com/#fawney19/Aether&Date) [![Star History Chart](https://api.star-history.com/svg?repos=fawney19/Aether&type=Date)](https://star-history.com/#fawney19/Aether&Date)

54
aether-hub/README.md Normal file
View File

@@ -0,0 +1,54 @@
# aether-hub
`aether-hub` 是 Tunnel Hub 服务,负责在 proxy 与 worker 之间路由帧。
## 快速命令
### 1) 构建并上传 Hub 二进制(推荐生产)
```bash
cd aether-hub
./build.sh --upload hub-v0.1.0
```
说明:
- 默认会构建 `amd64 + arm64` 两个二进制并上传到 GitHub Release。
- 如只需单架构,可先 `./build.sh amd64``./build.sh arm64`
### 2) 部署端指定 Hub 版本并构建
```bash
cd /path/to/Aether
./deploy.sh --hub-tag hub-v0.1.0
```
不指定 `--hub-tag` 时,`./deploy.sh` 会自动解析最新 `hub-v*` release并在构建 app 镜像时从 GitHub Release 下载对应架构的 Hub 二进制。
### 3) 镜像模式(可选,调试/实验用)
仅本地加载镜像(单平台):
```bash
cd aether-hub
BUILDKIT_PROGRESS=plain ./build.sh --image --tag local-test --platforms linux/amd64 --load
```
## build.sh 模式说明
- 默认是 `binary` 模式(`cross` 构建二进制)。
- `--upload <hub-vX.Y.Z>` 会把构建产物上传到 GitHub Release。
-`--image` 后进入镜像模式(`docker buildx`,可选)。
常用参数:
- `--tag <tag>`: 镜像 tag
- `--image-name <name>`: 镜像名(默认 `ghcr.io/fawney19/aether-hub`
- `--platforms <list>`: 例如 `linux/amd64,linux/arm64`
- `--push`: 推送镜像
- `--load`: 加载到本地 Docker单平台
- `--latest`: 额外打 `latest` tag
## 与部署脚本关系
- `./deploy.sh`: 本地构建部署(会本地构建 app/base并在构建 app 时从 GitHub Release 下载 Hub可用 `--hub-tag` 固定版本)。

View File

@@ -1,131 +1,272 @@
#!/bin/bash #!/bin/bash
# 本地构建 aether-hub 多架构二进制 # aether-hub 构建脚本
# #
# 前置条件: # 支持两种模式:
# cargo install cross --git https://github.com/cross-rs/cross # 1) binary 模式(默认): 构建多架构二进制并可上传 GitHub Release
# Docker 或 Podman 运行中cross 需要容器环境 # 2) image 模式: 构建并推送/加载 Docker 镜像(推荐生产发布用
# #
# 用法: # 示例:
# ./build.sh # 构建 linux/amd64 + linux/arm64 # # binary 模式(兼容旧行为)
# ./build.sh amd64 # 仅构建 linux/amd64 # ./build.sh
# ./build.sh arm64 # 仅构建 linux/arm64 # ./build.sh amd64
# ./build.sh --upload v0.1.0 # 构建并上传到 GitHub Release需要 gh CLI # ./build.sh --upload hub-v0.1.0
#
# # image 模式(多架构推送)
# ./build.sh --image --tag v0.2.5 --push --latest
# ./build.sh --image --tag sha-abc123 --image-name ghcr.io/fawney19/aether-hub --push
# ./build.sh --image --tag local-test --platforms linux/amd64 --load
set -e set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
DIST_DIR="$SCRIPT_DIR/dist" DIST_DIR="$SCRIPT_DIR/dist"
# 解析参数 # -------------------------------
# Defaults
# -------------------------------
MODE="binary" # binary | image
# binary mode options
UPLOAD=false UPLOAD=false
TAG="" UPLOAD_TAG=""
BUILD_TARGETS="" BINARY_TARGETS=""
# image mode options
IMAGE_NAME="${IMAGE_NAME:-ghcr.io/fawney19/aether-hub}"
IMAGE_TAG=""
IMAGE_PLATFORMS="linux/amd64,linux/arm64"
IMAGE_PUSH=false
IMAGE_LOAD=false
IMAGE_LATEST=false
usage() {
cat <<'EOF'
用法:
./build.sh [binary-args]
./build.sh --image [image-args]
binary 模式(默认):
amd64|arm64 仅构建指定架构(可重复)
--upload <hub-vX.Y.Z> 上传到 GitHub Release需要 gh CLI
image 模式:
--image 启用镜像模式
--tag <tag> 镜像 tag默认自动从 git describe 推导)
--image-name <name> 镜像名(默认 ghcr.io/fawney19/aether-hub
--platforms <list> 平台列表,逗号分隔(默认 linux/amd64,linux/arm64
--push 推送镜像到仓库
--load 加载到本地 Docker仅单平台
--latest 额外打 latest tag
通用:
-h, --help 显示帮助
EOF
}
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--image)
MODE="image"
shift
;;
--tag)
IMAGE_TAG="${2:-}"
shift 2
;;
--image-name)
IMAGE_NAME="${2:-}"
shift 2
;;
--platforms)
IMAGE_PLATFORMS="${2:-}"
shift 2
;;
--push)
IMAGE_PUSH=true
shift
;;
--load)
IMAGE_LOAD=true
shift
;;
--latest)
IMAGE_LATEST=true
shift
;;
--upload) --upload)
UPLOAD=true UPLOAD=true
TAG="$2" UPLOAD_TAG="${2:-}"
shift 2 shift 2
;; ;;
amd64|arm64) amd64|arm64)
BUILD_TARGETS="$BUILD_TARGETS $1" BINARY_TARGETS="$BINARY_TARGETS $1"
shift shift
;; ;;
-h|--help)
usage
exit 0
;;
*) *)
echo "用法: $0 [amd64|arm64] [--upload <tag>]" echo "❌ 未知参数: $1"
usage
exit 1 exit 1
;; ;;
esac esac
done done
# 默认构建所有平台 build_binary() {
if [ -z "$BUILD_TARGETS" ]; then if [ -z "$BINARY_TARGETS" ]; then
BUILD_TARGETS="amd64 arm64" BINARY_TARGETS="amd64 arm64"
fi fi
# 检查 cross 是否安装 if ! command -v cross >/dev/null 2>&1; then
if ! command -v cross &> /dev/null; then echo "❌ 需要安装 cross: cargo install cross --git https://github.com/cross-rs/cross"
echo "❌ 需要安装 cross: cargo install cross --git https://github.com/cross-rs/cross"
exit 1
fi
# 创建输出目录
mkdir -p "$DIST_DIR"
echo "🔨 开始构建 aether-hub..."
echo " 目标平台: $BUILD_TARGETS"
echo ""
ARTIFACTS=""
for arch in $BUILD_TARGETS; do
# 映射架构到 Rust target
case "$arch" in
amd64) target="x86_64-unknown-linux-gnu" ;;
arm64) target="aarch64-unknown-linux-gnu" ;;
*) echo "❌ 未知架构: $arch"; exit 1 ;;
esac
echo ">>> 构建 $arch ($target)..."
cd "$SCRIPT_DIR"
cross build --release --target "$target" --locked
# 提取二进制
BIN="target/$target/release/aether-hub"
if [ ! -f "$BIN" ]; then
echo "❌ 未找到二进制文件: $BIN"
exit 1 exit 1
fi fi
# 打包 mkdir -p "$DIST_DIR"
ARCHIVE="$DIST_DIR/aether-hub-linux-$arch.tar.gz"
tar czf "$ARCHIVE" -C "target/$target/release" aether-hub
ARTIFACTS="$ARTIFACTS $ARCHIVE"
SIZE=$(du -h "$ARCHIVE" | cut -f1) echo "🔨 开始构建 aether-hub 二进制..."
echo "$arch 构建完成: $ARCHIVE ($SIZE)" echo " 目标平台: $BINARY_TARGETS"
echo "" echo ""
done
# 生成校验和 ARTIFACTS=""
cd "$DIST_DIR" for arch in $BINARY_TARGETS; do
shasum -a 256 aether-hub-*.tar.gz > SHA256SUMS.txt case "$arch" in
echo "📋 SHA256 校验和:" amd64) target="x86_64-unknown-linux-gnu" ;;
cat SHA256SUMS.txt arm64) target="aarch64-unknown-linux-gnu" ;;
echo "" *) echo "❌ 未知架构: $arch"; exit 1 ;;
esac
# 上传到 GitHub Release echo ">>> 构建 $arch ($target)..."
if [ "$UPLOAD" = true ]; then cd "$SCRIPT_DIR"
if [ -z "$TAG" ]; then cross build --release --target "$target" --locked
echo "❌ --upload 需要指定 tag例如: ./build.sh --upload hub-v0.1.0"
BIN="target/$target/release/aether-hub"
if [ ! -f "$BIN" ]; then
echo "❌ 未找到二进制文件: $BIN"
exit 1
fi
ARCHIVE="$DIST_DIR/aether-hub-linux-$arch.tar.gz"
tar czf "$ARCHIVE" -C "target/$target/release" aether-hub
ARTIFACTS="$ARTIFACTS $ARCHIVE"
SIZE=$(du -h "$ARCHIVE" | cut -f1)
echo "$arch 构建完成: $ARCHIVE ($SIZE)"
echo ""
done
cd "$DIST_DIR"
shasum -a 256 aether-hub-*.tar.gz > SHA256SUMS.txt
echo "📋 SHA256 校验和:"
cat SHA256SUMS.txt
echo ""
if [ "$UPLOAD" = true ]; then
if [ -z "$UPLOAD_TAG" ]; then
echo "❌ --upload 需要指定 tag例如: ./build.sh --upload hub-v0.1.0"
exit 1
fi
if ! command -v gh >/dev/null 2>&1; then
echo "❌ 需要安装 GitHub CLI: brew install gh"
exit 1
fi
echo "📦 上传到 GitHub Release: $UPLOAD_TAG"
cd "$PROJECT_DIR"
if ! git rev-parse "$UPLOAD_TAG" >/dev/null 2>&1; then
git tag "$UPLOAD_TAG"
git push origin "$UPLOAD_TAG"
fi
gh release create "$UPLOAD_TAG" \
--title "aether-hub ${UPLOAD_TAG#hub-}" \
--generate-notes \
$ARTIFACTS \
"$DIST_DIR/SHA256SUMS.txt"
echo "✅ 上传完成!"
fi
echo "🎉 binary 模式完成!"
}
build_image() {
if ! command -v docker >/dev/null 2>&1; then
echo "❌ 未找到 docker请先安装 Docker"
exit 1
fi
if ! docker buildx version >/dev/null 2>&1; then
echo "❌ 未找到 docker buildx请先启用 buildx"
exit 1 exit 1
fi fi
if ! command -v gh &> /dev/null; then if [ "$IMAGE_PUSH" = true ] && [ "$IMAGE_LOAD" = true ]; then
echo "❌ 需要安装 GitHub CLI: brew install gh" echo "❌ --push 与 --load 不能同时使用"
exit 1 exit 1
fi fi
echo "📦 上传到 GitHub Release: $TAG" if [ "$IMAGE_PUSH" = false ] && [ "$IMAGE_LOAD" = false ]; then
cd "$PROJECT_DIR" # image 模式默认走 push符合发布场景
IMAGE_PUSH=true
# 创建 tag如果不存在
if ! git rev-parse "$TAG" > /dev/null 2>&1; then
git tag "$TAG"
git push origin "$TAG"
fi fi
# 创建 Release 并上传 if [ -z "$IMAGE_TAG" ]; then
gh release create "$TAG" \ IMAGE_TAG=$(git -C "$PROJECT_DIR" describe --tags --always 2>/dev/null | sed 's/^v//')
--title "aether-hub ${TAG#hub-}" \ if [ -z "$IMAGE_TAG" ]; then
--generate-notes \ IMAGE_TAG=$(date +%Y%m%d%H%M%S)
$ARTIFACTS \ fi
"$DIST_DIR/SHA256SUMS.txt" fi
echo "✅ 上传完成!" if [ "$IMAGE_LOAD" = true ] && [[ "$IMAGE_PLATFORMS" == *,* ]]; then
echo "❌ --load 仅支持单平台,请用 --platforms linux/amd64或 arm64"
exit 1
fi
local ref="${IMAGE_NAME}:${IMAGE_TAG}"
local cmd=(docker buildx build
--platform "$IMAGE_PLATFORMS"
-f "$SCRIPT_DIR/Dockerfile"
-t "$ref"
)
if [ "$IMAGE_LATEST" = true ]; then
cmd+=(-t "${IMAGE_NAME}:latest")
fi
if [ "$IMAGE_PUSH" = true ]; then
cmd+=(--push)
else
cmd+=(--load)
fi
cmd+=("$SCRIPT_DIR")
echo "🔨 开始构建 aether-hub 镜像..."
echo " image: $ref"
echo " platforms: $IMAGE_PLATFORMS"
echo " mode: $([ "$IMAGE_PUSH" = true ] && echo push || echo load)"
echo ""
"${cmd[@]}"
if [ "$IMAGE_PUSH" = true ]; then
echo "✅ 镜像已推送: $ref"
if [ "$IMAGE_LATEST" = true ]; then
echo "✅ 镜像已推送: ${IMAGE_NAME}:latest"
fi
else
echo "✅ 镜像已加载到本地: $ref"
fi
echo "🎉 image 模式完成!"
}
if [ "$MODE" = "image" ]; then
build_image
else
build_binary
fi fi
echo "🎉 全部完成!"

View File

@@ -258,7 +258,8 @@ impl HubRouter {
pub fn register_worker(&self, conn: Arc<WorkerConn>) { pub fn register_worker(&self, conn: Arc<WorkerConn>) {
info!(worker_id = conn.id, "worker connected"); info!(worker_id = conn.id, "worker connected");
self.worker_conns.insert(conn.id, conn); self.worker_conns.insert(conn.id, conn.clone());
self.sync_node_status_to_worker(&conn);
} }
pub fn unregister_worker(&self, conn_id: u64) { pub fn unregister_worker(&self, conn_id: u64) {
@@ -667,6 +668,28 @@ impl HubRouter {
); );
} }
/// When a worker connects, sync all current node statuses so worker state
/// is consistent even if proxies connected before this worker came online.
fn sync_node_status_to_worker(&self, worker: &Arc<WorkerConn>) {
let snapshot: Vec<(String, usize)> = {
let map = self.proxy_conns.read();
map.iter()
.map(|(node_id, conns)| (node_id.clone(), conns.len()))
.collect()
};
for (node_id, conn_count) in &snapshot {
let frame = protocol::encode_node_status(node_id, *conn_count > 0, *conn_count);
let _ = worker.send(Message::Binary(frame.into()));
}
debug!(
worker_id = worker.id,
nodes_synced = snapshot.len(),
"synced NODE_STATUS snapshot to worker"
);
}
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Stats // Stats
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------

199
deploy.sh
View File

@@ -2,12 +2,13 @@
# 智能部署脚本 - 自动检测依赖/代码/迁移变化 # 智能部署脚本 - 自动检测依赖/代码/迁移变化
# #
# 用法: # 用法:
# 部署/更新: ./deploy.sh (自动检测所有变化) # 部署/更新: ./deploy.sh (自动检测所有变化)
# 强制重建: ./deploy.sh --rebuild-base # 指定 Hub 版本: ./deploy.sh --hub-tag hub-v0.1.0
# 更新 Hub: ./deploy.sh --update-hub # 更新 Hub: ./deploy.sh --update-hub
# 强制全部重建: ./deploy.sh --force # 强制重建: ./deploy.sh --rebuild-base
# 强制全部重建: ./deploy.sh --force
set -e set -euo pipefail
cd "$(dirname "$0")" cd "$(dirname "$0")"
# 兼容 docker-compose 和 docker compose # 兼容 docker-compose 和 docker compose
@@ -32,10 +33,69 @@ HASH_FILE=".deps-hash"
CODE_HASH_FILE=".code-hash" CODE_HASH_FILE=".code-hash"
MIGRATION_HASH_FILE=".migration-hash" MIGRATION_HASH_FILE=".migration-hash"
# Hub 二进制配置 # Hub release 配置
GITHUB_REPO="fawney19/Aether" GITHUB_REPO="fawney19/Aether"
HUB_DIST_DIR="aether-hub/dist" HUB_TAG_STATE_FILE=".hub-tag"
HUB_VERSION_FILE="$HUB_DIST_DIR/.version"
usage() {
cat <<'EOF'
Usage: ./deploy.sh [options]
Options:
--hub-tag <hub-vX.Y.Z> 指定 Hub Release tag例如 hub-v0.1.0
--update-hub 强制刷新 Hub 版本标记(下次构建会重新下载)
--rebuild-base, -r 仅重建 base 镜像
--force, -f 强制重建全部hub/base/app并重启
-h, --help 显示帮助
EOF
}
FORCE_REBUILD_ALL=false
REBUILD_BASE_ONLY=false
FORCE_UPDATE_HUB=false
HUB_TAG="${HUB_TAG:-}"
RESOLVED_HUB_TAG=""
while [ $# -gt 0 ]; do
case "$1" in
--hub-tag)
if [ $# -lt 2 ]; then
echo "❌ --hub-tag 需要一个值,例如 hub-v0.1.0"
exit 1
fi
HUB_TAG="$2"
shift 2
;;
--update-hub)
FORCE_UPDATE_HUB=true
shift
;;
--rebuild-base|-r)
REBUILD_BASE_ONLY=true
shift
;;
--force|-f)
FORCE_REBUILD_ALL=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "❌ 未知参数: $1"
usage
exit 1
;;
esac
done
if [ -n "$HUB_TAG" ]; then
case "$HUB_TAG" in
hub-v*) ;;
*) echo "❌ --hub-tag 格式应为 hub-vX.Y.Z例如 hub-v0.1.0"; exit 1 ;;
esac
fi
# 提取 pyproject.toml 中"会影响运行时依赖安装"的最小指纹(与 CI 保持一致): # 提取 pyproject.toml 中"会影响运行时依赖安装"的最小指纹(与 CI 保持一致):
# - [build-system] requires / build-backend # - [build-system] requires / build-backend
@@ -79,17 +139,6 @@ calc_code_hash() {
} | md5sum | cut -d' ' -f1 } | md5sum | cut -d' ' -f1
} }
# 检测目标平台架构
detect_arch() {
local machine
machine=$(uname -m)
case "$machine" in
x86_64|amd64) echo "amd64" ;;
aarch64|arm64) echo "arm64" ;;
*) echo "❌ 不支持的架构: $machine" >&2; exit 1 ;;
esac
}
# 获取最新 hub release tag # 获取最新 hub release tag
get_latest_hub_tag() { get_latest_hub_tag() {
curl -sL "https://api.github.com/repos/$GITHUB_REPO/releases" | \ curl -sL "https://api.github.com/repos/$GITHUB_REPO/releases" | \
@@ -104,58 +153,45 @@ for r in releases:
" 2>/dev/null " 2>/dev/null
} }
# 下载 Hub 预编译二进制 # 解析当前应使用的 Hub release tag优先使用指定值否则拉取最新
download_hub() { resolve_hub_tag() {
local tag="$1" local requested_tag="${1:-}"
local arch local latest_tag
arch=$(detect_arch)
if [ -z "$tag" ]; then if [ -n "$requested_tag" ]; then
echo ">>> 正在查询最新 Hub 版本..." echo "$requested_tag"
tag=$(get_latest_hub_tag)
if [ -z "$tag" ]; then
echo "❌ 无法获取最新 Hub Release请检查网络或手动指定版本"
exit 1
fi
fi
echo ">>> Hub 版本: $tag, 架构: $arch"
# 检查是否已下载
if [ -f "$HUB_VERSION_FILE" ] && [ "$(cat "$HUB_VERSION_FILE")" = "$tag-$arch" ] && [ -f "$HUB_DIST_DIR/aether-hub" ]; then
echo ">>> Hub 二进制已是最新 ($tag), 跳过下载."
return 0 return 0
fi fi
mkdir -p "$HUB_DIST_DIR" latest_tag="$(get_latest_hub_tag || true)"
if [ -n "$latest_tag" ]; then
echo "$latest_tag"
return 0
fi
local archive="aether-hub-linux-$arch.tar.gz" if [ -f "$HUB_TAG_STATE_FILE" ]; then
local url="https://github.com/$GITHUB_REPO/releases/download/$tag/$archive" echo "⚠️ 无法查询最新 Hub 版本,回退使用本地记录: $(cat "$HUB_TAG_STATE_FILE")" >&2
cat "$HUB_TAG_STATE_FILE"
return 0
fi
echo ">>> 下载 Hub 二进制: $url" echo "❌ 无法获取 Hub Release tag请检查网络或手动指定 --hub-tag" >&2
curl -L --fail -o "$HUB_DIST_DIR/$archive" "$url" || { exit 1
echo "❌ 下载失败: $url"
exit 1
}
# 解压
tar xzf "$HUB_DIST_DIR/$archive" -C "$HUB_DIST_DIR"
chmod +x "$HUB_DIST_DIR/aether-hub"
rm -f "$HUB_DIST_DIR/$archive"
# 记录版本
echo "$tag-$arch" > "$HUB_VERSION_FILE"
echo "✅ Hub 二进制下载完成."
} }
# 确保 Hub 二进制存在 # 确保本次构建的 Hub tag 已解析(默认追踪最新 release也可通过 --hub-tag 固定版本)
ensure_hub_binary() { ensure_hub_tag() {
if [ ! -f "$HUB_DIST_DIR/aether-hub" ]; then local requested_tag="${1:-}"
echo ">>> Hub 二进制不存在,正在下载..." RESOLVED_HUB_TAG="$(resolve_hub_tag "$requested_tag")"
download_hub
return 0 if [ -f "$HUB_TAG_STATE_FILE" ] && [ "$(cat "$HUB_TAG_STATE_FILE")" = "$RESOLVED_HUB_TAG" ]; then
echo ">>> Hub 版本未变化: $RESOLVED_HUB_TAG"
return 1
fi fi
return 1
echo "$RESOLVED_HUB_TAG" > "$HUB_TAG_STATE_FILE"
echo ">>> 使用 Hub 版本: $RESOLVED_HUB_TAG"
return 0
} }
# 计算迁移文件的哈希值 # 计算迁移文件的哈希值
@@ -235,8 +271,17 @@ EOF
# 构建应用镜像 # 构建应用镜像
build_app() { build_app() {
echo ">>> Building app image (code only)..." echo ">>> Building app image (code only)..."
if [ -z "${RESOLVED_HUB_TAG:-}" ]; then
echo "❌ RESOLVED_HUB_TAG 为空,无法构建 app 镜像"
exit 1
fi
echo ">>> Build args: HUB_TAG=$RESOLVED_HUB_TAG"
generate_version_file generate_version_file
docker build --pull=false -f Dockerfile.app.local -t aether-app:latest . docker build --pull=false \
--build-arg HUB_RELEASE_REPO="$GITHUB_REPO" \
--build-arg HUB_TAG="$RESOLVED_HUB_TAG" \
-f Dockerfile.app.local \
-t aether-app:latest .
save_code_hash save_code_hash
} }
@@ -284,9 +329,12 @@ print('Old version cleared')
} }
# 强制全部重建 # 强制全部重建
if [ "$1" = "--force" ] || [ "$1" = "-f" ]; then if [ "$FORCE_REBUILD_ALL" = true ]; then
echo ">>> Force rebuilding everything..." echo ">>> Force rebuilding everything..."
download_hub if [ "$FORCE_UPDATE_HUB" = true ]; then
rm -f "$HUB_TAG_STATE_FILE"
fi
ensure_hub_tag "$HUB_TAG" || true
build_base build_base
build_app build_app
compose_up --force-recreate compose_up --force-recreate
@@ -299,17 +347,18 @@ if [ "$1" = "--force" ] || [ "$1" = "-f" ]; then
fi fi
# 强制重建基础镜像 # 强制重建基础镜像
if [ "$1" = "--rebuild-base" ] || [ "$1" = "-r" ]; then if [ "$REBUILD_BASE_ONLY" = true ]; then
build_base build_base
echo ">>> Base image rebuilt. Run ./deploy.sh to deploy." echo ">>> Base image rebuilt. Run ./deploy.sh to deploy."
exit 0 exit 0
fi fi
# 更新 Hub 二进制 # 更新 Hub 版本标记
if [ "$1" = "--update-hub" ]; then if [ "$FORCE_UPDATE_HUB" = true ]; then
rm -f "$HUB_VERSION_FILE" rm -f "$HUB_TAG_STATE_FILE"
download_hub ensure_hub_tag "$HUB_TAG" || true
echo ">>> Hub binary updated. Run ./deploy.sh to deploy." echo ">>> Hub tag updated: $RESOLVED_HUB_TAG"
echo ">>> Run ./deploy.sh to build app image with the new Hub release."
exit 0 exit 0
fi fi
@@ -337,12 +386,12 @@ else
echo ">>> Dependencies unchanged." echo ">>> Dependencies unchanged."
fi fi
# 确保 Hub 二进制存在 # 解析/检查 Hub 版本(构建时由 Dockerfile 从 GitHub Release 下载)
if ensure_hub_binary; then if ensure_hub_tag "$HUB_TAG"; then
HUB_UPDATED=true HUB_UPDATED=true
NEED_RESTART=true NEED_RESTART=true
else else
echo ">>> Hub binary present." echo ">>> Hub version unchanged."
fi fi
# 检查代码或迁移是否变化,或者 base 重建了app 依赖 base # 检查代码或迁移是否变化,或者 base 重建了app 依赖 base
@@ -361,7 +410,7 @@ elif [ "$BASE_REBUILT" = true ]; then
build_app build_app
NEED_RESTART=true NEED_RESTART=true
elif [ "$HUB_UPDATED" = true ]; then elif [ "$HUB_UPDATED" = true ]; then
echo ">>> Hub binary updated, rebuilding app image..." echo ">>> Hub version updated, rebuilding app image..."
build_app build_app
NEED_RESTART=true NEED_RESTART=true
elif check_code_changed; then elif check_code_changed; then

View File

@@ -1,8 +1,10 @@
# Aether 部署配置 - 本地构建 # Aether 部署配置 - 本地构建
# 使用方法: # 使用方法:
# 首次构建 base: docker build -f Dockerfile.base -t aether-base:latest . # 首次构建 base: docker build -f Dockerfile.base -t aether-base:latest .
# Hub 二进制: cd aether-hub && ./build.shdeploy.sh 自动下载 # Hub 二进制: cd aether-hub && ./build.sh(默认 binary 模式)供发布;deploy.sh 构建 app 时会自动下载
# Hub 镜像发布: cd aether-hub && ./build.sh --image --tag <tag> --push可选
# 启动服务: docker compose -f docker-compose.build.yml up -d --build # 启动服务: docker compose -f docker-compose.build.yml up -d --build
# 或使用: ./deploy.sh
services: services:
postgres: postgres:

View File

@@ -33,7 +33,7 @@ services:
restart: unless-stopped restart: unless-stopped
app: app:
image: ghcr.io/fawney19/aether:latest image: ${APP_IMAGE:-ghcr.io/fawney19/aether:latest}
container_name: aether-app container_name: aether-app
env_file: env_file:
- .env - .env

View File

@@ -97,6 +97,19 @@ async def _on_startup() -> None:
config.worker_processes, config.worker_processes,
) )
# Hub 模式下worker 启动时主动建立 /worker 长连接:
# - 立即接收 NODE_STATUS 广播避免“proxy 已连但 UI 仍显示离线”的窗口期
# - 确保后续 tunnel 请求不需要首请求触发懒连接
if hub_enabled:
from src.services.proxy_node.hub_transport import get_hub_connection_manager
try:
await get_hub_connection_manager().ensure_connected()
logger.info("Hub worker channel initialized on startup")
except Exception as e:
# ensure_connected 失败时内部会启动重连循环,这里仅记录告警不阻塞启动
logger.warning("Hub worker channel init failed, reconnecting in background: {}", e)
from src.clients import get_redis_client from src.clients import get_redis_client
redis_client = await get_redis_client() redis_client = await get_redis_client()