mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 18:37:46 +08:00
feat: OAuth 账户管理、维护调度、端点健康检查增强及前端优化
- 新增 OAuth 账户管理对话框和提供商详情抽屉中的 OAuth 信息展示 - 新增维护调度器(maintenance_scheduler)支持定时清理和健康检查 - 增强端点健康检查器,支持更多检测策略 - 重构 codex 服务为 metadata_collectors 模块 - 优化 OpenAI CLI normalizer 代码结构 - 前端: 改进使用量表格、统计图表、指南页面和异步任务管理 - 扩展多个数据库字符串列为 TEXT 类型 - 新增倒计时 composable 和 provider OAuth API 端点
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user