mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
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:
@@ -169,7 +169,13 @@ export interface PoolKeysQuery {
|
||||
|
||||
export interface PoolBatchAction {
|
||||
key_ids: string[]
|
||||
action: 'enable' | 'disable' | 'delete' | 'clear_cooldown' | 'reset_cost'
|
||||
action:
|
||||
| 'enable'
|
||||
| 'disable'
|
||||
| 'delete'
|
||||
| 'clear_cooldown'
|
||||
| 'reset_cost'
|
||||
| 'regenerate_fingerprint'
|
||||
}
|
||||
|
||||
export async function getPoolOverview(): Promise<PoolOverviewResponse> {
|
||||
|
||||
@@ -442,8 +442,6 @@ export interface ClaudeCodeAdvancedConfig {
|
||||
// 会话数量控制:null/undefined 表示不限制
|
||||
max_sessions?: number | null
|
||||
session_idle_timeout_minutes?: number | null
|
||||
// TLS 指纹模拟(模拟 Node.js/Claude Code 客户端指纹)
|
||||
enable_tls_fingerprint?: boolean
|
||||
// 会话 ID 伪装(固定 metadata.user_id 中 session 片段)
|
||||
session_id_masking_enabled?: boolean
|
||||
// Cache TTL 统一(强制所有 cache_control 使用相同 TTL 类型)
|
||||
|
||||
@@ -203,18 +203,6 @@
|
||||
<h3 class="text-sm font-medium border-b pb-2">
|
||||
Claude Code
|
||||
</h3>
|
||||
<div class="flex items-center justify-between p-3 border rounded-lg bg-muted/50">
|
||||
<div class="space-y-0.5">
|
||||
<span class="text-sm font-medium">TLS 指纹模拟</span>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
模拟 Node.js / Claude Code 客户端指纹
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
:model-value="claudeForm.enable_tls_fingerprint"
|
||||
@update:model-value="(v: boolean) => claudeForm.enable_tls_fingerprint = v"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 border rounded-lg bg-muted/50">
|
||||
<div class="space-y-0.5">
|
||||
<span class="text-sm font-medium">Session ID 伪装</span>
|
||||
@@ -507,7 +495,6 @@ interface ClaudeFormState {
|
||||
session_control_enabled: boolean
|
||||
max_sessions: number | undefined
|
||||
session_idle_timeout_minutes: number
|
||||
enable_tls_fingerprint: boolean
|
||||
session_id_masking_enabled: boolean
|
||||
cache_ttl_override_enabled: boolean
|
||||
cache_ttl_override_target: string
|
||||
@@ -518,7 +505,6 @@ const claudeForm = ref<ClaudeFormState>({
|
||||
session_control_enabled: true,
|
||||
max_sessions: undefined,
|
||||
session_idle_timeout_minutes: 5,
|
||||
enable_tls_fingerprint: true,
|
||||
session_id_masking_enabled: true,
|
||||
cache_ttl_override_enabled: false,
|
||||
cache_ttl_override_target: 'ephemeral',
|
||||
@@ -823,7 +809,6 @@ watch(() => props.modelValue, async (open) => {
|
||||
session_control_enabled: cc?.max_sessions !== null,
|
||||
max_sessions: cc?.max_sessions ?? undefined,
|
||||
session_idle_timeout_minutes: cc?.session_idle_timeout_minutes ?? 5,
|
||||
enable_tls_fingerprint: cc?.enable_tls_fingerprint !== false,
|
||||
session_id_masking_enabled: cc?.session_id_masking_enabled !== false,
|
||||
cache_ttl_override_enabled: cc?.cache_ttl_override_enabled ?? false,
|
||||
cache_ttl_override_target: cc?.cache_ttl_override_target ?? 'ephemeral',
|
||||
@@ -866,7 +851,6 @@ async function handleSave() {
|
||||
payload.claude_code_advanced = {
|
||||
max_sessions: cf.session_control_enabled ? (cf.max_sessions ?? null) : null,
|
||||
session_idle_timeout_minutes: cf.session_control_enabled ? cf.session_idle_timeout_minutes : null,
|
||||
enable_tls_fingerprint: cf.enable_tls_fingerprint,
|
||||
session_id_masking_enabled: cf.session_id_masking_enabled,
|
||||
cache_ttl_override_enabled: cf.cache_ttl_override_enabled,
|
||||
cache_ttl_override_target: cf.cache_ttl_override_enabled ? cf.cache_ttl_override_target : undefined,
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
Reference in New Issue
Block a user