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

@@ -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"