feat(proxy): CI 添加 Docker 构建推送和 README 自动更新,HardwareTooltip 增强字段兼容

- build-proxy workflow 新增 docker job 构建多架构镜像推送 GHCR/Docker Hub
- build-proxy workflow 新增 update-readme job 自动更新下载链接表格
- Dockerfile 改用预编译二进制替代多阶段 Rust 编译
- 新增 docker-compose.yml 简化部署
- .env.example 精简为必要配置项
- README 补充 Docker 部署说明和下载表格标记
- HardwareTooltip 兼容 camelCase/snake_case 字段名,添加 pickRecord 辅助函数和原生 tooltip 回退
This commit is contained in:
fawney19
2026-02-22 01:13:03 +08:00
parent cf2eee222e
commit 5da9197eee
6 changed files with 242 additions and 63 deletions

View File

@@ -1,4 +1,4 @@
name: Build aether-proxy Binaries
name: Build aether-proxy
on:
push:
@@ -7,6 +7,12 @@ on:
permissions:
contents: write
packages: write
env:
REGISTRY: ghcr.io
GHCR_IMAGE: fawney19/aether-proxy
DOCKERHUB_IMAGE: fawney19/aether-proxy
jobs:
build:
@@ -113,3 +119,109 @@ jobs:
artifacts/aether-proxy-*
artifacts/SHA256SUMS.txt
fail_on_unmatched_files: true
docker:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v4
- name: Download Linux artifacts
uses: actions/download-artifact@v4
with:
pattern: aether-proxy-linux-*
merge-multiple: true
path: artifacts
- name: Prepare binaries
run: |
mkdir -p aether-proxy/build/linux-amd64 aether-proxy/build/linux-arm64
tar xzf artifacts/aether-proxy-linux-amd64.tar.gz -C aether-proxy/build/linux-amd64
tar xzf artifacts/aether-proxy-linux-arm64.tar.gz -C aether-proxy/build/linux-arm64
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.GHCR_IMAGE }}
docker.io/${{ env.DOCKERHUB_IMAGE }}
tags: |
type=match,pattern=proxy-v(.*),group=1
type=match,pattern=proxy-v(\d+\.\d+),group=1
type=sha,prefix=
flavor: |
latest=auto
- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./aether-proxy
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
update-readme:
needs: release
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v4
with:
ref: master
- name: Update README download links
env:
TAG: ${{ github.ref_name }}
run: |
VERSION="${TAG#proxy-v}"
BASE="https://github.com/fawney19/Aether/releases/download/${TAG}"
cd aether-proxy
TABLE="| Platform | Download |\n|----------|----------|\n"
TABLE+="| Linux x86_64 | [aether-proxy-linux-amd64.tar.gz](${BASE}/aether-proxy-linux-amd64.tar.gz) |\n"
TABLE+="| Linux ARM64 | [aether-proxy-linux-arm64.tar.gz](${BASE}/aether-proxy-linux-arm64.tar.gz) |\n"
TABLE+="| macOS x86_64 | [aether-proxy-macos-amd64.tar.gz](${BASE}/aether-proxy-macos-amd64.tar.gz) |\n"
TABLE+="| macOS ARM64 | [aether-proxy-macos-arm64.tar.gz](${BASE}/aether-proxy-macos-arm64.tar.gz) |\n"
TABLE+="| Windows x86_64 | [aether-proxy-windows-amd64.zip](${BASE}/aether-proxy-windows-amd64.zip) |"
# Replace content between markers
if grep -q '<!-- DOWNLOAD_TABLE_START -->' README.md; then
awk -v table="$TABLE" '
/<!-- DOWNLOAD_TABLE_START -->/ { print; printf "%s\n", table; skip=1; next }
/<!-- DOWNLOAD_TABLE_END -->/ { skip=0 }
!skip { print }
' README.md > README.tmp && mv README.tmp README.md
fi
- name: Commit and push
run: |
cd aether-proxy
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add README.md
git diff --cached --quiet && exit 0
TAG="${GITHUB_REF#refs/tags/}"
git commit -m "chore(proxy): update download links for ${TAG}"
git push

View File

@@ -10,22 +10,5 @@ AETHER_PROXY_HMAC_KEY=
# Proxy listen port
AETHER_PROXY_LISTEN_PORT=18080
# Public IP (auto-detected if omitted)
# AETHER_PROXY_PUBLIC_IP=203.0.113.42
# Node identification
AETHER_PROXY_NODE_NAME=proxy-01
# AETHER_PROXY_NODE_REGION=ap-northeast-1
# Heartbeat interval in seconds
AETHER_PROXY_HEARTBEAT_INTERVAL=30
# Allowed destination ports (comma-separated)
AETHER_PROXY_ALLOWED_PORTS=80,443,8080,8443
# HMAC timestamp tolerance in seconds
AETHER_PROXY_TIMESTAMP_TOLERANCE=300
# Logging
AETHER_PROXY_LOG_LEVEL=info
AETHER_PROXY_LOG_JSON=false

View File

@@ -1,24 +1,11 @@
FROM rust:1.83-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
COPY Cargo.toml Cargo.lock* ./
# Create dummy main.rs for dependency caching
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release 2>/dev/null || true
COPY src/ src/
# Touch main.rs to force rebuild with real source
RUN touch src/main.rs
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
ARG TARGETARCH
COPY --from=builder /app/target/release/aether-proxy /usr/local/bin/aether-proxy
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY build/linux-${TARGETARCH}/aether-proxy /usr/local/bin/aether-proxy
EXPOSE 18080

View File

@@ -4,9 +4,23 @@ Aether 正向代理节点,部署在海外 VPS 上,为墙内的 Aether 实例
## 安装
### Docker 部署
```bash
# 拉取镜像
docker pull ghcr.io/fawney19/aether-proxy:latest
# 或使用 docker compose
cp .env.example .env
# 编辑 .env 填入 AETHER_PROXY_AETHER_URL, MANAGEMENT_TOKEN, HMAC_KEY
docker compose up -d
```
### 下载预编译二进制
在 [GitHub Releases](https://github.com/fawney19/Aether/releases) 页面下载对应平台的预编译文件,无需安装 Rust 环境。
<!-- DOWNLOAD_TABLE_START -->
在 [GitHub Releases](https://github.com/fawney19/Aether/releases?q=proxy-v) 页面下载对应平台的预编译文件,无需安装 Rust 环境。
<!-- DOWNLOAD_TABLE_END -->
## 快速开始
@@ -89,7 +103,10 @@ sudo aether-proxy uninstall
## 发布新版本
推送 `proxy-v*` 格式的 tagGitHub Actions 会自动编译所有平台并发布到 Releases
推送 `proxy-v*` 格式的 tagGitHub Actions 会自动:
- 编译所有平台二进制并发布到 Releases
- 构建 Docker 镜像并推送到 GHCR 和 Docker Hub
- 更新 README 中的下载链接表格
```bash
git tag proxy-v0.1.0

View File

@@ -0,0 +1,17 @@
services:
aether-proxy:
image: ghcr.io/fawney19/aether-proxy:latest
container_name: aether-proxy
restart: unless-stopped
ports:
- "${AETHER_PROXY_LISTEN_PORT:-18080}:18080"
env_file:
- .env
environment:
AETHER_PROXY_LISTEN_PORT: 18080
AETHER_PROXY_LOG_JSON: "true"
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"

View File

@@ -7,55 +7,77 @@ import { computed } from 'vue'
const props = defineProps<{ node: ProxyNode }>()
const hardwareInfo = computed<Record<string, unknown> | null>(() => {
const info = props.node.hardware_info
if (info == null) return null
if (typeof info === 'string') {
try {
const parsed = JSON.parse(info)
if (parsed && typeof parsed === 'object') return parsed as Record<string, unknown>
} catch {
return {}
}
return {}
}
if (typeof info === 'object') return info as Record<string, unknown>
return {}
return normalizeHardwareInfo(props.node.hardware_info)
})
const hardwareRows = computed(() => {
const info = hardwareInfo.value ?? {}
const rows: Array<{ label: string; value: string }> = []
const cpuObj = info.cpu as Record<string, unknown> | undefined
const cpuCores = pickNumber(info.cpu_cores, info.cpu_count, cpuObj?.cores)
const cpuObj = pickRecord(info.cpu, info.cpu_info, info.cpuInfo)
const cpuCores = pickNumber(
info.cpu_cores,
info.cpuCores,
info.cpu_count,
info.cpuCount,
cpuObj?.cores,
cpuObj?.core_count,
cpuObj?.coreCount
)
if (cpuCores != null) {
rows.push({ label: 'CPU', value: `${cpuCores} cores` })
}
const memObj = info.memory as Record<string, unknown> | undefined
const memObj = pickRecord(info.memory, info.mem, info.ram)
const memoryMb = pickNumber(
info.total_memory_mb,
info.totalMemoryMb,
info.memory_total_mb,
info.memoryTotalMb,
info.memory_mb,
memObj?.total_mb
info.memoryMb,
memObj?.total_mb,
memObj?.totalMb,
memObj?.mb
)
if (memoryMb != null) {
rows.push({ label: 'RAM', value: formatMemory(memoryMb) })
}
const osInfo = pickString(info.os_info, info.os, info.platform)
const osObj = pickRecord(info.os, info.os_info, info.osInfo, info.platform_info, info.platformInfo)
const osInfo = pickString(
info.os_info,
info.osInfo,
info.os,
info.platform,
osObj?.display,
osObj?.name && osObj?.version ? `${String(osObj.name)} ${String(osObj.version)}` : null,
osObj?.name
)
if (osInfo) {
rows.push({ label: 'OS', value: osInfo })
}
if (props.node.estimated_max_concurrency != null) {
const maxConcurrency = pickNumber(
props.node.estimated_max_concurrency,
info.estimated_max_concurrency,
info.estimatedMaxConcurrency
)
if (maxConcurrency != null) {
rows.push({
label: 'Max Concurrency',
value: `~${formatNumber(props.node.estimated_max_concurrency)}`,
value: `~${formatNumber(maxConcurrency)}`,
})
}
const fdLimit = pickNumber(info.fd_limit, info.file_descriptor_limit, info.ulimit_nofile)
const fdLimit = pickNumber(
info.fd_limit,
info.fdLimit,
info.file_descriptor_limit,
info.fileDescriptorLimit,
info.ulimit_nofile,
info.ulimitNofile
)
if (fdLimit != null) {
rows.push({ label: 'FD Limit', value: formatNumber(fdLimit) })
}
@@ -63,6 +85,12 @@ const hardwareRows = computed(() => {
return rows
})
const nativeTooltipText = computed(() =>
hardwareRows.value.length > 0
? hardwareRows.value.map((row) => `${row.label}: ${row.value}`).join('\n')
: '暂无硬件信息上报'
)
const showHardwareInfo = computed(
() =>
!props.node.is_manual
@@ -81,6 +109,31 @@ function formatNumber(n: number) {
return String(n)
}
function normalizeHardwareInfo(info: unknown): Record<string, unknown> | null {
if (info == null) return null
let current: unknown = info
for (let i = 0; i < 3; i++) {
if (typeof current !== 'string') break
const text = current.trim()
if (!text) return {}
try {
current = JSON.parse(text)
} catch {
return {}
}
}
if (!current || typeof current !== 'object') {
return {}
}
const obj = current as Record<string, unknown>
const nested = pickRecord(obj.hardware_info, obj.hardwareInfo, obj.hardware)
if (nested) return nested
return obj
}
function pickNumber(...values: unknown[]): number | null {
for (const value of values) {
if (value == null) continue
@@ -90,6 +143,15 @@ function pickNumber(...values: unknown[]): number | null {
return null
}
function pickRecord(...values: unknown[]): Record<string, unknown> | null {
for (const value of values) {
if (value && typeof value === 'object') {
return value as Record<string, unknown>
}
}
return null
}
function pickString(...values: unknown[]): string {
for (const value of values) {
if (value == null) continue
@@ -99,7 +161,6 @@ function pickString(...values: unknown[]): string {
return ''
}
</script>
<template>
@@ -109,12 +170,14 @@ function pickString(...values: unknown[]): string {
>
<Tooltip>
<TooltipTrigger as-child>
<span
<button
type="button"
aria-label="硬件信息"
class="inline-flex items-center justify-center rounded-sm p-0.5 hover:bg-muted/60 transition-colors cursor-help"
:title="nativeTooltipText"
class="inline-flex items-center justify-center rounded-sm p-0.5 hover:bg-muted/60 transition-colors cursor-pointer"
>
<Cpu class="h-3.5 w-3.5 text-muted-foreground" />
</span>
</button>
</TooltipTrigger>
<TooltipContent
side="right"