feat: 缓存计费细分、能力匹配优化、用户模型调用计数

1. 缓存创建 tokens 区分 5min/1h TTL,支持按缓存时长差异化计费
   - Usage 表新增 cache_creation_input_tokens_5m/1h 字段
   - Claude handler 解析新格式 (ephemeral_5m/1h, claude_cache_creation_5/1h)
   - 计费规则支持 cache_ttl_pricing 覆盖 cache_creation 价格

2. 能力匹配机制优化
   - COMPATIBLE 能力不再硬过滤,改为排序阶段通过 capability_miss_count 优先级处理
   - cache_1h 改为 COMPATIBLE + REQUEST_PARAM(自动检测请求体中的 ttl=1h)
   - gemini_files 改为 EXCLUSIVE + REQUEST_PARAM(自动检测 fileData.fileUri)
   - 移除前端模型偏好/能力配置 UI(不再需要用户手动配置)

3. 新增用户-模型维度调用次数计数器 (UserModelUsageCount)
   - 原子递增,避免从 Usage 表聚合查询
   - 前端模型目录和用户可用模型列表展示调用次数

4. 其他改进
   - global_model_id 改为必填(NOT NULL),清理孤立模型
   - 模型映射对话框支持从上游获取模型列表并分组折叠
   - 端点测试不再依赖端点启用状态
   - 异步任务页面对普通用户隐藏用户信息列
   - Dashboard 响应式布局断点调整 (sm -> lg)
   - 号池管理仅展示已启用号池的提供商
This commit is contained in:
fawney19
2026-02-28 11:45:04 +08:00
parent 82bbed2720
commit ecb16d345a
59 changed files with 1053 additions and 608 deletions
+1 -109
View File
@@ -76,9 +76,6 @@
<TableHead class="min-w-[200px] h-12 font-semibold">
密钥名称
</TableHead>
<TableHead class="min-w-[80px] h-12 font-semibold">
能力
</TableHead>
<TableHead class="min-w-[160px] h-12 font-semibold">
密钥
</TableHead>
@@ -120,42 +117,6 @@
</div>
</TableCell>
<!-- 能力 -->
<TableCell class="py-4">
<div class="flex gap-1.5 flex-wrap items-center">
<template v-if="userConfigurableCapabilities.length > 0">
<button
v-for="cap in userConfigurableCapabilities"
:key="cap.name"
class="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium transition-all"
:class="[
apiKey.is_locked ? 'opacity-50 cursor-not-allowed' : '',
isCapabilityEnabled(apiKey, cap.name)
? 'bg-primary text-primary-foreground'
: 'bg-transparent text-muted-foreground border border-dashed border-muted-foreground/50 hover:border-primary/50 hover:text-foreground'
]"
:title="apiKey.is_locked ? '已锁定' : getCapabilityTooltip(cap, isCapabilityEnabled(apiKey, cap.name))"
:disabled="apiKey.is_locked"
@click.stop="!apiKey.is_locked && toggleCapability(apiKey, cap.name)"
>
<Check
v-if="isCapabilityEnabled(apiKey, cap.name)"
class="w-3 h-3"
/>
<Plus
v-else
class="w-3 h-3"
/>
{{ cap.short_name || cap.display_name }}
</button>
</template>
<span
v-else
class="text-muted-foreground text-xs"
>-</span>
</div>
</TableCell>
<!-- 密钥显示 -->
<TableCell class="py-4">
<div class="flex items-center gap-1.5">
@@ -478,7 +439,6 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { meApi, type ApiKey } from '@/api/me'
import { getAllCapabilities, type CapabilityDefinition } from '@/api/endpoints'
import Card from '@/components/ui/card.vue'
import Button from '@/components/ui/button.vue'
import Input from '@/components/ui/input.vue'
@@ -495,7 +455,7 @@ import {
TableRow
} from '@/components/ui'
import RefreshButton from '@/components/ui/refresh-button.vue'
import { Plus, Key, Copy, Trash2, Loader2, Activity, CheckCircle, Power, Check } from 'lucide-vue-next'
import { Plus, Key, Copy, Trash2, Loader2, Activity, CheckCircle, Power } from 'lucide-vue-next'
import { useToast } from '@/composables/useToast'
import { log } from '@/utils/logger'
import { parseApiError } from '@/utils/errorParser'
@@ -526,26 +486,10 @@ const newKeyName = ref('')
const newKeyValue = ref('')
const keyToDelete = ref<ApiKey | null>(null)
// 能力配置相关
const availableCapabilities = ref<CapabilityDefinition[]>([])
const userConfigurableCapabilities = computed(() =>
availableCapabilities.value.filter(cap => cap.config_mode === 'user_configurable')
)
const savingCapability = ref<string | null>(null) // 正在保存的能力标识 "keyId:capName"
onMounted(() => {
loadApiKeys()
loadCapabilities()
})
async function loadCapabilities() {
try {
availableCapabilities.value = await getAllCapabilities()
} catch (error) {
log.error('Failed to load capabilities:', error)
}
}
async function loadApiKeys() {
loading.value = true
try {
@@ -625,50 +569,6 @@ async function toggleApiKey(apiKey: ApiKey) {
}
}
// 检查某个能力是否已启用
function isCapabilityEnabled(apiKey: ApiKey, capName: string): boolean {
return apiKey.force_capabilities?.[capName] || false
}
// 切换能力配置
async function toggleCapability(apiKey: ApiKey, capName: string) {
const capKey = `${apiKey.id}:${capName}`
if (savingCapability.value === capKey) return // 防止重复点击
savingCapability.value = capKey
try {
const currentEnabled = isCapabilityEnabled(apiKey, capName)
const newEnabled = !currentEnabled
// 构建新的能力配置
const newCapabilities: Record<string, boolean> = { ...(apiKey.force_capabilities || {}) }
if (newEnabled) {
newCapabilities[capName] = true
} else {
delete newCapabilities[capName]
}
const capabilitiesData = Object.keys(newCapabilities).length > 0 ? newCapabilities : null
// 调用 API 保存
await meApi.updateApiKeyCapabilities(apiKey.id, {
force_capabilities: capabilitiesData
})
// 更新本地数据
const index = apiKeys.value.findIndex(k => k.id === apiKey.id)
if (index !== -1) {
apiKeys.value[index].force_capabilities = capabilitiesData
}
} catch (err) {
log.error('保存能力配置失败:', err)
showError('保存失败,请重试')
} finally {
savingCapability.value = null
}
}
async function copyApiKey(apiKey: ApiKey) {
try {
// 调用后端 API 获取完整密钥
@@ -745,12 +645,4 @@ function formatRelativeTime(dateString: string): string {
return formatDate(dateString)
}
// 获取能力按钮的提示文字
function getCapabilityTooltip(cap: CapabilityDefinition, isEnabled: boolean): string {
if (isEnabled) {
return `[已启用] 此密钥只能访问支持${cap.display_name}的模型`
}
return `${cap.description}`
}
</script>