feat(fingerprint): 引入 per-key 请求指纹系统,替代全局 TLS 指纹开关

为每个 ProviderAPIKey 生成并持久化独立的请求指纹配置,涵盖 TLS impersonate
profile、浏览器 UA、Stainless SDK 头部、Node/Chrome/Electron 版本等维度。
指纹基于 key ID 确定性生成,支持手动编辑和批量重新生成。

- 新增 fingerprint 模块:生成、加载、校验、懒持久化
- 数据库迁移:provider_api_keys 新增 fingerprint JSON 列
- 请求链路注入:handler 基类设置上下文指纹,request_builder 和 envelope 消费
- HTTP Client 支持动态 impersonate profile 选择
- Antigravity 适配器使用指纹覆盖 UA/session/Node 版本
- 前端移除手动 TLS 指纹开关,新增批量 regenerate_fingerprint 操作
- 号池管理 UI 优化:token 缩写格式、blocked 行样式、时间显示改为日期格式
This commit is contained in:
fawney19
2026-03-05 02:07:34 +08:00
parent 1d04c41ae7
commit 32ccf61baa
24 changed files with 782 additions and 84 deletions

View File

@@ -310,7 +310,7 @@
v-for="key in keyPage.keys"
:key="key.key_id"
class="border-b border-border/40 last:border-b-0 hover:bg-muted/30 transition-colors"
:class="{ 'opacity-50': !key.is_active }"
:class="getRowClass(key)"
>
<TableCell class="py-3">
<div class="max-w-[260px] min-w-0">
@@ -461,7 +461,7 @@
<div class="flex items-center justify-between gap-2">
<span class="text-muted-foreground">Token</span>
<span class="tabular-nums text-foreground/90">
{{ formatStatInteger(key.total_tokens) }}
{{ formatTokenCount(key.total_tokens) }}
</span>
</div>
<div class="flex items-center justify-between gap-2">
@@ -599,7 +599,7 @@
v-for="key in keyPage.keys"
:key="key.key_id"
class="p-4 sm:p-5 hover:bg-muted/30 transition-colors"
:class="{ 'opacity-50': !key.is_active }"
:class="getRowClass(key)"
>
<div class="flex items-center gap-3">
<div class="flex-1 min-w-0">
@@ -825,7 +825,7 @@
</div>
<div class="flex items-center justify-between gap-2">
<span class="text-muted-foreground">Token</span>
<span class="tabular-nums">{{ formatStatInteger(key.total_tokens) }}</span>
<span class="tabular-nums">{{ formatTokenCount(key.total_tokens) }}</span>
</div>
<div class="flex items-center justify-between gap-2">
<span class="text-muted-foreground">费用</span>
@@ -1837,7 +1837,7 @@ function getSchedulingBadgeVariant(key: PoolKeyDetail): PoolStatusVariant {
if (getAccountAlertLabel(key)) return 'destructive'
const reason = key.scheduling_reason
if (reason === 'manual_disabled') return 'dark'
if (reason === 'manual_disabled') return 'secondary'
if (reason === 'cooldown' || reason === 'circuit_open' || reason === 'cost_exhausted') return 'destructive'
if (reason === 'cost_soft' || reason === 'cost') return 'warning'
if (reason === 'health_low' || reason === 'health_degraded' || reason === 'health') return 'warning'
@@ -1876,6 +1876,12 @@ function formatTTL(seconds: number): string {
return m > 0 ? `${m}m ${s}s` : `${s}s`
}
function getRowClass(key: PoolKeyDetail): string {
const status = getSchedulingStatus(key)
if (!key.is_active || status === 'blocked') return 'bg-muted/50 opacity-60'
return ''
}
function getHealthScoreColor(score: number): string {
if (score >= 0.8) return 'text-green-600 dark:text-green-400'
if (score >= 0.5) return 'text-yellow-600 dark:text-yellow-400'
@@ -2116,6 +2122,14 @@ function formatStatInteger(value: number | null | undefined): string {
return Math.round(n).toLocaleString('en-US')
}
function formatTokenCount(value: number | null | undefined): string {
const n = Number(value ?? 0)
if (!Number.isFinite(n) || n <= 0) return '0'
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
return String(Math.round(n))
}
function formatStatUsd(value: number | null | undefined): string {
const n = Number(value ?? 0)
if (!Number.isFinite(n) || n <= 0) return '$0.00'
@@ -2126,11 +2140,13 @@ function formatStatUsd(value: number | null | undefined): string {
}
function formatRelativeTime(isoStr: string): string {
const diff = (Date.now() - new Date(isoStr).getTime()) / 1000
if (diff < 60) return '刚刚'
if (diff < 3600) return `${Math.floor(diff / 60)}m 前`
if (diff < 86400) return `${Math.floor(diff / 3600)}h 前`
return `${Math.floor(diff / 86400)}d 前`
const date = new Date(isoStr)
const pad = (n: number) => String(n).padStart(2, '0')
const M = pad(date.getMonth() + 1)
const D = pad(date.getDate())
const h = pad(date.getHours())
const m = pad(date.getMinutes())
return `${M}-${D} ${h}:${m}`
}
// --- Init ---