feat: 完善多平台账户管理和API Keys页面展示

- 修复OpenAI路由中的gpt-5模型ID处理
- 增强统一调度器的账户选择日志输出
- 优化OAuth流程中的账户类型处理
- 完善API Keys页面的多平台账户信息展示

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-08-12 17:55:45 +08:00
parent b250b6ee3b
commit 4ca9674772
6 changed files with 112 additions and 20 deletions

View File

@@ -68,14 +68,14 @@ router.post('/responses', authenticateApiKey, async (req, res) => {
// 从请求体中提取模型和流式标志 // 从请求体中提取模型和流式标志
let requestedModel = req.body?.model || null let requestedModel = req.body?.model || null
// 如果模型是 gpt-5 开头且后面还有内容(如 gpt-5-2025-08-07则覆盖为 gpt-5 // 如果模型是 gpt-5 开头且后面还有内容(如 gpt-5-2025-08-07则覆盖为 gpt-5
if (requestedModel && requestedModel.startsWith('gpt-5-') && requestedModel !== 'gpt-5') { if (requestedModel && requestedModel.startsWith('gpt-5-') && requestedModel !== 'gpt-5') {
logger.info(`📝 Model ${requestedModel} detected, normalizing to gpt-5 for Codex API`) logger.info(`📝 Model ${requestedModel} detected, normalizing to gpt-5 for Codex API`)
requestedModel = 'gpt-5' requestedModel = 'gpt-5'
req.body.model = 'gpt-5' // 同时更新请求体中的模型 req.body.model = 'gpt-5' // 同时更新请求体中的模型
} }
const isStream = req.body?.stream !== false // 默认为流式(兼容现有行为) const isStream = req.body?.stream !== false // 默认为流式(兼容现有行为)
// 判断是否为 Codex CLI 的请求 // 判断是否为 Codex CLI 的请求

View File

@@ -38,6 +38,8 @@ class UnifiedGeminiScheduler {
logger.info( logger.info(
`🎯 Using bound dedicated Gemini account: ${boundAccount.name} (${apiKeyData.geminiAccountId}) for API key ${apiKeyData.name}` `🎯 Using bound dedicated Gemini account: ${boundAccount.name} (${apiKeyData.geminiAccountId}) for API key ${apiKeyData.name}`
) )
// 更新账户的最后使用时间
await geminiAccountService.markAccountUsed(apiKeyData.geminiAccountId)
return { return {
accountId: apiKeyData.geminiAccountId, accountId: apiKeyData.geminiAccountId,
accountType: 'gemini' accountType: 'gemini'
@@ -62,6 +64,8 @@ class UnifiedGeminiScheduler {
logger.info( logger.info(
`🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}` `🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
) )
// 更新账户的最后使用时间
await geminiAccountService.markAccountUsed(mappedAccount.accountId)
return mappedAccount return mappedAccount
} else { } else {
logger.warn( logger.warn(
@@ -108,6 +112,9 @@ class UnifiedGeminiScheduler {
`🎯 Selected account: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) with priority ${selectedAccount.priority} for API key ${apiKeyData.name}` `🎯 Selected account: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) with priority ${selectedAccount.priority} for API key ${apiKeyData.name}`
) )
// 更新账户的最后使用时间
await geminiAccountService.markAccountUsed(selectedAccount.accountId)
return { return {
accountId: selectedAccount.accountId, accountId: selectedAccount.accountId,
accountType: selectedAccount.accountType accountType: selectedAccount.accountType
@@ -378,6 +385,8 @@ class UnifiedGeminiScheduler {
logger.info( logger.info(
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}` `🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
) )
// 更新账户的最后使用时间
await geminiAccountService.markAccountUsed(mappedAccount.accountId)
return mappedAccount return mappedAccount
} }
} }
@@ -473,6 +482,9 @@ class UnifiedGeminiScheduler {
`🎯 Selected account from Gemini group ${group.name}: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) with priority ${selectedAccount.priority}` `🎯 Selected account from Gemini group ${group.name}: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) with priority ${selectedAccount.priority}`
) )
// 更新账户的最后使用时间
await geminiAccountService.markAccountUsed(selectedAccount.accountId)
return { return {
accountId: selectedAccount.accountId, accountId: selectedAccount.accountId,
accountType: selectedAccount.accountType accountType: selectedAccount.accountType

View File

@@ -38,6 +38,8 @@ class UnifiedOpenAIScheduler {
logger.info( logger.info(
`🎯 Using bound dedicated OpenAI account: ${boundAccount.name} (${apiKeyData.openaiAccountId}) for API key ${apiKeyData.name}` `🎯 Using bound dedicated OpenAI account: ${boundAccount.name} (${apiKeyData.openaiAccountId}) for API key ${apiKeyData.name}`
) )
// 更新账户的最后使用时间
await openaiAccountService.recordUsage(apiKeyData.openaiAccountId, 0)
return { return {
accountId: apiKeyData.openaiAccountId, accountId: apiKeyData.openaiAccountId,
accountType: 'openai' accountType: 'openai'
@@ -62,6 +64,8 @@ class UnifiedOpenAIScheduler {
logger.info( logger.info(
`🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}` `🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
) )
// 更新账户的最后使用时间
await openaiAccountService.recordUsage(mappedAccount.accountId, 0)
return mappedAccount return mappedAccount
} else { } else {
logger.warn( logger.warn(
@@ -108,6 +112,9 @@ class UnifiedOpenAIScheduler {
`🎯 Selected account: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) with priority ${selectedAccount.priority} for API key ${apiKeyData.name}` `🎯 Selected account: ${selectedAccount.name} (${selectedAccount.accountId}, ${selectedAccount.accountType}) with priority ${selectedAccount.priority} for API key ${apiKeyData.name}`
) )
// 更新账户的最后使用时间
await openaiAccountService.recordUsage(selectedAccount.accountId, 0)
return { return {
accountId: selectedAccount.accountId, accountId: selectedAccount.accountId,
accountType: selectedAccount.accountType accountType: selectedAccount.accountType
@@ -372,6 +379,8 @@ class UnifiedOpenAIScheduler {
logger.info( logger.info(
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType})` `🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType})`
) )
// 更新账户的最后使用时间
await openaiAccountService.recordUsage(mappedAccount.accountId, 0)
return mappedAccount return mappedAccount
} }
} }
@@ -459,6 +468,9 @@ class UnifiedOpenAIScheduler {
`🎯 Selected account from group: ${selectedAccount.name} (${selectedAccount.accountId}) with priority ${selectedAccount.priority}` `🎯 Selected account from group: ${selectedAccount.name} (${selectedAccount.accountId}) with priority ${selectedAccount.priority}`
) )
// 更新账户的最后使用时间
await openaiAccountService.recordUsage(selectedAccount.accountId, 0)
return { return {
accountId: selectedAccount.accountId, accountId: selectedAccount.accountId,
accountType: selectedAccount.accountType accountType: selectedAccount.accountType

View File

@@ -355,8 +355,8 @@
<label class="inline-flex cursor-pointer items-center"> <label class="inline-flex cursor-pointer items-center">
<input <input
v-model="form.enableRateLimit" v-model="form.enableRateLimit"
type="checkbox"
class="mr-2 rounded border-gray-300 text-blue-600 focus:border-blue-500 focus:ring focus:ring-blue-200" class="mr-2 rounded border-gray-300 text-blue-600 focus:border-blue-500 focus:ring focus:ring-blue-200"
type="checkbox"
/> />
<span class="text-sm text-gray-700">启用限流机制</span> <span class="text-sm text-gray-700">启用限流机制</span>
</label> </label>
@@ -529,8 +529,8 @@
<label class="inline-flex cursor-pointer items-center"> <label class="inline-flex cursor-pointer items-center">
<input <input
v-model="form.enableRateLimit" v-model="form.enableRateLimit"
type="checkbox"
class="mr-2 rounded border-gray-300 text-blue-600 focus:border-blue-500 focus:ring focus:ring-blue-200" class="mr-2 rounded border-gray-300 text-blue-600 focus:border-blue-500 focus:ring focus:ring-blue-200"
type="checkbox"
/> />
<span class="text-sm text-gray-700">启用限流机制</span> <span class="text-sm text-gray-700">启用限流机制</span>
</label> </label>
@@ -1115,8 +1115,8 @@
<label class="inline-flex cursor-pointer items-center"> <label class="inline-flex cursor-pointer items-center">
<input <input
v-model="form.enableRateLimit" v-model="form.enableRateLimit"
type="checkbox"
class="mr-2 rounded border-gray-300 text-blue-600 focus:border-blue-500 focus:ring focus:ring-blue-200" class="mr-2 rounded border-gray-300 text-blue-600 focus:border-blue-500 focus:ring focus:ring-blue-200"
type="checkbox"
/> />
<span class="text-sm text-gray-700">启用限流机制</span> <span class="text-sm text-gray-700">启用限流机制</span>
</label> </label>
@@ -1234,8 +1234,8 @@
<label class="inline-flex cursor-pointer items-center"> <label class="inline-flex cursor-pointer items-center">
<input <input
v-model="form.enableRateLimit" v-model="form.enableRateLimit"
type="checkbox"
class="mr-2 rounded border-gray-300 text-blue-600 focus:border-blue-500 focus:ring focus:ring-blue-200" class="mr-2 rounded border-gray-300 text-blue-600 focus:border-blue-500 focus:ring focus:ring-blue-200"
type="checkbox"
/> />
<span class="text-sm text-gray-700">启用限流机制</span> <span class="text-sm text-gray-700">启用限流机制</span>
</label> </label>

View File

@@ -325,6 +325,17 @@
<p class="mb-2 text-sm text-orange-700"> <p class="mb-2 text-sm text-orange-700">
请在新标签页中打开授权链接登录您的 OpenAI 账户并授权 请在新标签页中打开授权链接登录您的 OpenAI 账户并授权
</p> </p>
<div class="mb-3 rounded border border-amber-300 bg-amber-50 p-3">
<p class="text-xs text-amber-800">
<i class="fas fa-clock mr-1" />
<strong>重要提示</strong>授权后页面可能会加载较长时间请耐心等待
</p>
<p class="mt-2 text-xs text-amber-700">
当浏览器地址栏变为
<strong class="font-mono">http://localhost:1455/...</strong>
开头时表示授权已完成
</p>
</div>
<div class="rounded border border-yellow-300 bg-yellow-50 p-3"> <div class="rounded border border-yellow-300 bg-yellow-50 p-3">
<p class="text-xs text-yellow-800"> <p class="text-xs text-yellow-800">
<i class="fas fa-exclamation-triangle mr-1" /> <i class="fas fa-exclamation-triangle mr-1" />
@@ -345,27 +356,40 @@
3 3
</div> </div>
<div class="flex-1"> <div class="flex-1">
<p class="mb-2 font-medium text-orange-900">输入 Authorization Code</p> <p class="mb-2 font-medium text-orange-900">输入授权链接或 Code</p>
<p class="mb-3 text-sm text-orange-700"> <p class="mb-3 text-sm text-orange-700">
授权完成后页面会显示一个 授权完成后页面地址变为
<strong>Authorization Code</strong>请将其复制并粘贴到下方输入框 <strong class="font-mono">http://localhost:1455/...</strong> 时:
</p> </p>
<div class="space-y-3"> <div class="space-y-3">
<div> <div>
<label class="mb-2 block text-sm font-semibold text-gray-700"> <label class="mb-2 block text-sm font-semibold text-gray-700">
<i class="fas fa-key mr-2 text-orange-500" />Authorization Code <i class="fas fa-link mr-2 text-orange-500" />授权链接或 Code
</label> </label>
<textarea <textarea
v-model="authCode" v-model="authCode"
class="form-input w-full resize-none font-mono text-sm" class="form-input w-full resize-none font-mono text-sm"
placeholder="粘贴从OpenAI页面获取的Authorization Code..." placeholder="方式1复制完整的链接http://localhost:1455/auth/callback?code=...&#10;方式2仅复制 code 参数的值&#10;系统会自动识别并提取所需信息"
rows="3" rows="3"
/> />
</div> </div>
<p class="mt-2 text-xs text-gray-500"> <div class="rounded border border-blue-300 bg-blue-50 p-2">
<i class="fas fa-info-circle mr-1" /> <p class="text-xs text-blue-700">
请粘贴从OpenAI页面复制的Authorization Code <i class="fas fa-lightbulb mr-1" />
</p> <strong>提示</strong>您可以直接复制整个链接或仅复制 code
参数值系统会自动识别
</p>
<p class="mt-1 text-xs text-blue-600">
完整链接示例<span class="font-mono"
>http://localhost:1455/auth/callback?code=ac_4hm8...</span
>
</p>
<p class="text-xs text-blue-600">
Code 示例<span class="font-mono"
>ac_4hm8iqmx9A2fzMy_cwye7U3W7...</span
>
</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -444,8 +468,11 @@ watch(authCode, (newValue) => {
// 如果是 URL 格式 // 如果是 URL 格式
if (isUrl) { if (isUrl) {
// 检查是否是正确的 localhost:45462 开头的 URL // 检查是否是正确的 localhost 回调 URL
if (trimmedValue.startsWith('http://localhost:45462')) { if (
trimmedValue.startsWith('http://localhost:45462') ||
trimmedValue.startsWith('http://localhost:1455')
) {
try { try {
const url = new URL(trimmedValue) const url = new URL(trimmedValue)
const code = url.searchParams.get('code') const code = url.searchParams.get('code')
@@ -479,8 +506,8 @@ watch(authCode, (newValue) => {
// 不是有效的URL保持原值 // 不是有效的URL保持原值
} }
} else { } else {
// 错误的 URL不是 localhost:45462 开头 // 错误的 URL不是正确的 localhost 回调地址
showToast('请粘贴以 http://localhost:45462 开头的链接', 'error') showToast('请粘贴以 http://localhost:1455 或 http://localhost:45462 开头的链接', 'error')
} }
} }
// 如果不是 URL保持原值兼容直接输入授权码 // 如果不是 URL保持原值兼容直接输入授权码

View File

@@ -47,6 +47,30 @@
</div> </div>
</div> </div>
<!-- 搜索框 -->
<div class="group relative min-w-[200px]">
<div
class="absolute -inset-0.5 rounded-lg bg-gradient-to-r from-cyan-500 to-teal-500 opacity-0 blur transition duration-300 group-hover:opacity-20"
></div>
<div class="relative flex items-center">
<input
v-model="searchKeyword"
class="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 pl-9 text-sm text-gray-700 placeholder-gray-400 shadow-sm transition-all duration-200 hover:border-gray-300 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20"
placeholder="搜索名称..."
type="text"
@input="currentPage = 1"
/>
<i class="fas fa-search absolute left-3 text-sm text-cyan-500" />
<button
v-if="searchKeyword"
class="absolute right-2 flex h-5 w-5 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-600"
@click="clearSearch"
>
<i class="fas fa-times text-xs" />
</button>
</div>
</div>
<!-- 刷新按钮 --> <!-- 刷新按钮 -->
<button <button
class="group relative flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:shadow-md disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto" class="group relative flex items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300 hover:shadow-md disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
@@ -1153,6 +1177,9 @@ const selectedApiKeyForDetail = ref(null)
const selectedTagFilter = ref('') const selectedTagFilter = ref('')
const availableTags = ref([]) const availableTags = ref([])
// 搜索相关
const searchKeyword = ref('')
// 下拉选项数据 // 下拉选项数据
const timeRangeOptions = ref([ const timeRangeOptions = ref([
{ value: 'today', label: '今日', icon: 'fa-clock' }, { value: 'today', label: '今日', icon: 'fa-clock' },
@@ -1201,6 +1228,14 @@ const sortedApiKeys = computed(() => {
) )
} }
// 然后进行名称搜索
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase().trim()
filteredKeys = filteredKeys.filter(
(key) => key.name && key.name.toLowerCase().includes(keyword)
)
}
// 如果没有排序字段,返回筛选后的结果 // 如果没有排序字段,返回筛选后的结果
if (!apiKeysSortBy.value) return filteredKeys if (!apiKeysSortBy.value) return filteredKeys
@@ -1939,8 +1974,14 @@ const formatLastUsed = (dateString) => {
return date.toLocaleDateString('zh-CN') return date.toLocaleDateString('zh-CN')
} }
// 清除搜索
const clearSearch = () => {
searchKeyword.value = ''
currentPage.value = 1
}
// 监听筛选条件变化,重置页码 // 监听筛选条件变化,重置页码
watch([selectedTagFilter, apiKeyStatsTimeRange], () => { watch([selectedTagFilter, apiKeyStatsTimeRange, searchKeyword], () => {
currentPage.value = 1 currentPage.value = 1
}) })