mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
feat(claude-code): 增加 TLS 指纹伪装、Cache TTL 统一、CLI 限制与流超时冷却
- 新增 curl_cffi Transport,支持真实浏览器 TLS 指纹伪装 (Chrome/Node.js) - 增加 Cache TTL Override 功能,强制统一 cache_control 类型防止行为指纹差异 - 增加 CLI-only 客户端限制,支持仅允许 Claude Code CLI 访问 - 池健康策略增加 stream timeout 计数与自动冷却机制 - OAuth 账号 Region 选择改为从 AWS API 动态获取,支持搜索和自定义输入 - 前端 PoolConfigDialog 增加对应配置 UI
This commit is contained in:
@@ -8,3 +8,4 @@ export * from './models'
|
||||
export * from './adaptive'
|
||||
export * from './global-models'
|
||||
export * from './pool'
|
||||
export * from './system'
|
||||
|
||||
12
frontend/src/api/endpoints/system.ts
Normal file
12
frontend/src/api/endpoints/system.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { client } from '../client'
|
||||
|
||||
// AWS Regions
|
||||
|
||||
let _awsRegionsCache: string[] | null = null
|
||||
|
||||
export async function getAwsRegions(): Promise<string[]> {
|
||||
if (_awsRegionsCache) return _awsRegionsCache
|
||||
const resp = await client.get<{ regions: string[] }>('/api/admin/system/aws-regions')
|
||||
_awsRegionsCache = resp.data.regions
|
||||
return _awsRegionsCache
|
||||
}
|
||||
@@ -450,6 +450,11 @@ export interface ClaudeCodeAdvancedConfig {
|
||||
enable_tls_fingerprint?: boolean
|
||||
// 会话 ID 伪装(固定 metadata.user_id 中 session 片段)
|
||||
session_id_masking_enabled?: boolean
|
||||
// Cache TTL 统一(强制所有 cache_control 使用相同 TTL 类型)
|
||||
cache_ttl_override_enabled?: boolean
|
||||
cache_ttl_override_target?: string
|
||||
// 仅允许 CLI 客户端
|
||||
cli_only_enabled?: boolean
|
||||
}
|
||||
|
||||
export interface PoolAdvancedConfig {
|
||||
|
||||
@@ -227,6 +227,52 @@
|
||||
@update:model-value="(v: boolean) => claudeForm.session_id_masking_enabled = v"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 p-3 border rounded-lg bg-muted/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-0.5">
|
||||
<span class="text-sm font-medium">Cache TTL 统一</span>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
强制统一所有请求的 cache_control 类型,避免多人共用时行为指纹不一致
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
:model-value="claudeForm.cache_ttl_override_enabled"
|
||||
@update:model-value="(v: boolean) => claudeForm.cache_ttl_override_enabled = v"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="claudeForm.cache_ttl_override_enabled"
|
||||
class="space-y-1.5"
|
||||
>
|
||||
<Label class="text-xs">目标 TTL 类型</Label>
|
||||
<select
|
||||
:value="claudeForm.cache_ttl_override_target"
|
||||
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
@change="(e) => claudeForm.cache_ttl_override_target = (e.target as HTMLSelectElement).value"
|
||||
>
|
||||
<option value="ephemeral">
|
||||
ephemeral (5 分钟)
|
||||
</option>
|
||||
<option value="1h">
|
||||
1h (1 小时)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</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">仅限 CLI 客户端</span>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
仅允许 Claude Code CLI 客户端访问,拒绝非 CLI 流量
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
:model-value="claudeForm.cli_only_enabled"
|
||||
@update:model-value="(v: boolean) => claudeForm.cli_only_enabled = v"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</form>
|
||||
@@ -290,6 +336,9 @@ interface ClaudeFormState {
|
||||
session_idle_timeout_minutes: number
|
||||
enable_tls_fingerprint: boolean
|
||||
session_id_masking_enabled: boolean
|
||||
cache_ttl_override_enabled: boolean
|
||||
cache_ttl_override_target: string
|
||||
cli_only_enabled: boolean
|
||||
}
|
||||
|
||||
const claudeForm = ref<ClaudeFormState>({
|
||||
@@ -298,6 +347,9 @@ const claudeForm = ref<ClaudeFormState>({
|
||||
session_idle_timeout_minutes: 5,
|
||||
enable_tls_fingerprint: true,
|
||||
session_id_masking_enabled: true,
|
||||
cache_ttl_override_enabled: false,
|
||||
cache_ttl_override_target: 'ephemeral',
|
||||
cli_only_enabled: false,
|
||||
})
|
||||
|
||||
function parseNum(v: string | number): number | undefined {
|
||||
@@ -334,6 +386,9 @@ watch(() => props.modelValue, (v) => {
|
||||
session_idle_timeout_minutes: cc.session_idle_timeout_minutes ?? 5,
|
||||
enable_tls_fingerprint: cc.enable_tls_fingerprint ?? true,
|
||||
session_id_masking_enabled: cc.session_id_masking_enabled ?? true,
|
||||
cache_ttl_override_enabled: cc.cache_ttl_override_enabled ?? false,
|
||||
cache_ttl_override_target: cc.cache_ttl_override_target ?? 'ephemeral',
|
||||
cli_only_enabled: cc.cli_only_enabled ?? false,
|
||||
}
|
||||
} else {
|
||||
// 默认值:全部开启
|
||||
@@ -343,6 +398,9 @@ watch(() => props.modelValue, (v) => {
|
||||
session_idle_timeout_minutes: 5,
|
||||
enable_tls_fingerprint: true,
|
||||
session_id_masking_enabled: true,
|
||||
cache_ttl_override_enabled: false,
|
||||
cache_ttl_override_target: 'ephemeral',
|
||||
cli_only_enabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,6 +433,11 @@ async function handleSave() {
|
||||
: null,
|
||||
enable_tls_fingerprint: claudeForm.value.enable_tls_fingerprint,
|
||||
session_id_masking_enabled: claudeForm.value.session_id_masking_enabled,
|
||||
cache_ttl_override_enabled: claudeForm.value.cache_ttl_override_enabled,
|
||||
cache_ttl_override_target: claudeForm.value.cache_ttl_override_enabled
|
||||
? claudeForm.value.cache_ttl_override_target
|
||||
: 'ephemeral',
|
||||
cli_only_enabled: claudeForm.value.cli_only_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -144,19 +144,48 @@
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium">Region</label>
|
||||
<div class="grid grid-cols-2 gap-1.5">
|
||||
<button
|
||||
v-for="r in (['eu-north-1', 'us-east-1'] as const)"
|
||||
:key="r"
|
||||
class="h-8 text-xs font-medium font-mono rounded-md border transition-colors"
|
||||
:class="device.region === r
|
||||
? 'border-primary bg-primary/5 text-foreground'
|
||||
: 'border-border text-muted-foreground hover:text-foreground hover:border-foreground/20'"
|
||||
@click="device.region = r"
|
||||
<ComboboxRoot
|
||||
:model-value="device.region"
|
||||
:open="regionComboboxOpen"
|
||||
@update:model-value="(v: string) => { if (v) device.region = v }"
|
||||
@update:open="(v: boolean) => { regionComboboxOpen = v; if (v) ensureAwsRegions() }"
|
||||
>
|
||||
<ComboboxAnchor class="relative w-full">
|
||||
<ComboboxInput
|
||||
:display-value="() => device.region"
|
||||
placeholder="输入或选择 Region"
|
||||
class="w-full h-8 px-2 pr-7 text-xs rounded-md border border-border bg-background font-mono focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
spellcheck="false"
|
||||
@input="(e: Event) => regionSearch = (e.target as HTMLInputElement).value"
|
||||
@keydown.enter.prevent="onRegionEnter"
|
||||
/>
|
||||
<ComboboxTrigger class="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground">
|
||||
<ChevronsUpDown class="w-3.5 h-3.5" />
|
||||
</ComboboxTrigger>
|
||||
</ComboboxAnchor>
|
||||
<ComboboxContent
|
||||
position="popper"
|
||||
class="z-[99] mt-1 max-h-[200px] w-[--radix-combobox-trigger-width] overflow-y-auto rounded-md border border-border bg-popover shadow-md"
|
||||
>
|
||||
{{ r }}
|
||||
</button>
|
||||
</div>
|
||||
<ComboboxViewport>
|
||||
<ComboboxEmpty class="px-2 py-1.5 text-xs text-muted-foreground">
|
||||
{{ awsRegionsLoaded ? '无匹配结果,回车使用自定义值' : '加载中...' }}
|
||||
</ComboboxEmpty>
|
||||
<ComboboxItem
|
||||
v-for="r in filteredRegions"
|
||||
:key="r"
|
||||
:value="r"
|
||||
class="flex items-center gap-1.5 px-2 py-1.5 text-xs font-mono cursor-pointer rounded-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground"
|
||||
>
|
||||
<Check
|
||||
class="w-3 h-3 shrink-0"
|
||||
:class="device.region === r ? 'opacity-100' : 'opacity-0'"
|
||||
/>
|
||||
{{ r }}
|
||||
</ComboboxItem>
|
||||
</ComboboxViewport>
|
||||
</ComboboxContent>
|
||||
</ComboboxRoot>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">TOTP Secret (可选, 2FA认证)</label>
|
||||
@@ -501,7 +530,17 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
||||
import { Dialog, Button, Textarea, Popover, PopoverTrigger, PopoverContent } from '@/components/ui'
|
||||
import { UserPlus, Copy, ExternalLink, Upload, Globe, AlertCircle, ShieldCheck } from 'lucide-vue-next'
|
||||
import {
|
||||
ComboboxAnchor,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxRoot,
|
||||
ComboboxTrigger,
|
||||
ComboboxViewport,
|
||||
} from 'radix-vue'
|
||||
import { UserPlus, Copy, ExternalLink, Upload, Globe, AlertCircle, ShieldCheck, ChevronsUpDown, Check } from 'lucide-vue-next'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { useTotp } from '@/composables/useTotp'
|
||||
@@ -513,6 +552,7 @@ import {
|
||||
batchImportOAuth,
|
||||
startDeviceAuthorize,
|
||||
pollDeviceAuthorize,
|
||||
getAwsRegions,
|
||||
} from '@/api/endpoints'
|
||||
import ProxyNodeSelect from './ProxyNodeSelect.vue'
|
||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||
@@ -537,6 +577,38 @@ const totp = useTotp()
|
||||
const proxyPopoverOpen = ref(false)
|
||||
const selectedProxyNodeId = ref('')
|
||||
|
||||
// AWS Regions (动态获取 + 进程内缓存)
|
||||
const awsRegions = ref<string[]>([])
|
||||
const awsRegionsLoaded = ref(false)
|
||||
const regionSearch = ref('')
|
||||
const regionComboboxOpen = ref(false)
|
||||
|
||||
const filteredRegions = computed(() => {
|
||||
const q = regionSearch.value.trim().toLowerCase()
|
||||
if (!q) return awsRegions.value
|
||||
return awsRegions.value.filter(r => r.includes(q))
|
||||
})
|
||||
|
||||
async function ensureAwsRegions() {
|
||||
if (awsRegionsLoaded.value) return
|
||||
try {
|
||||
awsRegions.value = await getAwsRegions()
|
||||
} catch {
|
||||
awsRegions.value = ['us-east-1', 'us-east-2', 'us-west-1', 'us-west-2', 'eu-north-1']
|
||||
}
|
||||
awsRegionsLoaded.value = true
|
||||
}
|
||||
|
||||
function onRegionEnter() {
|
||||
// If no matching item is highlighted, accept the raw input as a custom region value.
|
||||
const raw = regionSearch.value.trim()
|
||||
if (raw && !filteredRegions.value.includes(raw)) {
|
||||
device.value.region = raw
|
||||
regionComboboxOpen.value = false
|
||||
regionSearch.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取已选代理节点的显示名称 */
|
||||
function getSelectedNodeLabel(): string {
|
||||
if (!selectedProxyNodeId.value) return ''
|
||||
|
||||
Reference in New Issue
Block a user