mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 10:57:03 +08:00
Fix OAuth token import and table filters
This commit is contained in:
@@ -410,8 +410,8 @@
|
||||
:reset-key="importInputResetKey"
|
||||
drop-title="拖入授权文件或点击选择"
|
||||
drop-hint="支持 .json / .txt,可多选"
|
||||
manual-placeholder="粘贴 Refresh Token 或 JSON 内容"
|
||||
paste-toggle-text="或手动粘贴 Refresh Token"
|
||||
manual-placeholder="粘贴 Refresh Token / Access Token 或 JSON 内容"
|
||||
paste-toggle-text="或手动粘贴 Token"
|
||||
file-toggle-text="或选择 JSON 文件导入"
|
||||
textarea-class="min-h-[200px] text-xs font-mono break-all !rounded-xl"
|
||||
@error="handleImportInputError"
|
||||
@@ -949,7 +949,7 @@ function isBatchImport(text: string): boolean {
|
||||
return lines.length > 1
|
||||
}
|
||||
|
||||
function parseImportText(text: string): { refresh_token: string; name?: string } | null {
|
||||
function parseImportText(text: string): { refresh_token?: string; access_token?: string; name?: string } | null {
|
||||
const trimmed = text.trim()
|
||||
if (!trimmed) return null
|
||||
|
||||
@@ -963,9 +963,19 @@ function parseImportText(text: string): { refresh_token: string; name?: string }
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
const obj = parsed as Record<string, unknown>
|
||||
const refreshToken = obj.refresh_token
|
||||
if (typeof refreshToken === 'string' && refreshToken.trim()) {
|
||||
const refreshTokenCamel = obj.refreshToken
|
||||
const accessToken = obj.access_token
|
||||
const accessTokenCamel = obj.accessToken
|
||||
const normalizedRefreshToken = typeof refreshToken === 'string' && refreshToken.trim()
|
||||
? refreshToken.trim()
|
||||
: (typeof refreshTokenCamel === 'string' && refreshTokenCamel.trim() ? refreshTokenCamel.trim() : undefined)
|
||||
const normalizedAccessToken = typeof accessToken === 'string' && accessToken.trim()
|
||||
? accessToken.trim()
|
||||
: (typeof accessTokenCamel === 'string' && accessTokenCamel.trim() ? accessTokenCamel.trim() : undefined)
|
||||
if (normalizedRefreshToken || normalizedAccessToken) {
|
||||
return {
|
||||
refresh_token: refreshToken.trim(),
|
||||
refresh_token: normalizedRefreshToken,
|
||||
access_token: normalizedAccessToken,
|
||||
name: (typeof obj.name === 'string' ? obj.name : undefined) || (typeof obj.oauth_email === 'string' ? obj.oauth_email : undefined),
|
||||
}
|
||||
}
|
||||
@@ -975,9 +985,34 @@ function parseImportText(text: string): { refresh_token: string; name?: string }
|
||||
// Not JSON: treat as raw token.
|
||||
}
|
||||
|
||||
if (isLikelyJwtToken(trimmed)) {
|
||||
return { access_token: trimmed }
|
||||
}
|
||||
|
||||
return { refresh_token: trimmed }
|
||||
}
|
||||
|
||||
function isLikelyJwtToken(token: string): boolean {
|
||||
const parts = token.trim().split('.')
|
||||
if (parts.length !== 3 || parts.some(part => !part)) return false
|
||||
|
||||
try {
|
||||
const header = JSON.parse(decodeBase64Url(parts[0])) as Record<string, unknown>
|
||||
const payload = JSON.parse(decodeBase64Url(parts[1])) as Record<string, unknown>
|
||||
const tokenType = typeof header.typ === 'string' ? header.typ.toLowerCase() : ''
|
||||
if (tokenType && tokenType !== 'jwt' && tokenType !== 'at+jwt') return false
|
||||
return ['exp', 'aud', 'iss', 'scope', 'scp'].some(key => key in payload)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function decodeBase64Url(value: string): string {
|
||||
const normalized = value.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), '=')
|
||||
return atob(padded)
|
||||
}
|
||||
|
||||
function handleImportInputError(payload: { message: string; title?: string }) {
|
||||
showError(payload.message, payload.title)
|
||||
}
|
||||
|
||||
@@ -348,7 +348,7 @@
|
||||
<Copy class="w-2.5 h-2.5" />
|
||||
</Button>
|
||||
<!-- OAuth 状态(失效/过期/倒计时)和刷新按钮 -->
|
||||
<template v-if="getKeyOAuthExpires(key)">
|
||||
<template v-if="shouldShowOAuthRefreshControl(key)">
|
||||
<!-- 账号级别异常:醒目提示 + 清除按钮 -->
|
||||
<template v-if="isAccountLevelBlock(key)">
|
||||
<Badge
|
||||
@@ -386,11 +386,19 @@
|
||||
>
|
||||
{{ getKeyOAuthExpires(key)?.text }}
|
||||
</span>
|
||||
<Badge
|
||||
v-if="key.oauth_temporary"
|
||||
variant="outline"
|
||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||
title="仅通过 Access Token 导入,无法自动刷新,到期后需要重新导入"
|
||||
>
|
||||
临时
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-4 w-4 shrink-0"
|
||||
:disabled="refreshingOAuthKeyId === key.id"
|
||||
:disabled="refreshingOAuthKeyId === key.id || !canRefreshOAuthCredential(key)"
|
||||
:title="getOAuthRefreshButtonTitle(key)"
|
||||
@click.stop="handleRefreshOAuth(key)"
|
||||
>
|
||||
@@ -1172,14 +1180,17 @@ import { getOAuthRefreshFeedback } from '@/utils/oauthRefreshFeedback'
|
||||
import {
|
||||
canEditOAuthCredential,
|
||||
canExportOAuthCredential,
|
||||
canRefreshOAuthCredential,
|
||||
isOAuthManagedCredential,
|
||||
isServiceAccountCredential,
|
||||
shouldShowOAuthRefreshControl,
|
||||
} from '@/utils/providerKeyAuth'
|
||||
import {
|
||||
getAccountStatusDisplay,
|
||||
getAccountStatusTitle,
|
||||
getOAuthRefreshButtonTitle as resolveOAuthRefreshButtonTitle,
|
||||
getOAuthStatusDisplay,
|
||||
getOAuthStatusDisplayWithFallback,
|
||||
getOAuthStatusTitle as resolveOAuthStatusTitle,
|
||||
} from '@/utils/providerKeyStatus'
|
||||
|
||||
@@ -3104,7 +3115,7 @@ function getOAuthPlanTypeClass(planType: string): string {
|
||||
|
||||
// OAuth 状态信息(包括失效和过期)
|
||||
function getKeyOAuthExpires(key: EndpointAPIKey) {
|
||||
return getOAuthStatusDisplay(key, countdownTick.value)
|
||||
return getOAuthStatusDisplayWithFallback(key, countdownTick.value)
|
||||
}
|
||||
|
||||
function getOAuthRefreshButtonTitle(key: EndpointAPIKey): string {
|
||||
|
||||
@@ -22,61 +22,67 @@
|
||||
</div>
|
||||
|
||||
<!-- 状态筛选 -->
|
||||
<Select
|
||||
:model-value="filterStatus"
|
||||
@update:model-value="$emit('update:filterStatus', $event)"
|
||||
>
|
||||
<SelectTrigger class="w-20 sm:w-28 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="status in statusFilters"
|
||||
:key="status.value"
|
||||
:value="status.value"
|
||||
>
|
||||
{{ status.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="xl:hidden">
|
||||
<Select
|
||||
:model-value="filterStatus"
|
||||
@update:model-value="$emit('update:filterStatus', $event)"
|
||||
>
|
||||
<SelectTrigger class="w-20 sm:w-28 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="status in statusFilters"
|
||||
:key="status.value"
|
||||
:value="status.value"
|
||||
>
|
||||
{{ status.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- API 格式筛选 -->
|
||||
<Select
|
||||
:model-value="filterApiFormat"
|
||||
@update:model-value="$emit('update:filterApiFormat', $event)"
|
||||
>
|
||||
<SelectTrigger class="w-20 sm:w-28 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部格式" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="fmt in apiFormatFilters"
|
||||
:key="fmt.value"
|
||||
:value="fmt.value"
|
||||
>
|
||||
{{ fmt.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="xl:hidden">
|
||||
<Select
|
||||
:model-value="filterApiFormat"
|
||||
@update:model-value="$emit('update:filterApiFormat', $event)"
|
||||
>
|
||||
<SelectTrigger class="w-20 sm:w-28 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部格式" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="fmt in apiFormatFilters"
|
||||
:key="fmt.value"
|
||||
:value="fmt.value"
|
||||
>
|
||||
{{ fmt.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- 模型筛选 -->
|
||||
<Select
|
||||
:model-value="filterModel"
|
||||
@update:model-value="$emit('update:filterModel', $event)"
|
||||
>
|
||||
<SelectTrigger class="w-20 sm:w-36 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部模型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="model in modelFilters"
|
||||
:key="model.value"
|
||||
:value="model.value"
|
||||
>
|
||||
{{ model.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="xl:hidden">
|
||||
<Select
|
||||
:model-value="filterModel"
|
||||
@update:model-value="$emit('update:filterModel', $event)"
|
||||
>
|
||||
<SelectTrigger class="w-20 sm:w-36 h-8 text-xs border-border/60">
|
||||
<SelectValue placeholder="全部模型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="model in modelFilters"
|
||||
:key="model.value"
|
||||
:value="model.value"
|
||||
>
|
||||
{{ model.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- 重置筛选 -->
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user