mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
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:
114
.github/workflows/build-proxy.yml
vendored
114
.github/workflows/build-proxy.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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*` 格式的 tag,GitHub Actions 会自动编译所有平台并发布到 Releases:
|
||||
推送 `proxy-v*` 格式的 tag,GitHub Actions 会自动:
|
||||
- 编译所有平台二进制并发布到 Releases
|
||||
- 构建 Docker 镜像并推送到 GHCR 和 Docker Hub
|
||||
- 更新 README 中的下载链接表格
|
||||
|
||||
```bash
|
||||
git tag proxy-v0.1.0
|
||||
|
||||
17
aether-proxy/docker-compose.yml
Normal file
17
aether-proxy/docker-compose.yml
Normal 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"
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user