refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构

- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/
- 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层
- 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构
- 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations)
- 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image
- 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
fawney19
2026-04-03 16:26:16 +08:00
parent 8f26e1a31f
commit 1d9c77522a
868 changed files with 1735 additions and 2433 deletions

View File

@@ -4,177 +4,13 @@ on:
push: push:
tags: ['v*'] tags: ['v*']
workflow_dispatch: workflow_dispatch:
inputs:
build_base:
description: 'Rebuild base image'
required: false
default: false
type: boolean
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
BASE_IMAGE_NAME: fawney19/aether-base
APP_IMAGE_NAME: fawney19/aether APP_IMAGE_NAME: fawney19/aether
GITHUB_REPO: fawney19/Aether GITHUB_REPO: fawney19/Aether
# Base image hash inputs:
# - Dockerfile.base
# - pyproject.toml (dependency fingerprint only; ignores tool/optional deps)
# - frontend/package-lock.json
jobs: jobs:
check-base-changes:
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
outputs:
base_changed: ${{ steps.check.outputs.base_changed }}
steps:
- uses: actions/checkout@v5
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Check if base image needs rebuild
id: check
run: |
if [ "${{ github.event.inputs.build_base }}" == "true" ]; then
echo "base_changed=true" >> $GITHUB_OUTPUT
exit 0
fi
# Calculate current hash of base-related inputs (dependency-only fingerprint)
PY_FINGERPRINT=$(python3 - <<'PY'
import json
import pathlib
import tomllib
data = tomllib.loads(pathlib.Path("pyproject.toml").read_text("utf-8"))
project = data.get("project") or {}
build = data.get("build-system") or {}
fingerprint = {
"requires-python": project.get("requires-python"),
"dependencies": sorted(project.get("dependencies") or []),
"build-backend": build.get("build-backend"),
"build-requires": sorted(build.get("requires") or []),
}
print(json.dumps(fingerprint, sort_keys=True, separators=(",", ":")))
PY
)
CURRENT_HASH=$(
(
cat Dockerfile.base
printf '%s\n' "$PY_FINGERPRINT"
cat frontend/package-lock.json
) | sha256sum | cut -d' ' -f1
)
echo "Current base hash: $CURRENT_HASH"
# Try to get hash label from remote image config
# Pull the image config and extract labels
REMOTE_HASH=""
if docker pull ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:latest; then
REMOTE_HASH=$(docker inspect ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:latest --format '{{ index .Config.Labels "org.opencontainers.image.base.hash" }}' 2>/dev/null) || true
else
echo "WARN: failed to pull remote base image; forcing base rebuild."
echo "base_changed=true" >> $GITHUB_OUTPUT
exit 0
fi
if [ -z "$REMOTE_HASH" ] || [ "$REMOTE_HASH" == "<no value>" ]; then
# No remote image or no hash label, need to rebuild
echo "No remote base image or hash label found, need rebuild"
echo "base_changed=true" >> $GITHUB_OUTPUT
elif [ "$CURRENT_HASH" != "$REMOTE_HASH" ]; then
echo "Hash mismatch: remote=$REMOTE_HASH, current=$CURRENT_HASH"
echo "base_changed=true" >> $GITHUB_OUTPUT
else
echo "Hash matches, no rebuild needed"
echo "base_changed=false" >> $GITHUB_OUTPUT
fi
build-base:
needs: check-base-changes
if: needs.check-base-changes.outputs.base_changed == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v5
- 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: Calculate base files hash
id: hash
run: |
PY_FINGERPRINT=$(python3 - <<'PY'
import json
import pathlib
import tomllib
data = tomllib.loads(pathlib.Path("pyproject.toml").read_text("utf-8"))
project = data.get("project") or {}
build = data.get("build-system") or {}
fingerprint = {
"requires-python": project.get("requires-python"),
"dependencies": sorted(project.get("dependencies") or []),
"build-backend": build.get("build-backend"),
"build-requires": sorted(build.get("requires") or []),
}
print(json.dumps(fingerprint, sort_keys=True, separators=(",", ":")))
PY
)
HASH=$(
(
cat Dockerfile.base
printf '%s\n' "$PY_FINGERPRINT"
cat frontend/package-lock.json
) | sha256sum | cut -d' ' -f1
)
echo "hash=$HASH" >> $GITHUB_OUTPUT
- name: Extract metadata for base image
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}
tags: |
type=raw,value=latest
type=sha,prefix=
labels: |
org.opencontainers.image.base.hash=${{ steps.hash.outputs.hash }}
- name: Build and push base image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.base
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=base
cache-to: type=gha,mode=max,scope=base
platforms: linux/amd64,linux/arm64
download-hub: download-hub:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -197,8 +33,7 @@ jobs:
echo "Hub release tag: $TAG" echo "Hub release tag: $TAG"
build-app: build-app:
needs: [check-base-changes, build-base, download-hub] needs: [download-hub]
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
@@ -241,30 +76,13 @@ jobs:
- name: Extract version from tag - name: Extract version from tag
id: version id: version
run: | run: |
# 从 tag 提取版本号,如 v0.2.5 -> 0.2.5
VERSION="${GITHUB_REF#refs/tags/v}" VERSION="${GITHUB_REF#refs/tags/v}"
if [ "$VERSION" = "$GITHUB_REF" ]; then if [ "$VERSION" = "$GITHUB_REF" ]; then
# 不是 tag 触发,使用 git describe
VERSION=$(git describe --tags --always | sed 's/^v//') VERSION=$(git describe --tags --always | sed 's/^v//')
fi fi
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Extracted version: $VERSION" echo "Extracted version: $VERSION"
- name: Update Dockerfile.app to use registry base image
run: |
sed -i "s|FROM aether-base:latest AS builder|FROM ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:latest AS builder|g" Dockerfile.app
- name: Generate version file
run: |
# 生成 _version.py 文件
cat > src/_version.py << EOF
# Auto-generated by CI
__version__ = '${{ steps.version.outputs.version }}'
__version_tuple__ = tuple(int(x) for x in '${{ steps.version.outputs.version }}'.split('.') if x.isdigit())
version = __version__
version_tuple = __version_tuple__
EOF
- name: Resolve hub release for build args - name: Resolve hub release for build args
run: | run: |
echo "Hub release tag: ${{ needs.download-hub.outputs.hub_tag }}" echo "Hub release tag: ${{ needs.download-hub.outputs.hub_tag }}"
@@ -276,7 +94,6 @@ jobs:
context: . context: .
file: ./Dockerfile.app file: ./Dockerfile.app
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
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: | build-args: |
@@ -292,7 +109,6 @@ jobs:
context: . context: .
file: ./Dockerfile.app file: ./Dockerfile.app
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
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: | build-args: |
@@ -303,16 +119,13 @@ jobs:
- name: Create multi-arch manifest and push - name: Create multi-arch manifest and push
run: | run: |
# Extract digests
AMD64_DIGEST="${{ steps.build-amd64.outputs.digest }}" AMD64_DIGEST="${{ steps.build-amd64.outputs.digest }}"
ARM64_DIGEST="${{ steps.build-arm64.outputs.digest }}" ARM64_DIGEST="${{ steps.build-arm64.outputs.digest }}"
echo "amd64 digest: $AMD64_DIGEST" echo "amd64 digest: $AMD64_DIGEST"
echo "arm64 digest: $ARM64_DIGEST" echo "arm64 digest: $ARM64_DIGEST"
# For each tag, create multi-arch manifest on each registry
TAGS=$(echo "${{ steps.meta.outputs.tags }}" | tr '\n' ' ') TAGS=$(echo "${{ steps.meta.outputs.tags }}" | tr '\n' ' ')
for FULL_TAG in $TAGS; do for FULL_TAG in $TAGS; do
# Determine which registry this tag belongs to
if [[ "$FULL_TAG" == ghcr.io/* ]]; then if [[ "$FULL_TAG" == ghcr.io/* ]]; then
REPO="${{ env.REGISTRY }}/${{ env.APP_IMAGE_NAME }}" REPO="${{ env.REGISTRY }}/${{ env.APP_IMAGE_NAME }}"
elif [[ "$FULL_TAG" == docker.io/* ]]; then elif [[ "$FULL_TAG" == docker.io/* ]]; then

64
Cargo.lock generated
View File

@@ -115,6 +115,8 @@ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tokio-util", "tokio-util",
"tower",
"tower-http",
"tracing", "tracing",
"url", "url",
"uuid", "uuid",
@@ -305,6 +307,18 @@ dependencies = [
"rustversion", "rustversion",
] ]
[[package]]
name = "async-compression"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
dependencies = [
"compression-codecs",
"compression-core",
"pin-project-lite",
"tokio",
]
[[package]] [[package]]
name = "async-stream" name = "async-stream"
version = "0.3.6" version = "0.3.6"
@@ -711,6 +725,23 @@ dependencies = [
"static_assertions", "static_assertions",
] ]
[[package]]
name = "compression-codecs"
version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
dependencies = [
"compression-core",
"flate2",
"memchr",
]
[[package]]
name = "compression-core"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@@ -1485,6 +1516,12 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "http-range-header"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.10.1" version = "1.10.1"
@@ -2024,6 +2061,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]] [[package]]
name = "minimal-lexical" name = "minimal-lexical"
version = "0.2.1" version = "0.2.1"
@@ -3917,16 +3964,27 @@ version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [ dependencies = [
"async-compression",
"bitflags 2.11.0", "bitflags 2.11.0",
"bytes", "bytes",
"futures-core",
"futures-util", "futures-util",
"http", "http",
"http-body", "http-body",
"http-body-util",
"http-range-header",
"httpdate",
"iri-string", "iri-string",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite", "pin-project-lite",
"tokio",
"tokio-util",
"tower", "tower",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]
@@ -4073,6 +4131,12 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.18" version = "0.3.18"

View File

@@ -1,14 +1,17 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# 运行镜像:从 base 提取产物到精简运行时 # Aether 运行镜像Rust gateway 直接服务 API + 前端静态文件
# 构建命令: docker build -f Dockerfile.app -t aether-app:latest . # 构建命令: docker build -f Dockerfile.app -t aether-app:latest .
# 用于 GitHub Actions CI官方源 # 用于 GitHub Actions CI官方源
FROM aether-base:latest AS builder # ==================== 前端构建 ====================
WORKDIR /app FROM node:22-slim AS frontend-builder
# 复制前端源码并构建CI 通过 no-cache-filters=builder 确保每次重建) WORKDIR /app/frontend
COPY frontend/ ./frontend/ COPY frontend/package*.json ./
RUN cd frontend && npm run build RUN npm ci
COPY frontend/ ./
RUN npm run build
# ==================== Rust gateway 构建 ====================
FROM rust:1.86-slim AS gateway-builder FROM rust:1.86-slim AS gateway-builder
WORKDIR /build WORKDIR /build
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
@@ -26,244 +29,42 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
cp target/release/aether-gateway /tmp/aether-gateway cp target/release/aether-gateway /tmp/aether-gateway
# ==================== 运行时镜像 ==================== # ==================== 运行时镜像 ====================
FROM python:3.13-slim FROM debian:bookworm-slim
WORKDIR /app
# 运行时依赖(无 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 \
apt-get update && apt-get install -y --no-install-recommends \ apt-get update && apt-get install -y --no-install-recommends \
nginx \
supervisor \
libpq5 \
curl \ curl \
libjemalloc2 libjemalloc2 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN set -eux; \ RUN set -eux; \
jemalloc_path="$(find /usr/lib -type f -name 'libjemalloc.so.2' | head -n1)"; \ jemalloc_path="$(find /usr/lib -type f -name 'libjemalloc.so.2' | head -n1)"; \
[ -n "$jemalloc_path" ]; \ [ -n "$jemalloc_path" ]; \
ln -sf "$jemalloc_path" /usr/local/lib/libjemalloc.so.2 ln -sf "$jemalloc_path" /usr/local/lib/libjemalloc.so.2
# 从 base 镜像复制 Python 包
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages # 复制 gateway 二进制
# 只复制需要的 Python 可执行文件
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=gateway-builder /tmp/aether-gateway /usr/local/bin/aether-gateway COPY --from=gateway-builder /tmp/aether-gateway /usr/local/bin/aether-gateway
# 从 builder 阶段复制前端构建产物
COPY --from=builder /app/frontend/dist /usr/share/nginx/html # 复制前端构建产物
RUN chmod -R 755 /usr/share/nginx/html COPY --from=frontend-builder /app/frontend/dist /srv/frontend
# 复制后端代码 RUN chmod -R 755 /srv/frontend
COPY src/ ./src/
COPY alembic.ini ./ RUN mkdir -p /app/logs /app/data
COPY alembic/ ./alembic/ WORKDIR /app
COPY gunicorn_conf.py ./
# Nginx 配置模板 ENV LANG=C.UTF-8 \
# 策略:白名单后端路由 → 后端代理,其余全部 → 前端 SPAindex.html
# 智能处理 IP有外层代理头就透传没有就用直连 IP
RUN printf '%s\n' \
'map $http_x_real_ip $real_ip {' \
' default $http_x_real_ip;' \
' "" $remote_addr;' \
'}' \
'' \
'map $http_x_forwarded_for $forwarded_for {' \
' default $http_x_forwarded_for;' \
' "" $remote_addr;' \
'}' \
'' \
'server {' \
' listen 80;' \
' server_name _;' \
' root /usr/share/nginx/html;' \
' index index.html;' \
' client_max_body_size 100M;' \
'' \
' # gzip 压缩配置(对 base64 图片等非流式响应有效)' \
' gzip on;' \
' gzip_min_length 256;' \
' gzip_comp_level 5;' \
' gzip_vary on;' \
' gzip_proxied any;' \
' gzip_types application/json text/plain text/css text/javascript application/javascript application/octet-stream;' \
' gzip_disable "msie6";' \
'' \
' # 静态资源:长期缓存' \
' location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {' \
' expires 1y;' \
' add_header Cache-Control "public, no-transform";' \
' try_files $uri =404;' \
' }' \
'' \
' # 安全:阻止访问源码目录' \
' location ~ ^/(src|node_modules)/ {' \
' deny all;' \
' return 404;' \
' }' \
'' \
' # WebSocket 隧道端点gateway-owned tunnel 模式)' \
' location = /api/internal/proxy-tunnel {' \
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
' proxy_http_version 1.1;' \
' proxy_set_header Host $host;' \
' proxy_set_header X-Real-IP $real_ip;' \
' proxy_set_header X-Forwarded-For $forwarded_for;' \
' proxy_set_header X-Forwarded-Proto $scheme;' \
' proxy_set_header Upgrade $http_upgrade;' \
' proxy_set_header Connection "upgrade";' \
' # 剥离 CF 头,防止泄露给上游或返回给客户端' \
' proxy_hide_header CF-Connecting-IP;' \
' proxy_hide_header CF-IPCountry;' \
' proxy_hide_header CF-Ray;' \
' proxy_hide_header CF-Visitor;' \
' proxy_hide_header CDN-Loop;' \
' proxy_hide_header True-Client-IP;' \
' proxy_hide_header CF-Worker;' \
' proxy_hide_header CF-EW-Via;' \
' proxy_hide_header CF-Warp-Tag-ID;' \
' proxy_set_header CF-Connecting-IP "";' \
' proxy_set_header CF-IPCountry "";' \
' proxy_set_header CF-Ray "";' \
' proxy_set_header CF-Visitor "";' \
' proxy_set_header CDN-Loop "";' \
' proxy_set_header True-Client-IP "";' \
' proxy_set_header CF-Worker "";' \
' proxy_set_header CF-EW-Via "";' \
' proxy_set_header CF-Warp-Tag-ID "";' \
' proxy_read_timeout 86400s;' \
' proxy_send_timeout 86400s;' \
' }' \
'' \
' # 后端 API 路由(白名单)→ 代理到后端' \
' location ~ ^/(api|v1|v1beta|upload|health)(/|$) {' \
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
' proxy_http_version 1.1;' \
' proxy_set_header Host $host;' \
' proxy_set_header X-Real-IP $real_ip;' \
' proxy_set_header X-Forwarded-For $forwarded_for;' \
' proxy_set_header X-Forwarded-Proto $scheme;' \
' proxy_set_header Connection "";' \
' proxy_set_header Accept $http_accept;' \
' proxy_set_header Content-Type $content_type;' \
' proxy_set_header Authorization $http_authorization;' \
' proxy_set_header X-Api-Key $http_x_api_key;' \
' # 剥离 CF 头,防止泄露给上游或返回给客户端' \
' proxy_hide_header CF-Connecting-IP;' \
' proxy_hide_header CF-IPCountry;' \
' proxy_hide_header CF-Ray;' \
' proxy_hide_header CF-Visitor;' \
' proxy_hide_header CDN-Loop;' \
' proxy_hide_header True-Client-IP;' \
' proxy_hide_header CF-Worker;' \
' proxy_hide_header CF-EW-Via;' \
' proxy_hide_header CF-Warp-Tag-ID;' \
' proxy_set_header CF-Connecting-IP "";' \
' proxy_set_header CF-IPCountry "";' \
' proxy_set_header CF-Ray "";' \
' proxy_set_header CF-Visitor "";' \
' proxy_set_header CDN-Loop "";' \
' proxy_set_header True-Client-IP "";' \
' proxy_set_header CF-Worker "";' \
' proxy_set_header CF-EW-Via "";' \
' proxy_set_header CF-Warp-Tag-ID "";' \
' proxy_buffering off;' \
' proxy_cache off;' \
' proxy_request_buffering off;' \
' chunked_transfer_encoding on;' \
' gzip off;' \
' add_header X-Accel-Buffering no;' \
' proxy_connect_timeout 60s;' \
' proxy_send_timeout 3600s;' \
' proxy_read_timeout 3600s;' \
' }' \
'' \
' # API 文档路由 → 代理到后端' \
' location ~ ^/(docs|redoc|openapi\\.json)$ {' \
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
' proxy_http_version 1.1;' \
' proxy_set_header Host $host;' \
' proxy_set_header X-Real-IP $real_ip;' \
' proxy_set_header X-Forwarded-For $forwarded_for;' \
' proxy_set_header X-Forwarded-Proto $scheme;' \
' # 剥离 CF 头,防止泄露给上游或返回给客户端' \
' proxy_hide_header CF-Connecting-IP;' \
' proxy_hide_header CF-IPCountry;' \
' proxy_hide_header CF-Ray;' \
' proxy_hide_header CF-Visitor;' \
' proxy_hide_header CDN-Loop;' \
' proxy_hide_header True-Client-IP;' \
' proxy_hide_header CF-Worker;' \
' proxy_hide_header CF-EW-Via;' \
' proxy_hide_header CF-Warp-Tag-ID;' \
' proxy_set_header CF-Connecting-IP "";' \
' proxy_set_header CF-IPCountry "";' \
' proxy_set_header CF-Ray "";' \
' proxy_set_header CF-Visitor "";' \
' proxy_set_header CDN-Loop "";' \
' proxy_set_header True-Client-IP "";' \
' proxy_set_header CF-Worker "";' \
' proxy_set_header CF-EW-Via "";' \
' proxy_set_header CF-Warp-Tag-ID "";' \
' }' \
'' \
' # 所有其他路由 → 前端 SPA先尝试静态文件再回退到 index.html' \
' location / {' \
' try_files $uri $uri/ /index.html;' \
' }' \
'}' > /etc/nginx/sites-available/default.template
# Supervisor 配置
RUN printf '%s\n' \
'[supervisord]' \
'nodaemon=true' \
'logfile=/var/log/supervisor/supervisord.log' \
'pidfile=/var/run/supervisord.pid' \
'' \
'[program:nginx]' \
'command=/bin/bash -c "sed \"s/PORT_PLACEHOLDER/%(ENV_PORT)s/g\" /etc/nginx/sites-available/default.template > /etc/nginx/sites-available/default && /usr/sbin/nginx -g \"daemon off;\""' \
'autostart=true' \
'autorestart=true' \
'stdout_logfile=/var/log/nginx/access.log' \
'stderr_logfile=/var/log/nginx/error.log' \
'' \
'[program:gateway]' \
'command=/usr/local/bin/aether-gateway --bind 127.0.0.1:%(ENV_PORT)s --upstream http://127.0.0.1:%(ENV_LEGACY_APP_PORT)s' \
'directory=/app' \
'autostart=true' \
'autorestart=true' \
'stdout_logfile=/dev/stdout' \
'stdout_logfile_maxbytes=0' \
'stderr_logfile=/dev/stderr' \
'stderr_logfile_maxbytes=0' \
'environment=RUST_LOG=aether_gateway=info' \
'' \
'[program:app]' \
'command=/bin/bash -c "MAX_REQUESTS_JITTER=$((${MAX_REQUESTS:-50000}/20)); exec gunicorn src.main:app -c gunicorn_conf.py --preload -w %(ENV_GUNICORN_WORKERS)s -k uvicorn.workers.UvicornWorker --bind 127.0.0.1:%(ENV_LEGACY_APP_PORT)s --max-requests ${MAX_REQUESTS:-50000} --max-requests-jitter $MAX_REQUESTS_JITTER --access-logfile - --error-logfile - --log-level info"' \
'directory=/app' \
'autostart=true' \
'autorestart=true' \
'stdout_logfile=/dev/stdout' \
'stdout_logfile_maxbytes=0' \
'stderr_logfile=/dev/stderr' \
'stderr_logfile_maxbytes=0' \
'environment=PYTHONUNBUFFERED=1,PYTHONIOENCODING=utf-8,LANG=C.UTF-8,LC_ALL=C.UTF-8,DOCKER_CONTAINER=true,LD_PRELOAD=/usr/local/lib/libjemalloc.so.2,MALLOC_CONF="background_thread:true,dirty_decay_ms:5000,muzzy_decay_ms:5000"' > /etc/supervisor/conf.d/supervisord.conf
# 创建目录
RUN mkdir -p /var/log/supervisor /app/logs /app/data
# 入口脚本(启动前执行迁移)
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# 环境变量
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONIOENCODING=utf-8 \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \ LC_ALL=C.UTF-8 \
LD_PRELOAD=/usr/local/lib/libjemalloc.so.2 \ LD_PRELOAD=/usr/local/lib/libjemalloc.so.2 \
MALLOC_CONF=background_thread:true,dirty_decay_ms:5000,muzzy_decay_ms:5000 \ MALLOC_CONF=background_thread:true,dirty_decay_ms:5000,muzzy_decay_ms:5000 \
PORT=8084 \ RUST_LOG=aether_gateway=info \
LEGACY_APP_PORT=18084 \ AETHER_GATEWAY_BIND=0.0.0.0:80 \
GUNICORN_WORKERS=2 \ AETHER_GATEWAY_STATIC_DIR=/srv/frontend
MAX_REQUESTS=4000
EXPOSE 80 EXPOSE 80
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost/health || exit 1 CMD curl -f http://localhost/health || exit 1
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] ENTRYPOINT ["/usr/local/bin/aether-gateway"]

View File

@@ -1,16 +1,16 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# 运行镜像:从 base 提取产物到精简运行时(国内镜像源版本) # Aether 运行镜像Rust gateway 直接服务 API + 前端静态文件(国内镜像源版本)
# 构建命令: docker build -f Dockerfile.app.local -t aether-app:latest . # 构建命令: docker build -f Dockerfile.app.local -t aether-app:latest .
# 用于本地/国内服务器部署
FROM aether-base:latest AS builder # ==================== 前端构建 ====================
FROM node:22-slim AS frontend-builder
WORKDIR /app WORKDIR /app/frontend
COPY frontend/package*.json ./
# 复制前端源码并构建 RUN npm config set registry https://registry.npmmirror.com && npm ci
COPY frontend/ ./frontend/ COPY frontend/ ./
RUN cd frontend && npm run build RUN npm run build
# ==================== Rust gateway 构建 ====================
FROM rust:1.86-slim AS gateway-builder FROM rust:1.86-slim AS gateway-builder
WORKDIR /build WORKDIR /build
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
@@ -29,258 +29,43 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
cp target/release/aether-gateway /tmp/aether-gateway cp target/release/aether-gateway /tmp/aether-gateway
# ==================== 运行时镜像 ==================== # ==================== 运行时镜像 ====================
FROM python:3.13-slim FROM debian:bookworm-slim
WORKDIR /app
# 运行时依赖(使用清华镜像源 + 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 \
sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources && \ sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources && \
apt-get update && apt-get install -y --no-install-recommends \ apt-get update && apt-get install -y --no-install-recommends \
nginx \
supervisor \
libpq5 \
curl \ curl \
libjemalloc2 libjemalloc2 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN set -eux; \ RUN set -eux; \
jemalloc_path="$(find /usr/lib -type f -name 'libjemalloc.so.2' | head -n1)"; \ jemalloc_path="$(find /usr/lib -type f -name 'libjemalloc.so.2' | head -n1)"; \
[ -n "$jemalloc_path" ]; \ [ -n "$jemalloc_path" ]; \
ln -sf "$jemalloc_path" /usr/local/lib/libjemalloc.so.2 ln -sf "$jemalloc_path" /usr/local/lib/libjemalloc.so.2
# 从 base 镜像复制 Python 包 # 复制 gateway 二进制
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
# 只复制需要的 Python 可执行文件
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=gateway-builder /tmp/aether-gateway /usr/local/bin/aether-gateway COPY --from=gateway-builder /tmp/aether-gateway /usr/local/bin/aether-gateway
# 从 builder 阶段复制前端构建产物 # 复制前端构建产物
COPY --from=builder /app/frontend/dist /usr/share/nginx/html COPY --from=frontend-builder /app/frontend/dist /srv/frontend
RUN chmod -R 755 /usr/share/nginx/html RUN chmod -R 755 /srv/frontend
# 复制后端代码 RUN mkdir -p /app/logs /app/data
COPY src/ ./src/ WORKDIR /app
COPY alembic.ini ./
COPY alembic/ ./alembic/
COPY gunicorn_conf.py ./
# Nginx 配置模板 ENV LANG=C.UTF-8 \
# 策略:白名单后端路由 → 后端代理,其余全部 → 前端 SPAindex.html
# 智能处理 IP有外层代理头就透传没有就用直连 IP
RUN printf '%s\n' \
'map $http_x_real_ip $real_ip {' \
' default $http_x_real_ip;' \
' "" $remote_addr;' \
'}' \
'' \
'map $http_x_forwarded_for $forwarded_for {' \
' default $http_x_forwarded_for;' \
' "" $remote_addr;' \
'}' \
'' \
'server {' \
' listen 80;' \
' server_name _;' \
' root /usr/share/nginx/html;' \
' index index.html;' \
' client_max_body_size 100M;' \
'' \
' # gzip 压缩配置(对 base64 图片等非流式响应有效)' \
' gzip on;' \
' gzip_min_length 256;' \
' gzip_comp_level 5;' \
' gzip_vary on;' \
' gzip_proxied any;' \
' gzip_types application/json text/plain text/css text/javascript application/javascript application/octet-stream;' \
' gzip_disable "msie6";' \
'' \
' # 静态资源:长期缓存' \
' location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {' \
' expires 1y;' \
' add_header Cache-Control "public, no-transform";' \
' try_files $uri =404;' \
' }' \
'' \
' # 安全:阻止访问源码目录' \
' location ~ ^/(src|node_modules)/ {' \
' deny all;' \
' return 404;' \
' }' \
'' \
' # WebSocket 隧道端点gateway-owned tunnel 模式)' \
' location = /api/internal/proxy-tunnel {' \
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
' proxy_http_version 1.1;' \
' proxy_set_header Host $host;' \
' proxy_set_header X-Real-IP $real_ip;' \
' proxy_set_header X-Forwarded-For $forwarded_for;' \
' proxy_set_header X-Forwarded-Proto $scheme;' \
' proxy_set_header Upgrade $http_upgrade;' \
' proxy_set_header Connection "upgrade";' \
' # 剥离 CF 头,防止泄露给上游或返回给客户端' \
' proxy_hide_header CF-Connecting-IP;' \
' proxy_hide_header CF-IPCountry;' \
' proxy_hide_header CF-Ray;' \
' proxy_hide_header CF-Visitor;' \
' proxy_hide_header CDN-Loop;' \
' proxy_hide_header True-Client-IP;' \
' proxy_hide_header CF-Worker;' \
' proxy_hide_header CF-EW-Via;' \
' proxy_hide_header CF-Warp-Tag-ID;' \
' proxy_set_header CF-Connecting-IP "";' \
' proxy_set_header CF-IPCountry "";' \
' proxy_set_header CF-Ray "";' \
' proxy_set_header CF-Visitor "";' \
' proxy_set_header CDN-Loop "";' \
' proxy_set_header True-Client-IP "";' \
' proxy_set_header CF-Worker "";' \
' proxy_set_header CF-EW-Via "";' \
' proxy_set_header CF-Warp-Tag-ID "";' \
' proxy_read_timeout 86400s;' \
' proxy_send_timeout 86400s;' \
' }' \
'' \
' # 后端 API 路由(白名单)→ 代理到后端' \
' location ~ ^/(api|v1|v1beta|upload|health)(/|$) {' \
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
' proxy_http_version 1.1;' \
' proxy_set_header Host $host;' \
' proxy_set_header X-Real-IP $real_ip;' \
' proxy_set_header X-Forwarded-For $forwarded_for;' \
' proxy_set_header X-Forwarded-Proto $scheme;' \
' proxy_set_header Connection "";' \
' proxy_set_header Accept $http_accept;' \
' proxy_set_header Content-Type $content_type;' \
' proxy_set_header Authorization $http_authorization;' \
' proxy_set_header X-Api-Key $http_x_api_key;' \
' # 剥离 CF 头,防止泄露给上游或返回给客户端' \
' proxy_hide_header CF-Connecting-IP;' \
' proxy_hide_header CF-IPCountry;' \
' proxy_hide_header CF-Ray;' \
' proxy_hide_header CF-Visitor;' \
' proxy_hide_header CDN-Loop;' \
' proxy_hide_header True-Client-IP;' \
' proxy_hide_header CF-Worker;' \
' proxy_hide_header CF-EW-Via;' \
' proxy_hide_header CF-Warp-Tag-ID;' \
' proxy_set_header CF-Connecting-IP "";' \
' proxy_set_header CF-IPCountry "";' \
' proxy_set_header CF-Ray "";' \
' proxy_set_header CF-Visitor "";' \
' proxy_set_header CDN-Loop "";' \
' proxy_set_header True-Client-IP "";' \
' proxy_set_header CF-Worker "";' \
' proxy_set_header CF-EW-Via "";' \
' proxy_set_header CF-Warp-Tag-ID "";' \
' proxy_buffering off;' \
' proxy_cache off;' \
' proxy_request_buffering off;' \
' chunked_transfer_encoding on;' \
' gzip off;' \
' add_header X-Accel-Buffering no;' \
' proxy_connect_timeout 60s;' \
' proxy_send_timeout 3600s;' \
' proxy_read_timeout 3600s;' \
' }' \
'' \
' # API 文档路由 → 代理到后端' \
' location ~ ^/(docs|redoc|openapi\\.json)$ {' \
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
' proxy_http_version 1.1;' \
' proxy_set_header Host $host;' \
' proxy_set_header X-Real-IP $real_ip;' \
' proxy_set_header X-Forwarded-For $forwarded_for;' \
' proxy_set_header X-Forwarded-Proto $scheme;' \
' # 剥离 CF 头,防止泄露给上游或返回给客户端' \
' proxy_hide_header CF-Connecting-IP;' \
' proxy_hide_header CF-IPCountry;' \
' proxy_hide_header CF-Ray;' \
' proxy_hide_header CF-Visitor;' \
' proxy_hide_header CDN-Loop;' \
' proxy_hide_header True-Client-IP;' \
' proxy_hide_header CF-Worker;' \
' proxy_hide_header CF-EW-Via;' \
' proxy_hide_header CF-Warp-Tag-ID;' \
' proxy_set_header CF-Connecting-IP "";' \
' proxy_set_header CF-IPCountry "";' \
' proxy_set_header CF-Ray "";' \
' proxy_set_header CF-Visitor "";' \
' proxy_set_header CDN-Loop "";' \
' proxy_set_header True-Client-IP "";' \
' proxy_set_header CF-Worker "";' \
' proxy_set_header CF-EW-Via "";' \
' proxy_set_header CF-Warp-Tag-ID "";' \
' }' \
'' \
' # 所有其他路由 → 前端 SPA先尝试静态文件再回退到 index.html' \
' location / {' \
' try_files $uri $uri/ /index.html;' \
' }' \
'}' > /etc/nginx/sites-available/default.template
# Supervisor 配置
RUN printf '%s\n' \
'[supervisord]' \
'nodaemon=true' \
'logfile=/var/log/supervisor/supervisord.log' \
'pidfile=/var/run/supervisord.pid' \
'' \
'[program:nginx]' \
'command=/bin/bash -c "sed \"s/PORT_PLACEHOLDER/%(ENV_PORT)s/g\" /etc/nginx/sites-available/default.template > /etc/nginx/sites-available/default && /usr/sbin/nginx -g \"daemon off;\""' \
'autostart=true' \
'autorestart=true' \
'stdout_logfile=/var/log/nginx/access.log' \
'stderr_logfile=/var/log/nginx/error.log' \
'' \
'[program:gateway]' \
'command=/usr/local/bin/aether-gateway --bind 127.0.0.1:%(ENV_PORT)s --upstream http://127.0.0.1:%(ENV_LEGACY_APP_PORT)s' \
'directory=/app' \
'autostart=true' \
'autorestart=true' \
'stdout_logfile=/dev/stdout' \
'stdout_logfile_maxbytes=0' \
'stderr_logfile=/dev/stderr' \
'stderr_logfile_maxbytes=0' \
'environment=RUST_LOG=aether_gateway=info' \
'' \
'[program:app]' \
'command=/bin/bash -c "MAX_REQUESTS_JITTER=$((${MAX_REQUESTS:-50000}/20)); exec gunicorn src.main:app -c gunicorn_conf.py --preload -w %(ENV_GUNICORN_WORKERS)s -k uvicorn.workers.UvicornWorker --bind 127.0.0.1:%(ENV_LEGACY_APP_PORT)s --max-requests ${MAX_REQUESTS:-50000} --max-requests-jitter $MAX_REQUESTS_JITTER --access-logfile - --error-logfile - --log-level info"' \
'directory=/app' \
'autostart=true' \
'autorestart=true' \
'stdout_logfile=/dev/stdout' \
'stdout_logfile_maxbytes=0' \
'stderr_logfile=/dev/stderr' \
'stderr_logfile_maxbytes=0' \
'environment=PYTHONUNBUFFERED=1,PYTHONIOENCODING=utf-8,LANG=C.UTF-8,LC_ALL=C.UTF-8,DOCKER_CONTAINER=true,LD_PRELOAD=/usr/local/lib/libjemalloc.so.2,MALLOC_CONF="background_thread:true,dirty_decay_ms:5000,muzzy_decay_ms:5000"' > /etc/supervisor/conf.d/supervisord.conf
# 创建目录
RUN mkdir -p /var/log/supervisor /app/logs /app/data
# 入口脚本(启动前执行迁移)
COPY entrypoint.sh /entrypoint.sh
RUN sed -i 's/\r$//' /entrypoint.sh && chmod +x /entrypoint.sh
# 环境变量
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONIOENCODING=utf-8 \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \ LC_ALL=C.UTF-8 \
LD_PRELOAD=/usr/local/lib/libjemalloc.so.2 \ LD_PRELOAD=/usr/local/lib/libjemalloc.so.2 \
MALLOC_CONF=background_thread:true,dirty_decay_ms:5000,muzzy_decay_ms:5000 \ MALLOC_CONF=background_thread:true,dirty_decay_ms:5000,muzzy_decay_ms:5000 \
PORT=8084 \ RUST_LOG=aether_gateway=info \
LEGACY_APP_PORT=18084 \ AETHER_GATEWAY_BIND=0.0.0.0:80 \
GUNICORN_WORKERS=2 \ AETHER_GATEWAY_STATIC_DIR=/srv/frontend
MAX_REQUESTS=4000
EXPOSE 80 EXPOSE 80
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost/health || exit 1 CMD curl -f http://localhost/health || exit 1
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/usr/local/bin/aether-gateway"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@@ -1,28 +0,0 @@
# syntax=docker/dockerfile:1
# 构建镜像:编译环境 + 预编译的依赖
# 用于 GitHub Actions CI 构建(不使用国内镜像源)
# 构建命令: docker build -f Dockerfile.base -t aether-base:latest .
# 只在 pyproject.toml 或 frontend/package*.json 变化时需要重建
FROM python:3.13-slim
WORKDIR /app
# 构建工具(使用 BuildKit 缓存加速)
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \
gcc \
nodejs \
npm
# Python 依赖(使用 BuildKit 缓存加速)
COPY pyproject.toml README.md ./
RUN --mount=type=cache,target=/root/.cache/pip \
mkdir -p src && touch src/__init__.py && \
SETUPTOOLS_SCM_PRETEND_VERSION=0.1.0 pip install .
# 前端依赖(只安装,不构建,使用 BuildKit 缓存加速)
COPY frontend/package*.json ./frontend/
RUN --mount=type=cache,target=/root/.npm \
cd frontend && npm ci

View File

@@ -1,31 +0,0 @@
# syntax=docker/dockerfile:1
# 构建镜像:编译环境 + 预编译的依赖(国内镜像源版本)
# 构建命令: docker build -f Dockerfile.base.local -t aether-base:latest .
# 只在 pyproject.toml 或 frontend/package*.json 变化时需要重建
FROM python:3.13-slim
WORKDIR /app
# 构建工具(使用清华镜像源 + BuildKit 缓存加速)
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources && \
apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \
gcc \
nodejs \
npm
# pip 镜像源
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
# Python 依赖(使用 BuildKit 缓存加速)
COPY pyproject.toml README.md ./
RUN --mount=type=cache,target=/root/.cache/pip \
mkdir -p src && touch src/__init__.py && \
SETUPTOOLS_SCM_PRETEND_VERSION=0.1.0 pip install .
# 前端依赖(只安装,不构建,使用淘宝镜像源 + BuildKit 缓存加速)
COPY frontend/package*.json ./frontend/
RUN --mount=type=cache,target=/root/.npm \
cd frontend && npm config set registry https://registry.npmmirror.com && npm ci

View File

@@ -0,0 +1,24 @@
# file generated by vcs-versioning
# don't change, don't track in version control
from __future__ import annotations
__all__ = [
"__version__",
"__version_tuple__",
"version",
"version_tuple",
"__commit_id__",
"commit_id",
]
version: str
__version__: str
__version_tuple__: tuple[int | str, ...]
version_tuple: tuple[int | str, ...]
commit_id: str | None
__commit_id__: str | None
__version__ = version = '0.6.4.dev6+gddf18fed9.d20260331'
__version_tuple__ = version_tuple = (0, 6, 4, 'dev6', 'gddf18fed9.d20260331')
__commit_id__ = commit_id = None

Some files were not shown because too many files have changed in this diff Show More