feat: OAuth 账户管理、维护调度、端点健康检查增强及前端优化

- 新增 OAuth 账户管理对话框和提供商详情抽屉中的 OAuth 信息展示
- 新增维护调度器(maintenance_scheduler)支持定时清理和健康检查
- 增强端点健康检查器,支持更多检测策略
- 重构 codex 服务为 metadata_collectors 模块
- 优化 OpenAI CLI normalizer 代码结构
- 前端: 改进使用量表格、统计图表、指南页面和异步任务管理
- 扩展多个数据库字符串列为 TEXT 类型
- 新增倒计时 composable 和 provider OAuth API 端点
This commit is contained in:
fawney19
2026-02-04 23:59:45 +08:00
parent 24c9105628
commit 4d6e7c094f
64 changed files with 3885 additions and 930 deletions
@@ -0,0 +1,232 @@
<template>
<Dialog
:model-value="isOpen"
title="添加账号"
:icon="UserPlus"
size="md"
@update:model-value="handleDialogUpdate"
>
<div class="space-y-6">
<!-- 加载中 -->
<div
v-if="oauth.starting && !oauth.authorization_url"
class="py-12 text-center"
>
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4" />
<p class="text-sm text-muted-foreground">
正在准备授权...
</p>
</div>
<!-- 授权流程 -->
<template v-else-if="oauth.authorization_url">
<!-- 步骤 1: 打开授权链接 -->
<div class="space-y-3">
<div class="flex items-center gap-2">
<div class="w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-medium shrink-0">
1
</div>
<span class="text-sm font-medium">打开授权链接</span>
</div>
<p class="text-xs text-muted-foreground pl-7">
点击下方按钮在浏览器中完成登录授权
</p>
<div class="ml-7 p-2.5 rounded-md bg-muted/50 border border-border/50">
<p class="text-xs font-mono text-muted-foreground break-all line-clamp-3 leading-relaxed">
{{ oauth.authorization_url }}
</p>
</div>
<div class="flex gap-2 pl-7">
<Button
size="sm"
:disabled="oauthBusy"
@click="openAuthorizationUrl"
>
<ExternalLink class="w-3.5 h-3.5 mr-1.5" />
前往授权
</Button>
<Button
size="sm"
variant="outline"
:disabled="oauthBusy"
@click="copyToClipboard(oauth.authorization_url)"
>
<Copy class="w-3.5 h-3.5 mr-1.5" />
复制链接
</Button>
</div>
</div>
<!-- 步骤 2: 粘贴回调地址 -->
<div class="space-y-3">
<div class="flex items-center gap-2">
<div class="w-5 h-5 rounded-full bg-muted text-muted-foreground flex items-center justify-center text-xs font-medium shrink-0">
2
</div>
<span class="text-sm font-medium">粘贴回调地址</span>
</div>
<p class="text-xs text-muted-foreground pl-7">
授权完成后,复制浏览器地址栏的完整 URL 并粘贴到下方
</p>
<div class="pl-7">
<Textarea
v-model="oauth.callback_url"
:disabled="oauthBusy"
placeholder="http://localhost:xxx/callback?code=..."
class="min-h-[80px] text-xs font-mono resize-none"
spellcheck="false"
/>
</div>
</div>
</template>
</div>
<template #footer>
<Button
variant="outline"
@click="handleClose"
>
取消
</Button>
<Button
:disabled="!canCompleteOAuth"
@click="handleCompleteOAuth"
>
{{ oauth.completing ? '验证中...' : '完成授权' }}
</Button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { Dialog, Button, Textarea } from '@/components/ui'
import { UserPlus, Copy, ExternalLink } from 'lucide-vue-next'
import { useToast } from '@/composables/useToast'
import { useClipboard } from '@/composables/useClipboard'
import { parseApiError } from '@/utils/errorParser'
import {
startProviderLevelOAuth,
completeProviderLevelOAuth,
} from '@/api/endpoints'
const props = defineProps<{
open: boolean
providerId: string | null
}>()
const emit = defineEmits<{
close: []
saved: []
}>()
const { success, error: showError } = useToast()
const { copyToClipboard } = useClipboard()
// OAuth 状态
interface OAuthState {
authorization_url: string
redirect_uri: string
instructions: string
provider_type: string
callback_url: string
starting: boolean
completing: boolean
}
function createInitialOAuthState(): OAuthState {
return {
authorization_url: '',
redirect_uri: '',
instructions: '',
provider_type: '',
callback_url: '',
starting: false,
completing: false,
}
}
const oauth = ref<OAuthState>(createInitialOAuthState())
const isOpen = computed(() => props.open)
const oauthBusy = computed(() =>
oauth.value.starting || oauth.value.completing
)
const canCompleteOAuth = computed(() => {
if (!oauth.value.authorization_url) return false
if (!oauth.value.callback_url.trim()) return false
return !oauthBusy.value
})
function resetForm() {
oauth.value = createInitialOAuthState()
}
function handleDialogUpdate(value: boolean) {
if (!value) {
handleClose()
}
}
function handleClose() {
resetForm()
emit('close')
}
function openAuthorizationUrl() {
const url = oauth.value.authorization_url
if (!url) return
window.open(url, '_blank', 'noopener,noreferrer')
}
// 对话框打开时获取授权 URL(不创建 key)
async function initOAuth() {
if (!props.providerId) return
oauth.value.starting = true
try {
const resp = await startProviderLevelOAuth(props.providerId)
oauth.value.authorization_url = resp.authorization_url
oauth.value.redirect_uri = resp.redirect_uri
oauth.value.instructions = resp.instructions
oauth.value.provider_type = resp.provider_type
} catch (err: any) {
const errorMessage = parseApiError(err, '初始化授权失败')
showError(errorMessage, '错误')
handleClose()
} finally {
oauth.value.starting = false
}
}
// 完成授权(此时才创建 key)
async function handleCompleteOAuth() {
if (!canCompleteOAuth.value || !props.providerId) return
oauth.value.completing = true
try {
await completeProviderLevelOAuth(props.providerId, {
callback_url: oauth.value.callback_url.trim(),
})
success('授权成功,账号已添加')
emit('saved')
handleClose()
} catch (err: any) {
const errorMessage = parseApiError(err, '完成授权失败')
showError(errorMessage, '错误')
} finally {
oauth.value.completing = false
}
}
// 监听对话框打开
watch(() => props.open, (newOpen) => {
if (newOpen) {
initOAuth()
} else {
resetForm()
}
})
</script>