feat: OAuth Token 自动刷新调度、OAuth 对话框优化及 OpenAI CLI normalizer 重构

- 系统设置新增 OAuth Token 自动刷新任务开关,支持动态调度
- OAuth 授权对话框简化步骤布局,移除编号圆圈样式
- 重构 OpenAI CLI normalizer:将 if-else 链替换为事件处理器映射表,
  将 stream_event_from_internal 拆分为独立方法
- 维护调度器在禁用刷新时移除已调度的 job
- .gitignore 新增 CLIProxyAPI/ 和 sub2api/ 排除项
- 补充 noop/unknown 事件处理器的测试覆盖
This commit is contained in:
fawney19
2026-02-05 01:07:47 +08:00
parent ba4d88e8ce
commit 9a8f25d1a9
7 changed files with 609 additions and 513 deletions

View File

@@ -6,7 +6,7 @@
size="md"
@update:model-value="handleDialogUpdate"
>
<div class="space-y-6">
<div class="space-y-5">
<!-- 加载中 -->
<div
v-if="oauth.starting && !oauth.authorization_url"
@@ -21,22 +21,14 @@
<!-- 授权流程 -->
<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">
<div class="space-y-2">
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
第一步 · 前往授权
</p>
<p class="text-xs text-muted-foreground">
点击下方按钮在浏览器中完成登录授权
</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">
<div class="flex gap-2 pt-1">
<Button
size="sm"
:disabled="oauthBusy"
@@ -57,28 +49,26 @@
</div>
</div>
<Separator />
<!-- 步骤 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">
<div class="space-y-2">
<p class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
第二步 · 粘贴回调
</p>
<p class="text-xs text-muted-foreground">
授权完成后复制浏览器地址栏的完整 URL 并粘贴到下方
</p>
<div class="pl-7">
<div class="pt-1">
<Textarea
v-model="oauth.callback_url"
:disabled="oauthBusy"
placeholder="http://localhost:xxx/callback?code=..."
class="min-h-[80px] text-xs font-mono resize-none"
class="min-h-[80px] text-xs font-mono break-all !rounded-xl"
spellcheck="false"
/>
</div>
</div>
</template>
</div>
@@ -93,7 +83,7 @@
:disabled="!canCompleteOAuth"
@click="handleCompleteOAuth"
>
{{ oauth.completing ? '验证中...' : '完成授权' }}
{{ oauth.completing ? '验证中...' : '验证' }}
</Button>
</template>
</Dialog>
@@ -101,7 +91,7 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { Dialog, Button, Textarea } from '@/components/ui'
import { Dialog, Button, Textarea, Separator } from '@/components/ui'
import { UserPlus, Copy, ExternalLink } from 'lucide-vue-next'
import { useToast } from '@/composables/useToast'
import { useClipboard } from '@/composables/useClipboard'

View File

@@ -534,7 +534,7 @@
<!-- 右侧时间选择器 + 保存按钮 -->
<div
v-if="task.enabled"
v-if="task.enabled && task.hasTimeConfig"
class="flex items-center gap-2 shrink-0"
>
<Clock class="w-4 h-4 text-muted-foreground" />
@@ -995,7 +995,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { Download, Upload, CalendarCheck, RotateCcw, Clock, Check, Loader2 } from 'lucide-vue-next'
import { Download, Upload, CalendarCheck, RotateCcw, RefreshCw, Clock, Check, Loader2 } from 'lucide-vue-next'
import Button from '@/components/ui/button.vue'
import Input from '@/components/ui/input.vue'
import Label from '@/components/ui/label.vue'
@@ -1044,6 +1044,7 @@ interface SystemConfig {
enable_user_quota_reset: boolean
user_quota_reset_time: string
user_quota_reset_interval_days: number
enable_oauth_token_refresh: boolean
}
const basicConfigLoading = ref(false)
@@ -1104,6 +1105,7 @@ const systemConfig = ref<SystemConfig>({
enable_user_quota_reset: false,
user_quota_reset_time: '05:00',
user_quota_reset_interval_days: 1,
enable_oauth_token_refresh: true,
})
// 原始配置值(用于检测变动)
@@ -1215,6 +1217,7 @@ async function loadSystemConfig() {
'enable_user_quota_reset',
'user_quota_reset_time',
'user_quota_reset_interval_days',
'enable_oauth_token_refresh',
]
for (const key of configs) {
@@ -1423,6 +1426,28 @@ async function handleUserQuotaResetToggle(enabled: boolean) {
}
}
async function handleOAuthTokenRefreshToggle(enabled: boolean) {
const previousValue = systemConfig.value.enable_oauth_token_refresh
systemConfig.value.enable_oauth_token_refresh = enabled
try {
await adminApi.updateSystemConfig(
'enable_oauth_token_refresh',
enabled,
'是否启用 OAuth Token 自动刷新任务'
)
success(enabled ? '已启用 OAuth Token 自动刷新' : '已禁用 OAuth Token 自动刷新')
} catch (err) {
error('保存配置失败')
log.error('保存 OAuth Token 自动刷新配置失败:', err)
// 回滚状态
systemConfig.value.enable_oauth_token_refresh = previousValue
}
}
// OAuth Token 刷新(无时间配置,占位 ref
const oauthRefreshHourSelectOpen = ref(false)
const oauthRefreshMinuteSelectOpen = ref(false)
// 用户配额重置时间相关
const previousUserQuotaResetTime = ref('')
const userQuotaResetHourSelectOpen = ref(false)
@@ -1465,6 +1490,7 @@ const scheduledTasks = computed(() => [
title: 'Provider 自动签到',
description: '自动执行已配置 Provider 的签到任务',
enabled: systemConfig.value.enable_provider_checkin,
hasTimeConfig: true,
hour: checkinHour.value,
minute: checkinMinute.value,
hourSelectOpen: checkinHourSelectOpen,
@@ -1481,6 +1507,7 @@ const scheduledTasks = computed(() => [
title: '用户配额自动重置',
description: '定时将用户已使用配额重置为零',
enabled: systemConfig.value.enable_user_quota_reset,
hasTimeConfig: true,
hour: userQuotaResetHour.value,
minute: userQuotaResetMinute.value,
hourSelectOpen: userQuotaResetHourSelectOpen,
@@ -1491,6 +1518,23 @@ const scheduledTasks = computed(() => [
onToggle: handleUserQuotaResetToggle,
onSave: handleQuotaResetConfigSave,
},
{
id: 'oauth-token-refresh',
icon: RefreshCw,
title: 'OAuth Token 自动刷新',
description: '主动刷新即将过期的 OAuth Token动态调度',
enabled: systemConfig.value.enable_oauth_token_refresh,
hasTimeConfig: false,
hour: '',
minute: '',
hourSelectOpen: oauthRefreshHourSelectOpen,
minuteSelectOpen: oauthRefreshMinuteSelectOpen,
updateTime: () => {},
hasChanges: false,
loading: false,
onToggle: handleOAuthTokenRefreshToggle,
onSave: () => {},
},
])
// Provider 签到时间保存