mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +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:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -7,6 +7,12 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
GHCR_IMAGE: fawney19/aether-proxy
|
||||||
|
DOCKERHUB_IMAGE: fawney19/aether-proxy
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -113,3 +119,109 @@ jobs:
|
|||||||
artifacts/aether-proxy-*
|
artifacts/aether-proxy-*
|
||||||
artifacts/SHA256SUMS.txt
|
artifacts/SHA256SUMS.txt
|
||||||
fail_on_unmatched_files: true
|
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
|
# Proxy listen port
|
||||||
AETHER_PROXY_LISTEN_PORT=18080
|
AETHER_PROXY_LISTEN_PORT=18080
|
||||||
|
|
||||||
# Public IP (auto-detected if omitted)
|
|
||||||
# AETHER_PROXY_PUBLIC_IP=203.0.113.42
|
|
||||||
|
|
||||||
# Node identification
|
# Node identification
|
||||||
AETHER_PROXY_NODE_NAME=proxy-01
|
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
|
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
|
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
|
```bash
|
||||||
git tag proxy-v0.1.0
|
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 props = defineProps<{ node: ProxyNode }>()
|
||||||
|
|
||||||
const hardwareInfo = computed<Record<string, unknown> | null>(() => {
|
const hardwareInfo = computed<Record<string, unknown> | null>(() => {
|
||||||
const info = props.node.hardware_info
|
return normalizeHardwareInfo(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 {}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const hardwareRows = computed(() => {
|
const hardwareRows = computed(() => {
|
||||||
const info = hardwareInfo.value ?? {}
|
const info = hardwareInfo.value ?? {}
|
||||||
const rows: Array<{ label: string; value: string }> = []
|
const rows: Array<{ label: string; value: string }> = []
|
||||||
|
|
||||||
const cpuObj = info.cpu as Record<string, unknown> | undefined
|
const cpuObj = pickRecord(info.cpu, info.cpu_info, info.cpuInfo)
|
||||||
const cpuCores = pickNumber(info.cpu_cores, info.cpu_count, cpuObj?.cores)
|
const cpuCores = pickNumber(
|
||||||
|
info.cpu_cores,
|
||||||
|
info.cpuCores,
|
||||||
|
info.cpu_count,
|
||||||
|
info.cpuCount,
|
||||||
|
cpuObj?.cores,
|
||||||
|
cpuObj?.core_count,
|
||||||
|
cpuObj?.coreCount
|
||||||
|
)
|
||||||
if (cpuCores != null) {
|
if (cpuCores != null) {
|
||||||
rows.push({ label: 'CPU', value: `${cpuCores} cores` })
|
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(
|
const memoryMb = pickNumber(
|
||||||
info.total_memory_mb,
|
info.total_memory_mb,
|
||||||
|
info.totalMemoryMb,
|
||||||
info.memory_total_mb,
|
info.memory_total_mb,
|
||||||
|
info.memoryTotalMb,
|
||||||
info.memory_mb,
|
info.memory_mb,
|
||||||
memObj?.total_mb
|
info.memoryMb,
|
||||||
|
memObj?.total_mb,
|
||||||
|
memObj?.totalMb,
|
||||||
|
memObj?.mb
|
||||||
)
|
)
|
||||||
if (memoryMb != null) {
|
if (memoryMb != null) {
|
||||||
rows.push({ label: 'RAM', value: formatMemory(memoryMb) })
|
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) {
|
if (osInfo) {
|
||||||
rows.push({ label: 'OS', value: 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({
|
rows.push({
|
||||||
label: 'Max Concurrency',
|
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) {
|
if (fdLimit != null) {
|
||||||
rows.push({ label: 'FD Limit', value: formatNumber(fdLimit) })
|
rows.push({ label: 'FD Limit', value: formatNumber(fdLimit) })
|
||||||
}
|
}
|
||||||
@@ -63,6 +85,12 @@ const hardwareRows = computed(() => {
|
|||||||
return rows
|
return rows
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const nativeTooltipText = computed(() =>
|
||||||
|
hardwareRows.value.length > 0
|
||||||
|
? hardwareRows.value.map((row) => `${row.label}: ${row.value}`).join('\n')
|
||||||
|
: '暂无硬件信息上报'
|
||||||
|
)
|
||||||
|
|
||||||
const showHardwareInfo = computed(
|
const showHardwareInfo = computed(
|
||||||
() =>
|
() =>
|
||||||
!props.node.is_manual
|
!props.node.is_manual
|
||||||
@@ -81,6 +109,31 @@ function formatNumber(n: number) {
|
|||||||
return String(n)
|
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 {
|
function pickNumber(...values: unknown[]): number | null {
|
||||||
for (const value of values) {
|
for (const value of values) {
|
||||||
if (value == null) continue
|
if (value == null) continue
|
||||||
@@ -90,6 +143,15 @@ function pickNumber(...values: unknown[]): number | null {
|
|||||||
return 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 {
|
function pickString(...values: unknown[]): string {
|
||||||
for (const value of values) {
|
for (const value of values) {
|
||||||
if (value == null) continue
|
if (value == null) continue
|
||||||
@@ -99,7 +161,6 @@ function pickString(...values: unknown[]): string {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -109,12 +170,14 @@ function pickString(...values: unknown[]): string {
|
|||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<span
|
<button
|
||||||
|
type="button"
|
||||||
aria-label="硬件信息"
|
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" />
|
<Cpu class="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
</span>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
side="right"
|
side="right"
|
||||||
|
|||||||
Reference in New Issue
Block a user