mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
fix: 修改hub构建方式
This commit is contained in:
@@ -11,6 +11,7 @@ ENV/
|
|||||||
.uv/
|
.uv/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
dist/
|
dist/
|
||||||
|
!aether-hub/dist/aether-hub
|
||||||
build/
|
build/
|
||||||
*.egg
|
*.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:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
BASE_IMAGE_NAME: fawney19/aether-base
|
BASE_IMAGE_NAME: fawney19/aether-base
|
||||||
HUB_IMAGE_NAME: fawney19/aether-hub
|
|
||||||
APP_IMAGE_NAME: fawney19/aether
|
APP_IMAGE_NAME: fawney19/aether
|
||||||
|
GITHUB_REPO: fawney19/Aether
|
||||||
# Base image hash inputs:
|
# Base image hash inputs:
|
||||||
# - Dockerfile.base
|
# - Dockerfile.base
|
||||||
# - pyproject.toml (dependency fingerprint only; ignores tool/optional deps)
|
# - pyproject.toml (dependency fingerprint only; ignores tool/optional deps)
|
||||||
@@ -175,61 +175,56 @@ jobs:
|
|||||||
cache-to: type=gha,mode=max,scope=base
|
cache-to: type=gha,mode=max,scope=base
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
build-hub:
|
download-hub:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
|
||||||
outputs:
|
|
||||||
hub_binary_image: ${{ steps.hub-ref.outputs.image }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Get latest hub release tag
|
||||||
|
id: hub-tag
|
||||||
- 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
|
|
||||||
run: |
|
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:
|
build-app:
|
||||||
needs: [check-base-changes, build-base, build-hub]
|
needs: [check-base-changes, build-base, download-hub]
|
||||||
if: always() && (needs.build-base.result == 'success' || needs.build-base.result == 'skipped') && needs.build-hub.result == 'success'
|
if: always() && (needs.build-base.result == 'success' || needs.build-base.result == 'skipped') && needs.download-hub.result == 'success'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -296,17 +291,53 @@ jobs:
|
|||||||
version_tuple = __version_tuple__
|
version_tuple = __version_tuple__
|
||||||
EOF
|
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
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile.app
|
file: ./Dockerfile.app
|
||||||
push: true
|
push: true
|
||||||
build-args: |
|
|
||||||
HUB_BINARY_IMAGE=${{ needs.build-hub.outputs.hub_binary_image }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
no-cache-filters: builder
|
no-cache-filters: builder
|
||||||
cache-from: type=gha,scope=app
|
cache-from: type=gha,scope=app-amd64
|
||||||
cache-to: type=gha,mode=min,scope=app
|
cache-to: type=gha,mode=min,scope=app-amd64
|
||||||
platforms: linux/amd64,linux/arm64
|
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
|
.deps-hash
|
||||||
.code-hash
|
.code-hash
|
||||||
.migration-hash
|
.migration-hash
|
||||||
|
.hub-hash
|
||||||
|
|
||||||
|
# Hub prebuilt binaries
|
||||||
|
aether-hub/dist/
|
||||||
|
|
||||||
# Version file (auto-generated by hatch-vcs)
|
# Version file (auto-generated by hatch-vcs)
|
||||||
src/_version.py
|
src/_version.py
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
# 运行镜像:从 base 提取产物到精简运行时
|
# 运行镜像:从 base 提取产物到精简运行时
|
||||||
# 构建命令: docker build -f Dockerfile.app -t aether-app:latest .
|
# 构建命令: docker build -f Dockerfile.app -t aether-app:latest .
|
||||||
# 用于 GitHub Actions CI(官方源)
|
# 用于 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
|
FROM aether-base:latest AS builder
|
||||||
WORKDIR /app
|
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/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/
|
||||||
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 阶段复制前端构建产物
|
# 从 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
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
# 运行镜像:从 base 提取产物到精简运行时(国内镜像源版本)
|
# 运行镜像:从 base 提取产物到精简运行时(国内镜像源版本)
|
||||||
# 构建命令: docker build -f Dockerfile.app.local -t aether-app:latest .
|
# 构建命令: 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
|
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/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/
|
||||||
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 阶段复制前端构建产物
|
# 从 builder 阶段复制前端构建产物
|
||||||
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
|
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 (自动检测所有变化)
|
||||||
# 强制重建: ./deploy.sh --rebuild-base
|
# 强制重建: ./deploy.sh --rebuild-base
|
||||||
# 强制重建 Hub: ./deploy.sh --rebuild-hub
|
# 更新 Hub: ./deploy.sh --update-hub
|
||||||
# 强制全部重建: ./deploy.sh --force
|
# 强制全部重建: ./deploy.sh --force
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@@ -29,10 +29,14 @@ compose_up() {
|
|||||||
|
|
||||||
# 缓存文件
|
# 缓存文件
|
||||||
HASH_FILE=".deps-hash"
|
HASH_FILE=".deps-hash"
|
||||||
HUB_HASH_FILE=".hub-hash"
|
|
||||||
CODE_HASH_FILE=".code-hash"
|
CODE_HASH_FILE=".code-hash"
|
||||||
MIGRATION_HASH_FILE=".migration-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 保持一致):
|
# 提取 pyproject.toml 中"会影响运行时依赖安装"的最小指纹(与 CI 保持一致):
|
||||||
# - [build-system] requires / build-backend
|
# - [build-system] requires / build-backend
|
||||||
# - [project] requires-python / dependencies
|
# - [project] requires-python / dependencies
|
||||||
@@ -75,14 +79,83 @@ calc_code_hash() {
|
|||||||
} | md5sum | cut -d' ' -f1
|
} | md5sum | cut -d' ' -f1
|
||||||
}
|
}
|
||||||
|
|
||||||
# 计算 Hub 文件的哈希值(本地构建 aether-hub:local)
|
# 检测目标平台架构
|
||||||
calc_hub_hash() {
|
detect_arch() {
|
||||||
{
|
local machine
|
||||||
cat aether-hub/Dockerfile 2>/dev/null
|
machine=$(uname -m)
|
||||||
cat aether-hub/Cargo.toml 2>/dev/null
|
case "$machine" in
|
||||||
cat aether-hub/Cargo.lock 2>/dev/null
|
x86_64|amd64) echo "amd64" ;;
|
||||||
find aether-hub/src -type f -name "*.rs" 2>/dev/null | sort | xargs cat 2>/dev/null
|
aarch64|arm64) echo "arm64" ;;
|
||||||
} | md5sum | cut -d' ' -f1
|
*) 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
|
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() {
|
check_migration_changed() {
|
||||||
@@ -140,7 +203,6 @@ check_migration_changed() {
|
|||||||
|
|
||||||
# 保存哈希
|
# 保存哈希
|
||||||
save_deps_hash() { calc_deps_hash > "$HASH_FILE"; }
|
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_code_hash() { calc_code_hash > "$CODE_HASH_FILE"; }
|
||||||
save_migration_hash() { calc_migration_hash > "$MIGRATION_HASH_FILE"; }
|
save_migration_hash() { calc_migration_hash > "$MIGRATION_HASH_FILE"; }
|
||||||
|
|
||||||
@@ -151,12 +213,6 @@ build_base() {
|
|||||||
save_deps_hash
|
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() {
|
generate_version_file() {
|
||||||
@@ -180,7 +236,7 @@ EOF
|
|||||||
build_app() {
|
build_app() {
|
||||||
echo ">>> Building app image (code only)..."
|
echo ">>> Building app image (code only)..."
|
||||||
generate_version_file
|
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
|
save_code_hash
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,8 +286,8 @@ print('Old version cleared')
|
|||||||
# 强制全部重建
|
# 强制全部重建
|
||||||
if [ "$1" = "--force" ] || [ "$1" = "-f" ]; then
|
if [ "$1" = "--force" ] || [ "$1" = "-f" ]; then
|
||||||
echo ">>> Force rebuilding everything..."
|
echo ">>> Force rebuilding everything..."
|
||||||
|
download_hub
|
||||||
build_base
|
build_base
|
||||||
build_hub
|
|
||||||
build_app
|
build_app
|
||||||
compose_up --force-recreate
|
compose_up --force-recreate
|
||||||
sleep 3
|
sleep 3
|
||||||
@@ -249,10 +305,11 @@ if [ "$1" = "--rebuild-base" ] || [ "$1" = "-r" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 强制重建 Hub 镜像
|
# 更新 Hub 二进制
|
||||||
if [ "$1" = "--rebuild-hub" ]; then
|
if [ "$1" = "--update-hub" ]; then
|
||||||
build_hub
|
rm -f "$HUB_VERSION_FILE"
|
||||||
echo ">>> Hub image rebuilt. Run ./deploy.sh to deploy."
|
download_hub
|
||||||
|
echo ">>> Hub binary updated. Run ./deploy.sh to deploy."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -263,7 +320,7 @@ git pull
|
|||||||
# 标记是否需要重启
|
# 标记是否需要重启
|
||||||
NEED_RESTART=false
|
NEED_RESTART=false
|
||||||
BASE_REBUILT=false
|
BASE_REBUILT=false
|
||||||
HUB_REBUILT=false
|
HUB_UPDATED=false
|
||||||
|
|
||||||
# 检查基础镜像是否存在,或依赖是否变化
|
# 检查基础镜像是否存在,或依赖是否变化
|
||||||
if ! docker image inspect aether-base:latest >/dev/null 2>&1; then
|
if ! docker image inspect aether-base:latest >/dev/null 2>&1; then
|
||||||
@@ -280,19 +337,12 @@ else
|
|||||||
echo ">>> Dependencies unchanged."
|
echo ">>> Dependencies unchanged."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查 Hub 镜像是否存在,或 Hub 代码是否变化
|
# 确保 Hub 二进制存在
|
||||||
if ! docker image inspect aether-hub:local >/dev/null 2>&1; then
|
if ensure_hub_binary; then
|
||||||
echo ">>> Hub image not found, building..."
|
HUB_UPDATED=true
|
||||||
build_hub
|
|
||||||
HUB_REBUILT=true
|
|
||||||
NEED_RESTART=true
|
|
||||||
elif check_hub_changed; then
|
|
||||||
echo ">>> Hub changed, rebuilding hub image..."
|
|
||||||
build_hub
|
|
||||||
HUB_REBUILT=true
|
|
||||||
NEED_RESTART=true
|
NEED_RESTART=true
|
||||||
else
|
else
|
||||||
echo ">>> Hub unchanged."
|
echo ">>> Hub binary present."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查代码或迁移是否变化,或者 base 重建了(app 依赖 base)
|
# 检查代码或迁移是否变化,或者 base 重建了(app 依赖 base)
|
||||||
@@ -310,8 +360,8 @@ elif [ "$BASE_REBUILT" = true ]; then
|
|||||||
echo ">>> Base image rebuilt, rebuilding app image..."
|
echo ">>> Base image rebuilt, rebuilding app image..."
|
||||||
build_app
|
build_app
|
||||||
NEED_RESTART=true
|
NEED_RESTART=true
|
||||||
elif [ "$HUB_REBUILT" = true ]; then
|
elif [ "$HUB_UPDATED" = true ]; then
|
||||||
echo ">>> Hub image rebuilt, rebuilding app image..."
|
echo ">>> Hub binary updated, rebuilding app image..."
|
||||||
build_app
|
build_app
|
||||||
NEED_RESTART=true
|
NEED_RESTART=true
|
||||||
elif check_code_changed; then
|
elif check_code_changed; then
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Aether 部署配置 - 本地构建
|
# Aether 部署配置 - 本地构建
|
||||||
# 使用方法:
|
# 使用方法:
|
||||||
# 首次构建 base: docker build -f Dockerfile.base -t aether-base:latest .
|
# 首次构建 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
|
# 启动服务: docker compose -f docker-compose.build.yml up -d --build
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -43,8 +43,6 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.app.local
|
dockerfile: Dockerfile.app.local
|
||||||
args:
|
|
||||||
HUB_BINARY_IMAGE: ${HUB_BINARY_IMAGE:-aether-hub:local}
|
|
||||||
image: aether-app:latest
|
image: aether-app:latest
|
||||||
container_name: aether-app
|
container_name: aether-app
|
||||||
env_file:
|
env_file:
|
||||||
|
|||||||
@@ -28,6 +28,48 @@ export interface ProviderOAuthCompleteResponseWithKey {
|
|||||||
email?: string | null
|
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> {
|
export async function refreshProviderOAuth(keyId: string): Promise<ProviderOAuthCompleteResponse> {
|
||||||
const resp = await client.post(`/api/admin/provider-oauth/keys/${keyId}/refresh`)
|
const resp = await client.post(`/api/admin/provider-oauth/keys/${keyId}/refresh`)
|
||||||
return resp.data
|
return resp.data
|
||||||
@@ -56,6 +98,26 @@ export async function importProviderRefreshToken(
|
|||||||
return resp.data
|
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)
|
// Device Authorization (AWS SSO OIDC)
|
||||||
|
|
||||||
export interface DeviceAuthorizeRequest {
|
export interface DeviceAuthorizeRequest {
|
||||||
|
|||||||
@@ -467,6 +467,51 @@
|
|||||||
或选择 JSON 文件导入
|
或选择 JSON 文件导入
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -490,7 +535,7 @@
|
|||||||
:disabled="!canImport"
|
:disabled="!canImport"
|
||||||
@click="handleImport"
|
@click="handleImport"
|
||||||
>
|
>
|
||||||
{{ importing ? '导入中...' : '导入' }}
|
{{ importing ? (importTask ? `导入中 ${importTask.progress_percent}%` : '导入中...') : '导入' }}
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -518,11 +563,16 @@ import {
|
|||||||
startProviderLevelOAuth,
|
startProviderLevelOAuth,
|
||||||
completeProviderLevelOAuth,
|
completeProviderLevelOAuth,
|
||||||
importProviderRefreshToken,
|
importProviderRefreshToken,
|
||||||
batchImportOAuth,
|
startBatchImportOAuthTask,
|
||||||
|
getBatchImportOAuthTaskStatus,
|
||||||
startDeviceAuthorize,
|
startDeviceAuthorize,
|
||||||
pollDeviceAuthorize,
|
pollDeviceAuthorize,
|
||||||
getAwsRegions,
|
getAwsRegions,
|
||||||
} from '@/api/endpoints'
|
} from '@/api/endpoints'
|
||||||
|
import type {
|
||||||
|
OAuthBatchImportTaskStatus,
|
||||||
|
OAuthBatchImportTaskStatusResponse,
|
||||||
|
} from '@/api/endpoints/provider_oauth'
|
||||||
import ProxyNodeSelect from './ProxyNodeSelect.vue'
|
import ProxyNodeSelect from './ProxyNodeSelect.vue'
|
||||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||||
|
|
||||||
@@ -665,6 +715,9 @@ const importing = ref(false)
|
|||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const showManualInput = ref(false)
|
const showManualInput = ref(false)
|
||||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
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)
|
const isOpen = computed(() => props.open)
|
||||||
|
|
||||||
@@ -691,6 +744,78 @@ const canImport = computed(() => {
|
|||||||
return importText.value.trim().length > 0 && !importing.value
|
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() {
|
function stopDevicePolling() {
|
||||||
if (devicePollTimer) {
|
if (devicePollTimer) {
|
||||||
clearTimeout(devicePollTimer)
|
clearTimeout(devicePollTimer)
|
||||||
@@ -715,11 +840,13 @@ function resetDevice() {
|
|||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
oauth.value = createInitialOAuthState()
|
oauth.value = createInitialOAuthState()
|
||||||
|
stopImportPolling()
|
||||||
stopDevicePolling()
|
stopDevicePolling()
|
||||||
totp.stop()
|
totp.stop()
|
||||||
device.value = createInitialDeviceState()
|
device.value = createInitialDeviceState()
|
||||||
importText.value = ''
|
importText.value = ''
|
||||||
importing.value = false
|
importing.value = false
|
||||||
|
importTask.value = null
|
||||||
isDragging.value = false
|
isDragging.value = false
|
||||||
showManualInput.value = false
|
showManualInput.value = false
|
||||||
proxyPopoverOpen.value = false
|
proxyPopoverOpen.value = false
|
||||||
@@ -938,25 +1065,32 @@ async function handleImport() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
importing.value = true
|
importing.value = true
|
||||||
|
let keepImporting = false
|
||||||
try {
|
try {
|
||||||
const proxyNodeId = selectedProxyNodeId.value || undefined
|
const proxyNodeId = selectedProxyNodeId.value || undefined
|
||||||
// 检测是否为批量导入
|
// 检测是否为批量导入
|
||||||
if (isBatchImport(inputText)) {
|
if (isBatchImport(inputText)) {
|
||||||
// 批量导入
|
const task = await startBatchImportOAuthTask(props.providerId, inputText, proxyNodeId)
|
||||||
const result = await batchImportOAuth(props.providerId, inputText, proxyNodeId)
|
importTask.value = {
|
||||||
if (result.success > 0) {
|
task_id: task.task_id,
|
||||||
if (result.failed > 0) {
|
provider_id: props.providerId,
|
||||||
success(`批量导入完成:成功 ${result.success} 个,失败 ${result.failed} 个`)
|
provider_type: props.providerType || '',
|
||||||
} else {
|
status: task.status,
|
||||||
success(`批量导入成功:${result.success} 个账号已添加`)
|
total: task.total,
|
||||||
}
|
processed: task.processed,
|
||||||
emit('saved')
|
success: task.success,
|
||||||
handleClose()
|
failed: task.failed,
|
||||||
} else {
|
progress_percent: task.progress_percent,
|
||||||
// 全部失败,显示第一个错误
|
message: task.message || null,
|
||||||
const firstError = result.results.find(r => r.status === 'error')
|
error: null,
|
||||||
showError(firstError?.error || '批量导入失败', '导入失败')
|
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 {
|
} else {
|
||||||
// 单条导入
|
// 单条导入
|
||||||
const parsed = parseImportText(inputText)
|
const parsed = parseImportText(inputText)
|
||||||
@@ -976,7 +1110,9 @@ async function handleImport() {
|
|||||||
const errorMessage = parseApiError(err, '导入失败')
|
const errorMessage = parseApiError(err, '导入失败')
|
||||||
showError(errorMessage, '错误')
|
showError(errorMessage, '错误')
|
||||||
} finally {
|
} finally {
|
||||||
importing.value = false
|
if (!keepImporting) {
|
||||||
|
importing.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1080,6 +1216,7 @@ async function pollDevice() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
stopImportPolling()
|
||||||
stopDevicePolling()
|
stopDevicePolling()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -702,6 +702,7 @@ const editingModel = ref<GlobalModelResponse | null>(null)
|
|||||||
// 数据
|
// 数据
|
||||||
const globalModels = ref<GlobalModelResponse[]>([])
|
const globalModels = ref<GlobalModelResponse[]>([])
|
||||||
const providers = ref<ProviderWithEndpointsSummary[]>([])
|
const providers = ref<ProviderWithEndpointsSummary[]>([])
|
||||||
|
const GLOBAL_MODELS_FETCH_PAGE_SIZE = 1000
|
||||||
|
|
||||||
// 模型目录分页
|
// 模型目录分页
|
||||||
const catalogCurrentPage = ref(1)
|
const catalogCurrentPage = ref(1)
|
||||||
@@ -1026,9 +1027,25 @@ watch([searchQuery, capabilityFilters], () => {
|
|||||||
async function loadGlobalModels() {
|
async function loadGlobalModels() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await listGlobalModels()
|
const allModels: GlobalModelResponse[] = []
|
||||||
// API 返回 { models: [...], total: number }
|
let skip = 0
|
||||||
globalModels.value = response.models || []
|
|
||||||
|
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) {
|
} catch (err: unknown) {
|
||||||
log.error('加载模型失败:', err)
|
log.error('加载模型失败:', err)
|
||||||
showError(parseApiError(err, '加载模型失败'), '加载模型失败')
|
showError(parseApiError(err, '加载模型失败'), '加载模型失败')
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user