feat: 新增 Kiro 适配器、OAuth 改进与多项功能增强

- 新增 Kiro provider 适配器(EventStream 协议解析、令牌管理、用量追踪)
- 重构 OAuth 账户管理与统一配额机制
- 重构 Handler 基类(CLI adapter/handler、请求构建器、流处理器)
- 增强缓存监控后端 API 与前端可视化
- 改进 Gemini 格式标准化器与请求头处理
- Antigravity/Codex 适配器更新,移除旧 metadata_collector
- 新增数据库迁移:proxy provider API keys
- 前端 UI 多项优化

Co-Authored-By: AAEE86 <ppk0227@hotmail.com>
This commit is contained in:
fawney19
2026-02-09 01:05:48 +08:00
parent e324ffdcd6
commit b34dd12863
82 changed files with 6498 additions and 840 deletions

View File

@@ -784,10 +784,10 @@ function clearFilters() {
const skip = computed(() => (currentPage.value - 1) * limit.value)
const activeKeyCount = computed(() => apiKeys.value.filter(key => key.is_active).length)
const inactiveKeyCount = computed(() => Math.max(0, apiKeys.value.length - activeKeyCount.value))
const _inactiveKeyCount = computed(() => Math.max(0, apiKeys.value.length - activeKeyCount.value))
const limitedKeyCount = computed(() => apiKeys.value.filter(isBalanceLimited).length)
const unlimitedKeyCount = computed(() => Math.max(0, apiKeys.value.length - limitedKeyCount.value))
const expiringSoonCount = computed(() => apiKeys.value.filter(key => isExpiringSoon(key)).length)
const _unlimitedKeyCount = computed(() => Math.max(0, apiKeys.value.length - limitedKeyCount.value))
const _expiringSoonCount = computed(() => apiKeys.value.filter(key => isExpiringSoon(key)).length)
// 筛选后的 API Keys
const filteredApiKeys = computed(() => {

View File

@@ -18,10 +18,10 @@ import SelectContent from '@/components/ui/select-content.vue'
import SelectItem from '@/components/ui/select-item.vue'
import SelectValue from '@/components/ui/select-value.vue'
import ScatterChart from '@/components/charts/ScatterChart.vue'
import { Trash2, Eraser, Search, X, BarChart3, ChevronDown, ChevronRight, Database, ArrowRight } from 'lucide-vue-next'
import { Trash2, Eraser, Search, X, BarChart3, ChevronDown, ChevronRight, Database, ArrowRight, HardDrive } from 'lucide-vue-next'
import { useToast } from '@/composables/useToast'
import { useConfirm } from '@/composables/useConfirm'
import { cacheApi, modelMappingCacheApi, type CacheStats, type CacheConfig, type UserAffinity, type ModelMappingCacheStats } from '@/api/cache'
import { cacheApi, modelMappingCacheApi, redisCacheApi, type CacheStats, type CacheConfig, type UserAffinity, type ModelMappingCacheStats, type RedisCacheCategoriesResponse } from '@/api/cache'
import type { TTLAnalysisUser } from '@/api/cache'
import { formatNumber, formatTokens, formatCost, formatRemainingTime } from '@/utils/format'
import {
@@ -54,6 +54,12 @@ const modelMappingLoading = ref(false)
const clearingModelMapping = ref(false)
const clearingModelName = ref<string | null>(null)
// ==================== Redis 缓存分类管理 ====================
const redisCacheData = ref<RedisCacheCategoriesResponse | null>(null)
const redisCacheLoading = ref(false)
const clearingCategory = ref<string | null>(null)
const { success: showSuccess, error: showError, info: showInfo } = useToast()
const { confirm: showConfirm } = useConfirm()
@@ -324,6 +330,54 @@ async function clearProviderModelMapping(providerId: string, globalModelId: stri
}
}
// ==================== Redis 缓存分类管理方法 ====================
async function fetchRedisCacheCategories() {
redisCacheLoading.value = true
try {
redisCacheData.value = await redisCacheApi.getCategories()
} catch (error) {
showError('获取 Redis 缓存分类失败')
log.error('获取 Redis 缓存分类失败', error)
} finally {
redisCacheLoading.value = false
}
}
async function clearRedisCategory(categoryKey: string, categoryName: string, count: number) {
if (count === 0) {
showInfo(`${categoryName} 缓存为空,无需清理`)
return
}
const confirmed = await showConfirm({
title: `清除 ${categoryName} 缓存`,
message: `确定要清除 ${categoryName} 的所有缓存吗?共 ${count} 个键。`,
})
if (!confirmed) return
clearingCategory.value = categoryKey
try {
const result = await redisCacheApi.clearCategory(categoryKey)
showSuccess(`已清除 ${categoryName} 缓存(${result.deleted_count} 个键)`)
await fetchRedisCacheCategories()
} catch (error) {
showError(`清除 ${categoryName} 缓存失败`)
log.error('清除 Redis 缓存分类失败', error)
} finally {
clearingCategory.value = null
}
}
const redisCategoriesWithKeys = computed(() => {
if (!redisCacheData.value?.categories) return []
return redisCacheData.value.categories.filter(c => c.count > 0)
})
const redisCategoriesEmpty = computed(() => {
if (!redisCacheData.value?.categories) return []
return redisCacheData.value.categories.filter(c => c.count === 0)
})
function formatTTL(ttl: number | null): string {
if (ttl === null || ttl < 0) return '-'
if (ttl < 60) return `${ttl}s`
@@ -353,7 +407,8 @@ async function refreshData() {
fetchCacheStats(),
fetchCacheConfig(),
fetchAffinityList(),
fetchModelMappingStats()
fetchModelMappingStats(),
fetchRedisCacheCategories()
])
}
@@ -379,6 +434,7 @@ onMounted(() => {
fetchCacheConfig()
fetchAffinityList()
fetchModelMappingStats()
fetchRedisCacheCategories()
startCountdown()
refreshAnalysis()
})
@@ -1045,6 +1101,108 @@ onBeforeUnmount(() => {
</div>
</Card>
<!-- Redis 缓存分类管理 -->
<Card class="overflow-hidden">
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div class="flex items-center gap-3 shrink-0">
<HardDrive class="h-5 w-5 text-muted-foreground hidden sm:block" />
<h3 class="text-sm sm:text-base font-semibold">
Redis 缓存管理
</h3>
<Badge
v-if="redisCacheData?.total_keys !== undefined"
variant="secondary"
>
{{ redisCacheData.total_keys }} 个键
</Badge>
</div>
<div class="flex items-center gap-2">
<RefreshButton
:loading="redisCacheLoading"
size="sm"
title="刷新缓存分类"
@click="fetchRedisCacheCategories"
/>
</div>
</div>
</div>
<!-- 有数据 -->
<div v-if="redisCacheData?.available && redisCacheData.categories.length > 0">
<!-- 有缓存的分类 -->
<div
v-if="redisCategoriesWithKeys.length > 0"
class="divide-y divide-border/40"
>
<div
v-for="cat in redisCategoriesWithKeys"
:key="cat.key"
class="flex items-center justify-between px-4 sm:px-6 py-2.5 hover:bg-muted/30 transition-colors"
>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="text-sm font-medium">{{ cat.name }}</span>
<Badge variant="outline">
{{ cat.count }}
</Badge>
</div>
<p class="text-xs text-muted-foreground mt-0.5 truncate">
{{ cat.description }}
</p>
</div>
<Button
variant="ghost"
size="sm"
class="shrink-0 ml-3 text-destructive hover:text-destructive hover:bg-destructive/10"
:disabled="clearingCategory === cat.key"
title="清除该分类缓存"
@click="clearRedisCategory(cat.key, cat.name, cat.count)"
>
<Trash2 class="h-3.5 w-3.5" />
</Button>
</div>
</div>
<!-- 空分类折叠 -->
<div
v-if="redisCategoriesEmpty.length > 0"
class="px-4 sm:px-6 py-3 border-t border-border/40"
>
<p class="text-xs text-muted-foreground">
另有 {{ redisCategoriesEmpty.length }} 个分类为空{{ redisCategoriesEmpty.map(c => c.name).join('、') }}
</p>
</div>
<!-- 全部为空 -->
<div
v-if="redisCategoriesWithKeys.length === 0"
class="px-6 py-8 text-center"
>
<HardDrive class="h-10 w-10 text-muted-foreground/30 mx-auto mb-2" />
<p class="text-sm text-muted-foreground">
所有缓存分类为空
</p>
</div>
</div>
<!-- Redis 不可用 -->
<div
v-else-if="redisCacheData && !redisCacheData.available"
class="px-6 py-8 text-center text-sm text-muted-foreground"
>
{{ redisCacheData.message || 'Redis 未启用' }}
</div>
<!-- 加载中 -->
<div
v-else-if="redisCacheLoading"
class="px-6 py-8 text-center text-sm text-muted-foreground"
>
正在扫描 Redis 缓存...
</div>
</Card>
<!-- TTL 分析区域 -->
<Card class="overflow-hidden">
<div class="px-4 sm:px-6 py-3 sm:py-3.5 border-b border-border/60">

View File

@@ -499,10 +499,6 @@ import {
Trash2,
Loader2,
Eye,
Wrench,
Brain,
Zap,
Image,
Building2,
Search,
Power,
@@ -648,7 +644,7 @@ function hasVideoPricing(model: GlobalModelResponse): boolean {
}
// 获取视频分辨率计费的数量
function getVideoPricingCount(model: GlobalModelResponse): number {
function _getVideoPricingCount(model: GlobalModelResponse): number {
const priceByResolution = model.config?.billing?.video?.price_per_second_by_resolution
if (!priceByResolution || typeof priceByResolution !== 'object') return 0
return Object.keys(priceByResolution).length

View File

@@ -3,7 +3,7 @@ import { RouterLink } from 'vue-router'
import { ArrowRight, Shuffle, FileCode, Globe, Shield, Check, Info, AlertTriangle, Settings } from 'lucide-vue-next'
import { panelClasses } from './guide-config'
const props = withDefaults(
withDefaults(
defineProps<{
baseUrl?: string
}>(),

View File

@@ -3,7 +3,7 @@ import { ref, computed } from 'vue'
import { Search, ChevronDown, ExternalLink, HelpCircle } from 'lucide-vue-next'
import { faqItems, panelClasses } from './guide-config'
const props = withDefaults(
withDefaults(
defineProps<{
baseUrl?: string
}>(),
@@ -18,12 +18,6 @@ const searchQuery = ref('')
// 展开的 FAQ
const expandedIds = ref<Set<string>>(new Set())
// 分类列表
const categories = computed(() => {
const cats = new Set(faqItems.map(item => item.category))
return Array.from(cats)
})
// 过滤后的 FAQ
const filteredFaqs = computed(() => {
if (!searchQuery.value.trim()) {

View File

@@ -3,7 +3,7 @@ import { RouterLink } from 'vue-router'
import { ArrowRight, Layers, Check, Info, Shuffle, TrendingUp, Gauge, Clock } from 'lucide-vue-next'
import { loadBalanceModes, panelClasses } from './guide-config'
const props = withDefaults(
withDefaults(
defineProps<{
baseUrl?: string
}>(),

View File

@@ -3,7 +3,7 @@ import { RouterLink } from 'vue-router'
import { ArrowRight, Server, Layers, Key, Box, ChevronRight } from 'lucide-vue-next'
import { coreConcepts, apiFormats, configSteps, panelClasses } from './guide-config'
const props = withDefaults(
withDefaults(
defineProps<{
baseUrl?: string
}>(),

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import { ArrowRight, Server, Plus, Settings, Check, AlertTriangle, Info } from 'lucide-vue-next'
import { ArrowRight, Server, Settings, Check, AlertTriangle, Info } from 'lucide-vue-next'
import { apiFormats, panelClasses } from './guide-config'
const props = withDefaults(
withDefaults(
defineProps<{
baseUrl?: string
}>(),

View File

@@ -3,7 +3,7 @@ import { RouterLink } from 'vue-router'
import { ArrowRight, Users, Key, Shield, Check, Info, AlertTriangle, Clock } from 'lucide-vue-next'
import { panelClasses } from './guide-config'
const props = withDefaults(
withDefaults(
defineProps<{
baseUrl?: string
}>(),
@@ -12,14 +12,6 @@ const props = withDefaults(
}
)
// 用户字段说明
const userFields = [
{ name: '用户名/邮箱', description: '用户的登录凭证', required: true },
{ name: '角色', description: '普通用户或管理员', required: true },
{ name: '状态', description: '启用/禁用用户', required: false },
{ name: '默认配额', description: '该用户创建的 Key 默认继承的配额设置', required: false }
]
// API Key 字段说明
const keyFields = [
{ name: '名称', description: 'Key 的描述性名称,方便识别', required: true },

View File

@@ -262,9 +262,6 @@
import { ref, computed, onMounted, watch } from 'vue'
import {
Loader2,
Eye,
Wrench,
Brain,
Search,
Copy,
Check,