mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix: 修改hub构建方式
This commit is contained in:
@@ -11,6 +11,7 @@ ENV/
|
||||
.uv/
|
||||
*.egg-info/
|
||||
dist/
|
||||
!aether-hub/dist/aether-hub
|
||||
build/
|
||||
*.egg
|
||||
|
||||
|
||||
91
.github/workflows/build-hub.yml
vendored
Normal file
91
.github/workflows/build-hub.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: Build aether-hub
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['hub-v*']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: linux-amd64
|
||||
target: x86_64-unknown-linux-gnu
|
||||
use_cross: true
|
||||
- name: linux-arm64
|
||||
target: aarch64-unknown-linux-gnu
|
||||
use_cross: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: aether-hub -> target
|
||||
key: ${{ matrix.target }}
|
||||
|
||||
- name: Install cross
|
||||
if: matrix.use_cross
|
||||
uses: taiki-e/install-action@cross
|
||||
|
||||
- name: Build
|
||||
working-directory: aether-hub
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ matrix.use_cross }}" = "true" ]; then
|
||||
cross build --release --target ${{ matrix.target }}
|
||||
else
|
||||
cargo build --release --target ${{ matrix.target }}
|
||||
fi
|
||||
|
||||
- name: Package
|
||||
shell: bash
|
||||
run: |
|
||||
cd aether-hub/target/${{ matrix.target }}/release
|
||||
chmod +x aether-hub
|
||||
tar czf ../../../../aether-hub-${{ matrix.name }}.tar.gz aether-hub
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aether-hub-${{ matrix.name }}
|
||||
path: aether-hub-*.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
path: artifacts
|
||||
|
||||
- name: Generate checksums
|
||||
working-directory: artifacts
|
||||
run: sha256sum aether-hub-* > SHA256SUMS.txt
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: "aether-hub ${{ github.ref_name }}"
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
artifacts/aether-hub-*
|
||||
artifacts/SHA256SUMS.txt
|
||||
fail_on_unmatched_files: true
|
||||
141
.github/workflows/docker-publish.yml
vendored
141
.github/workflows/docker-publish.yml
vendored
@@ -14,8 +14,8 @@ on:
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
BASE_IMAGE_NAME: fawney19/aether-base
|
||||
HUB_IMAGE_NAME: fawney19/aether-hub
|
||||
APP_IMAGE_NAME: fawney19/aether
|
||||
GITHUB_REPO: fawney19/Aether
|
||||
# Base image hash inputs:
|
||||
# - Dockerfile.base
|
||||
# - pyproject.toml (dependency fingerprint only; ignores tool/optional deps)
|
||||
@@ -175,61 +175,56 @@ jobs:
|
||||
cache-to: type=gha,mode=max,scope=base
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
build-hub:
|
||||
download-hub:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
outputs:
|
||||
hub_binary_image: ${{ steps.hub-ref.outputs.image }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for hub image
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.HUB_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=pre,enable=${{ contains(github.ref, '-') }}
|
||||
type=raw,value=fix,enable=${{ contains(github.ref, '-fix') }}
|
||||
type=raw,value=${{ github.sha }}
|
||||
type=sha,prefix=
|
||||
flavor: |
|
||||
latest=auto
|
||||
|
||||
- name: Build and push hub image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./aether-hub
|
||||
file: ./aether-hub/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=hub
|
||||
cache-to: type=gha,mode=max,scope=hub
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Export hub binary image reference
|
||||
id: hub-ref
|
||||
- name: Get latest hub release tag
|
||||
id: hub-tag
|
||||
run: |
|
||||
echo "image=${{ env.REGISTRY }}/${{ env.HUB_IMAGE_NAME }}:${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
TAG=$(curl -sL "https://api.github.com/repos/${{ env.GITHUB_REPO }}/releases" | \
|
||||
python3 -c "
|
||||
import json, sys
|
||||
releases = json.load(sys.stdin)
|
||||
for r in releases:
|
||||
tag = r.get('tag_name', '')
|
||||
if tag.startswith('hub-v') and not r.get('draft') and not r.get('prerelease'):
|
||||
print(tag)
|
||||
break
|
||||
")
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "No hub release found"
|
||||
exit 1
|
||||
fi
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
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:
|
||||
needs: [check-base-changes, build-base, build-hub]
|
||||
if: always() && (needs.build-base.result == 'success' || needs.build-base.result == 'skipped') && needs.build-hub.result == 'success'
|
||||
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'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -296,17 +291,53 @@ jobs:
|
||||
version_tuple = __version_tuple__
|
||||
EOF
|
||||
|
||||
- name: Build and push app image
|
||||
- name: Download hub binary for amd64
|
||||
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: |
|
||||
# 为当前构建平台准备二进制(多架构构建每个平台分别运行)
|
||||
# 使用 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)
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.app
|
||||
push: true
|
||||
build-args: |
|
||||
HUB_BINARY_IMAGE=${{ needs.build-hub.outputs.hub_binary_image }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
no-cache-filters: builder
|
||||
cache-from: type=gha,scope=app
|
||||
cache-to: type=gha,mode=min,scope=app
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha,scope=app-amd64
|
||||
cache-to: type=gha,mode=min,scope=app-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)
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.app
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
no-cache-filters: builder
|
||||
cache-from: type=gha,scope=app-arm64
|
||||
cache-to: type=gha,mode=min,scope=app-arm64
|
||||
platforms: linux/arm64
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -226,6 +226,10 @@ test.py
|
||||
.deps-hash
|
||||
.code-hash
|
||||
.migration-hash
|
||||
.hub-hash
|
||||
|
||||
# Hub prebuilt binaries
|
||||
aether-hub/dist/
|
||||
|
||||
# Version file (auto-generated by hatch-vcs)
|
||||
src/_version.py
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
# 运行镜像:从 base 提取产物到精简运行时
|
||||
# 构建命令: docker build -f Dockerfile.app -t aether-app:latest .
|
||||
# 用于 GitHub Actions CI(官方源)
|
||||
ARG HUB_BINARY_IMAGE=ghcr.io/fawney19/aether-hub:latest
|
||||
FROM ${HUB_BINARY_IMAGE} AS hub-bin
|
||||
|
||||
FROM aether-base:latest AS builder
|
||||
WORKDIR /app
|
||||
@@ -28,7 +26,9 @@ 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/uvicorn /usr/local/bin/
|
||||
COPY --from=builder /usr/local/bin/alembic /usr/local/bin/
|
||||
COPY --from=hub-bin /usr/local/bin/aether-hub /usr/local/bin/
|
||||
# Hub 预编译二进制(从 GitHub Release 下载)
|
||||
COPY aether-hub/dist/aether-hub /usr/local/bin/aether-hub
|
||||
RUN chmod +x /usr/local/bin/aether-hub
|
||||
# 从 builder 阶段复制前端构建产物
|
||||
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
|
||||
RUN chmod -R 755 /usr/share/nginx/html
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
# 运行镜像:从 base 提取产物到精简运行时(国内镜像源版本)
|
||||
# 构建命令: docker build -f Dockerfile.app.local -t aether-app:latest .
|
||||
# 用于本地/国内服务器部署
|
||||
ARG HUB_BINARY_IMAGE=aether-hub:local
|
||||
FROM ${HUB_BINARY_IMAGE} AS hub-bin
|
||||
|
||||
FROM aether-base:latest AS builder
|
||||
|
||||
@@ -35,7 +33,10 @@ 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/uvicorn /usr/local/bin/
|
||||
COPY --from=builder /usr/local/bin/alembic /usr/local/bin/
|
||||
COPY --from=hub-bin /usr/local/bin/aether-hub /usr/local/bin/
|
||||
|
||||
# Hub 预编译二进制(从 GitHub Release 下载)
|
||||
COPY aether-hub/dist/aether-hub /usr/local/bin/aether-hub
|
||||
RUN chmod +x /usr/local/bin/aether-hub
|
||||
|
||||
# 从 builder 阶段复制前端构建产物
|
||||
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
|
||||
|
||||
131
aether-hub/build.sh
Executable file
131
aether-hub/build.sh
Executable file
@@ -0,0 +1,131 @@
|
||||
#!/bin/bash
|
||||
# 本地构建 aether-hub 多架构二进制
|
||||
#
|
||||
# 前置条件:
|
||||
# cargo install cross --git https://github.com/cross-rs/cross
|
||||
# Docker 或 Podman 运行中(cross 需要容器环境)
|
||||
#
|
||||
# 用法:
|
||||
# ./build.sh # 构建 linux/amd64 + linux/arm64
|
||||
# ./build.sh amd64 # 仅构建 linux/amd64
|
||||
# ./build.sh arm64 # 仅构建 linux/arm64
|
||||
# ./build.sh --upload v0.1.0 # 构建并上传到 GitHub Release(需要 gh CLI)
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DIST_DIR="$SCRIPT_DIR/dist"
|
||||
|
||||
# 解析参数
|
||||
UPLOAD=false
|
||||
TAG=""
|
||||
BUILD_TARGETS=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--upload)
|
||||
UPLOAD=true
|
||||
TAG="$2"
|
||||
shift 2
|
||||
;;
|
||||
amd64|arm64)
|
||||
BUILD_TARGETS="$BUILD_TARGETS $1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "用法: $0 [amd64|arm64] [--upload <tag>]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 默认构建所有平台
|
||||
if [ -z "$BUILD_TARGETS" ]; then
|
||||
BUILD_TARGETS="amd64 arm64"
|
||||
fi
|
||||
|
||||
# 检查 cross 是否安装
|
||||
if ! command -v cross &> /dev/null; then
|
||||
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
|
||||
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 ""
|
||||
|
||||
# 上传到 GitHub Release
|
||||
if [ "$UPLOAD" = true ]; then
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "❌ --upload 需要指定 tag,例如: ./build.sh --upload hub-v0.1.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v gh &> /dev/null; then
|
||||
echo "❌ 需要安装 GitHub CLI: brew install gh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 上传到 GitHub Release: $TAG"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# 创建 tag(如果不存在)
|
||||
if ! git rev-parse "$TAG" > /dev/null 2>&1; then
|
||||
git tag "$TAG"
|
||||
git push origin "$TAG"
|
||||
fi
|
||||
|
||||
# 创建 Release 并上传
|
||||
gh release create "$TAG" \
|
||||
--title "aether-hub ${TAG#hub-}" \
|
||||
--generate-notes \
|
||||
$ARTIFACTS \
|
||||
"$DIST_DIR/SHA256SUMS.txt"
|
||||
|
||||
echo "✅ 上传完成!"
|
||||
fi
|
||||
|
||||
echo "🎉 全部完成!"
|
||||
146
deploy.sh
146
deploy.sh
@@ -4,7 +4,7 @@
|
||||
# 用法:
|
||||
# 部署/更新: ./deploy.sh (自动检测所有变化)
|
||||
# 强制重建: ./deploy.sh --rebuild-base
|
||||
# 强制重建 Hub: ./deploy.sh --rebuild-hub
|
||||
# 更新 Hub: ./deploy.sh --update-hub
|
||||
# 强制全部重建: ./deploy.sh --force
|
||||
|
||||
set -e
|
||||
@@ -29,10 +29,14 @@ compose_up() {
|
||||
|
||||
# 缓存文件
|
||||
HASH_FILE=".deps-hash"
|
||||
HUB_HASH_FILE=".hub-hash"
|
||||
CODE_HASH_FILE=".code-hash"
|
||||
MIGRATION_HASH_FILE=".migration-hash"
|
||||
|
||||
# Hub 二进制配置
|
||||
GITHUB_REPO="fawney19/Aether"
|
||||
HUB_DIST_DIR="aether-hub/dist"
|
||||
HUB_VERSION_FILE="$HUB_DIST_DIR/.version"
|
||||
|
||||
# 提取 pyproject.toml 中"会影响运行时依赖安装"的最小指纹(与 CI 保持一致):
|
||||
# - [build-system] requires / build-backend
|
||||
# - [project] requires-python / dependencies
|
||||
@@ -75,14 +79,83 @@ calc_code_hash() {
|
||||
} | md5sum | cut -d' ' -f1
|
||||
}
|
||||
|
||||
# 计算 Hub 文件的哈希值(本地构建 aether-hub:local)
|
||||
calc_hub_hash() {
|
||||
{
|
||||
cat aether-hub/Dockerfile 2>/dev/null
|
||||
cat aether-hub/Cargo.toml 2>/dev/null
|
||||
cat aether-hub/Cargo.lock 2>/dev/null
|
||||
find aether-hub/src -type f -name "*.rs" 2>/dev/null | sort | xargs cat 2>/dev/null
|
||||
} | 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
|
||||
get_latest_hub_tag() {
|
||||
curl -sL "https://api.github.com/repos/$GITHUB_REPO/releases" | \
|
||||
python3 -c "
|
||||
import json, sys
|
||||
releases = json.load(sys.stdin)
|
||||
for r in releases:
|
||||
tag = r.get('tag_name', '')
|
||||
if tag.startswith('hub-v') and not r.get('draft') and not r.get('prerelease'):
|
||||
print(tag)
|
||||
break
|
||||
" 2>/dev/null
|
||||
}
|
||||
|
||||
# 下载 Hub 预编译二进制
|
||||
download_hub() {
|
||||
local tag="$1"
|
||||
local arch
|
||||
arch=$(detect_arch)
|
||||
|
||||
if [ -z "$tag" ]; then
|
||||
echo ">>> 正在查询最新 Hub 版本..."
|
||||
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
|
||||
fi
|
||||
|
||||
mkdir -p "$HUB_DIST_DIR"
|
||||
|
||||
local archive="aether-hub-linux-$arch.tar.gz"
|
||||
local url="https://github.com/$GITHUB_REPO/releases/download/$tag/$archive"
|
||||
|
||||
echo ">>> 下载 Hub 二进制: $url"
|
||||
curl -L --fail -o "$HUB_DIST_DIR/$archive" "$url" || {
|
||||
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 二进制存在
|
||||
ensure_hub_binary() {
|
||||
if [ ! -f "$HUB_DIST_DIR/aether-hub" ]; then
|
||||
echo ">>> Hub 二进制不存在,正在下载..."
|
||||
download_hub
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# 计算迁移文件的哈希值
|
||||
@@ -114,17 +187,7 @@ check_code_changed() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# 检查 Hub 是否变化
|
||||
check_hub_changed() {
|
||||
local current_hash=$(calc_hub_hash)
|
||||
if [ -f "$HUB_HASH_FILE" ]; then
|
||||
local saved_hash=$(cat "$HUB_HASH_FILE")
|
||||
if [ "$current_hash" = "$saved_hash" ]; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
# 检查迁移是否变化
|
||||
check_migration_changed() {
|
||||
@@ -140,7 +203,6 @@ check_migration_changed() {
|
||||
|
||||
# 保存哈希
|
||||
save_deps_hash() { calc_deps_hash > "$HASH_FILE"; }
|
||||
save_hub_hash() { calc_hub_hash > "$HUB_HASH_FILE"; }
|
||||
save_code_hash() { calc_code_hash > "$CODE_HASH_FILE"; }
|
||||
save_migration_hash() { calc_migration_hash > "$MIGRATION_HASH_FILE"; }
|
||||
|
||||
@@ -151,12 +213,6 @@ build_base() {
|
||||
save_deps_hash
|
||||
}
|
||||
|
||||
# 构建 Hub 镜像(本地)
|
||||
build_hub() {
|
||||
echo ">>> Building hub image (local)..."
|
||||
docker build --pull=false --build-arg CARGO_MIRROR=1 -f aether-hub/Dockerfile -t aether-hub:local ./aether-hub
|
||||
save_hub_hash
|
||||
}
|
||||
|
||||
# 生成版本文件
|
||||
generate_version_file() {
|
||||
@@ -180,7 +236,7 @@ EOF
|
||||
build_app() {
|
||||
echo ">>> Building app image (code only)..."
|
||||
generate_version_file
|
||||
docker build --pull=false --build-arg HUB_BINARY_IMAGE=aether-hub:local -f Dockerfile.app.local -t aether-app:latest .
|
||||
docker build --pull=false -f Dockerfile.app.local -t aether-app:latest .
|
||||
save_code_hash
|
||||
}
|
||||
|
||||
@@ -230,8 +286,8 @@ print('Old version cleared')
|
||||
# 强制全部重建
|
||||
if [ "$1" = "--force" ] || [ "$1" = "-f" ]; then
|
||||
echo ">>> Force rebuilding everything..."
|
||||
download_hub
|
||||
build_base
|
||||
build_hub
|
||||
build_app
|
||||
compose_up --force-recreate
|
||||
sleep 3
|
||||
@@ -249,10 +305,11 @@ if [ "$1" = "--rebuild-base" ] || [ "$1" = "-r" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 强制重建 Hub 镜像
|
||||
if [ "$1" = "--rebuild-hub" ]; then
|
||||
build_hub
|
||||
echo ">>> Hub image rebuilt. Run ./deploy.sh to deploy."
|
||||
# 更新 Hub 二进制
|
||||
if [ "$1" = "--update-hub" ]; then
|
||||
rm -f "$HUB_VERSION_FILE"
|
||||
download_hub
|
||||
echo ">>> Hub binary updated. Run ./deploy.sh to deploy."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -263,7 +320,7 @@ git pull
|
||||
# 标记是否需要重启
|
||||
NEED_RESTART=false
|
||||
BASE_REBUILT=false
|
||||
HUB_REBUILT=false
|
||||
HUB_UPDATED=false
|
||||
|
||||
# 检查基础镜像是否存在,或依赖是否变化
|
||||
if ! docker image inspect aether-base:latest >/dev/null 2>&1; then
|
||||
@@ -280,19 +337,12 @@ else
|
||||
echo ">>> Dependencies unchanged."
|
||||
fi
|
||||
|
||||
# 检查 Hub 镜像是否存在,或 Hub 代码是否变化
|
||||
if ! docker image inspect aether-hub:local >/dev/null 2>&1; then
|
||||
echo ">>> Hub image not found, building..."
|
||||
build_hub
|
||||
HUB_REBUILT=true
|
||||
NEED_RESTART=true
|
||||
elif check_hub_changed; then
|
||||
echo ">>> Hub changed, rebuilding hub image..."
|
||||
build_hub
|
||||
HUB_REBUILT=true
|
||||
# 确保 Hub 二进制存在
|
||||
if ensure_hub_binary; then
|
||||
HUB_UPDATED=true
|
||||
NEED_RESTART=true
|
||||
else
|
||||
echo ">>> Hub unchanged."
|
||||
echo ">>> Hub binary present."
|
||||
fi
|
||||
|
||||
# 检查代码或迁移是否变化,或者 base 重建了(app 依赖 base)
|
||||
@@ -310,8 +360,8 @@ elif [ "$BASE_REBUILT" = true ]; then
|
||||
echo ">>> Base image rebuilt, rebuilding app image..."
|
||||
build_app
|
||||
NEED_RESTART=true
|
||||
elif [ "$HUB_REBUILT" = true ]; then
|
||||
echo ">>> Hub image rebuilt, rebuilding app image..."
|
||||
elif [ "$HUB_UPDATED" = true ]; then
|
||||
echo ">>> Hub binary updated, rebuilding app image..."
|
||||
build_app
|
||||
NEED_RESTART=true
|
||||
elif check_code_changed; then
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Aether 部署配置 - 本地构建
|
||||
# 使用方法:
|
||||
# 首次构建 base: docker build -f Dockerfile.base -t aether-base:latest .
|
||||
# 首次构建 hub: docker build -f aether-hub/Dockerfile -t aether-hub:local ./aether-hub
|
||||
# Hub 二进制: cd aether-hub && ./build.sh 或 deploy.sh 自动下载
|
||||
# 启动服务: docker compose -f docker-compose.build.yml up -d --build
|
||||
|
||||
services:
|
||||
@@ -43,8 +43,6 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.app.local
|
||||
args:
|
||||
HUB_BINARY_IMAGE: ${HUB_BINARY_IMAGE:-aether-hub:local}
|
||||
image: aether-app:latest
|
||||
container_name: aether-app
|
||||
env_file:
|
||||
|
||||
@@ -28,6 +28,48 @@ export interface ProviderOAuthCompleteResponseWithKey {
|
||||
email?: string | null
|
||||
}
|
||||
|
||||
export interface OAuthBatchImportResultItem {
|
||||
index: number
|
||||
status: 'success' | 'error'
|
||||
key_id?: string
|
||||
key_name?: string
|
||||
auth_method?: string
|
||||
error?: string
|
||||
replaced?: boolean
|
||||
}
|
||||
|
||||
export type OAuthBatchImportTaskStatus = 'submitted' | 'processing' | 'completed' | 'failed'
|
||||
|
||||
export interface OAuthBatchImportTaskStartResponse {
|
||||
task_id: string
|
||||
status: OAuthBatchImportTaskStatus
|
||||
total: number
|
||||
processed: number
|
||||
success: number
|
||||
failed: number
|
||||
progress_percent: number
|
||||
message?: string | null
|
||||
}
|
||||
|
||||
export interface OAuthBatchImportTaskStatusResponse {
|
||||
task_id: string
|
||||
provider_id: string
|
||||
provider_type: string
|
||||
status: OAuthBatchImportTaskStatus
|
||||
total: number
|
||||
processed: number
|
||||
success: number
|
||||
failed: number
|
||||
progress_percent: number
|
||||
message?: string | null
|
||||
error?: string | null
|
||||
error_samples: OAuthBatchImportResultItem[]
|
||||
created_at: number
|
||||
started_at?: number | null
|
||||
finished_at?: number | null
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export async function refreshProviderOAuth(keyId: string): Promise<ProviderOAuthCompleteResponse> {
|
||||
const resp = await client.post(`/api/admin/provider-oauth/keys/${keyId}/refresh`)
|
||||
return resp.data
|
||||
@@ -56,6 +98,26 @@ export async function importProviderRefreshToken(
|
||||
return resp.data
|
||||
}
|
||||
|
||||
export async function startBatchImportOAuthTask(
|
||||
providerId: string,
|
||||
credentials: string,
|
||||
proxyNodeId?: string
|
||||
): Promise<OAuthBatchImportTaskStartResponse> {
|
||||
const resp = await client.post(`/api/admin/provider-oauth/providers/${providerId}/batch-import/tasks`, {
|
||||
credentials,
|
||||
proxy_node_id: proxyNodeId || undefined,
|
||||
})
|
||||
return resp.data
|
||||
}
|
||||
|
||||
export async function getBatchImportOAuthTaskStatus(
|
||||
providerId: string,
|
||||
taskId: string
|
||||
): Promise<OAuthBatchImportTaskStatusResponse> {
|
||||
const resp = await client.get(`/api/admin/provider-oauth/providers/${providerId}/batch-import/tasks/${taskId}`)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
// Device Authorization (AWS SSO OIDC)
|
||||
|
||||
export interface DeviceAuthorizeRequest {
|
||||
|
||||
@@ -467,6 +467,51 @@
|
||||
或选择 JSON 文件导入
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="importTask"
|
||||
class="rounded-xl border border-border bg-muted/20 p-3 space-y-2"
|
||||
>
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="font-medium">
|
||||
{{ getImportTaskStatusText(importTask.status) }}
|
||||
</span>
|
||||
<span class="font-mono tabular-nums">
|
||||
{{ importTask.progress_percent }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-1.5 rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full bg-primary transition-all duration-300"
|
||||
:style="{ width: `${Math.max(0, Math.min(importTask.progress_percent, 100))}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-[11px] text-muted-foreground">
|
||||
<span>进度 {{ importTask.processed }}/{{ importTask.total }}</span>
|
||||
<span>成功 {{ importTask.success }} · 失败 {{ importTask.failed }}</span>
|
||||
</div>
|
||||
<p
|
||||
v-if="importTask.message"
|
||||
class="text-[11px] text-muted-foreground"
|
||||
>
|
||||
{{ importTask.message }}
|
||||
</p>
|
||||
<div
|
||||
v-if="importTask.error_samples.length > 0"
|
||||
class="space-y-1"
|
||||
>
|
||||
<p class="text-[11px] text-destructive">
|
||||
最近错误
|
||||
</p>
|
||||
<p
|
||||
v-for="item in importTask.error_samples.slice(0, 3)"
|
||||
:key="`${item.index}-${item.error || item.status}`"
|
||||
class="text-[11px] text-destructive/90"
|
||||
>
|
||||
#{{ item.index + 1 }} {{ item.error || '导入失败' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -490,7 +535,7 @@
|
||||
:disabled="!canImport"
|
||||
@click="handleImport"
|
||||
>
|
||||
{{ importing ? '导入中...' : '导入' }}
|
||||
{{ importing ? (importTask ? `导入中 ${importTask.progress_percent}%` : '导入中...') : '导入' }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
@@ -518,11 +563,16 @@ import {
|
||||
startProviderLevelOAuth,
|
||||
completeProviderLevelOAuth,
|
||||
importProviderRefreshToken,
|
||||
batchImportOAuth,
|
||||
startBatchImportOAuthTask,
|
||||
getBatchImportOAuthTaskStatus,
|
||||
startDeviceAuthorize,
|
||||
pollDeviceAuthorize,
|
||||
getAwsRegions,
|
||||
} from '@/api/endpoints'
|
||||
import type {
|
||||
OAuthBatchImportTaskStatus,
|
||||
OAuthBatchImportTaskStatusResponse,
|
||||
} from '@/api/endpoints/provider_oauth'
|
||||
import ProxyNodeSelect from './ProxyNodeSelect.vue'
|
||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||
|
||||
@@ -665,6 +715,9 @@ const importing = ref(false)
|
||||
const isDragging = ref(false)
|
||||
const showManualInput = ref(false)
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
const importTask = ref<OAuthBatchImportTaskStatusResponse | null>(null)
|
||||
let importPollTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const importPolling = ref(false)
|
||||
|
||||
const isOpen = computed(() => props.open)
|
||||
|
||||
@@ -691,6 +744,78 @@ const canImport = computed(() => {
|
||||
return importText.value.trim().length > 0 && !importing.value
|
||||
})
|
||||
|
||||
function stopImportPolling() {
|
||||
if (importPollTimer) {
|
||||
clearTimeout(importPollTimer)
|
||||
importPollTimer = null
|
||||
}
|
||||
importPolling.value = false
|
||||
}
|
||||
|
||||
function getImportTaskStatusText(status: OAuthBatchImportTaskStatus): string {
|
||||
switch (status) {
|
||||
case 'submitted':
|
||||
return '任务已提交'
|
||||
case 'processing':
|
||||
return '正在导入'
|
||||
case 'completed':
|
||||
return '导入完成'
|
||||
case 'failed':
|
||||
return '导入失败'
|
||||
default:
|
||||
return '处理中'
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleImportPoll(taskId: string, delayMs = 1200) {
|
||||
stopImportPolling()
|
||||
importPollTimer = setTimeout(() => {
|
||||
void pollImportTaskStatus(taskId)
|
||||
}, delayMs)
|
||||
}
|
||||
|
||||
async function pollImportTaskStatus(taskId: string) {
|
||||
if (!props.providerId || importPolling.value) return
|
||||
|
||||
importPolling.value = true
|
||||
try {
|
||||
const task = await getBatchImportOAuthTaskStatus(props.providerId, taskId)
|
||||
importTask.value = task
|
||||
|
||||
if (task.status === 'completed') {
|
||||
stopImportPolling()
|
||||
importing.value = false
|
||||
if (task.success > 0) {
|
||||
if (task.failed > 0) {
|
||||
success(`批量导入完成:成功 ${task.success} 个,失败 ${task.failed} 个`)
|
||||
} else {
|
||||
success(`批量导入成功:${task.success} 个账号已添加`)
|
||||
}
|
||||
emit('saved')
|
||||
handleClose()
|
||||
} else {
|
||||
showError(task.error || '批量导入失败', '导入失败')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (task.status === 'failed') {
|
||||
stopImportPolling()
|
||||
importing.value = false
|
||||
showError(task.error || task.message || '批量导入失败', '导入失败')
|
||||
return
|
||||
}
|
||||
|
||||
scheduleImportPoll(taskId)
|
||||
} catch {
|
||||
if (importing.value) {
|
||||
scheduleImportPoll(taskId, 2000)
|
||||
}
|
||||
} finally {
|
||||
importPolling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function stopDevicePolling() {
|
||||
if (devicePollTimer) {
|
||||
clearTimeout(devicePollTimer)
|
||||
@@ -715,11 +840,13 @@ function resetDevice() {
|
||||
|
||||
function resetForm() {
|
||||
oauth.value = createInitialOAuthState()
|
||||
stopImportPolling()
|
||||
stopDevicePolling()
|
||||
totp.stop()
|
||||
device.value = createInitialDeviceState()
|
||||
importText.value = ''
|
||||
importing.value = false
|
||||
importTask.value = null
|
||||
isDragging.value = false
|
||||
showManualInput.value = false
|
||||
proxyPopoverOpen.value = false
|
||||
@@ -938,25 +1065,32 @@ async function handleImport() {
|
||||
}
|
||||
|
||||
importing.value = true
|
||||
let keepImporting = false
|
||||
try {
|
||||
const proxyNodeId = selectedProxyNodeId.value || undefined
|
||||
// 检测是否为批量导入
|
||||
if (isBatchImport(inputText)) {
|
||||
// 批量导入
|
||||
const result = await batchImportOAuth(props.providerId, inputText, proxyNodeId)
|
||||
if (result.success > 0) {
|
||||
if (result.failed > 0) {
|
||||
success(`批量导入完成:成功 ${result.success} 个,失败 ${result.failed} 个`)
|
||||
} else {
|
||||
success(`批量导入成功:${result.success} 个账号已添加`)
|
||||
}
|
||||
emit('saved')
|
||||
handleClose()
|
||||
} else {
|
||||
// 全部失败,显示第一个错误
|
||||
const firstError = result.results.find(r => r.status === 'error')
|
||||
showError(firstError?.error || '批量导入失败', '导入失败')
|
||||
const task = await startBatchImportOAuthTask(props.providerId, inputText, proxyNodeId)
|
||||
importTask.value = {
|
||||
task_id: task.task_id,
|
||||
provider_id: props.providerId,
|
||||
provider_type: props.providerType || '',
|
||||
status: task.status,
|
||||
total: task.total,
|
||||
processed: task.processed,
|
||||
success: task.success,
|
||||
failed: task.failed,
|
||||
progress_percent: task.progress_percent,
|
||||
message: task.message || null,
|
||||
error: null,
|
||||
error_samples: [],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
started_at: null,
|
||||
finished_at: null,
|
||||
updated_at: Math.floor(Date.now() / 1000),
|
||||
}
|
||||
keepImporting = true
|
||||
scheduleImportPoll(task.task_id, 400)
|
||||
} else {
|
||||
// 单条导入
|
||||
const parsed = parseImportText(inputText)
|
||||
@@ -976,7 +1110,9 @@ async function handleImport() {
|
||||
const errorMessage = parseApiError(err, '导入失败')
|
||||
showError(errorMessage, '错误')
|
||||
} finally {
|
||||
importing.value = false
|
||||
if (!keepImporting) {
|
||||
importing.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1080,6 +1216,7 @@ async function pollDevice() {
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopImportPolling()
|
||||
stopDevicePolling()
|
||||
})
|
||||
|
||||
|
||||
@@ -702,6 +702,7 @@ const editingModel = ref<GlobalModelResponse | null>(null)
|
||||
// 数据
|
||||
const globalModels = ref<GlobalModelResponse[]>([])
|
||||
const providers = ref<ProviderWithEndpointsSummary[]>([])
|
||||
const GLOBAL_MODELS_FETCH_PAGE_SIZE = 1000
|
||||
|
||||
// 模型目录分页
|
||||
const catalogCurrentPage = ref(1)
|
||||
@@ -1026,9 +1027,25 @@ watch([searchQuery, capabilityFilters], () => {
|
||||
async function loadGlobalModels() {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await listGlobalModels()
|
||||
// API 返回 { models: [...], total: number }
|
||||
globalModels.value = response.models || []
|
||||
const allModels: GlobalModelResponse[] = []
|
||||
let skip = 0
|
||||
|
||||
while (true) {
|
||||
const response = await listGlobalModels({
|
||||
skip,
|
||||
limit: GLOBAL_MODELS_FETCH_PAGE_SIZE,
|
||||
})
|
||||
const pageModels = response.models || []
|
||||
allModels.push(...pageModels)
|
||||
|
||||
if (pageModels.length < GLOBAL_MODELS_FETCH_PAGE_SIZE) {
|
||||
break
|
||||
}
|
||||
|
||||
skip += pageModels.length
|
||||
}
|
||||
|
||||
globalModels.value = allModels
|
||||
} catch (err: unknown) {
|
||||
log.error('加载模型失败:', err)
|
||||
showError(parseApiError(err, '加载模型失败'), '加载模型失败')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user