feat: 实现 OpenAI token 自动刷新功能并优化账户管理界面

主要更改:
1. OpenAI Token 自动刷新
   - 实现 refreshAccessToken 函数,支持 OAuth 2.0 refresh_token grant type
   - 使用 Codex CLI 官方 CLIENT_ID (app_EMoamEEZ73f0CkXaXp7hrann)
   - 支持 SOCKS5 和 HTTP/HTTPS 代理
   - 自动更新 access token、id token 和 refresh token

2. 账户管理界面优化
   - 移除手动刷新 token 按钮(桌面端和移动端)
   - 保留后端自动刷新机制
   - 优化代码结构,删除不再需要的函数和变量

3. 测试和文档
   - 添加 test-openai-refresh.js 测试脚本
   - 创建详细的实现文档

技术细节:
- Token 端点: https://auth.openai.com/oauth/token
- 默认有效期: 1小时
- 加密存储: AES-256-CBC

所有平台现在都支持自动 token 刷新:
 Claude - OAuth 自动刷新
 Gemini - Google OAuth2 自动刷新
 OpenAI - OAuth 自动刷新(新实现)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-08-15 16:56:44 +08:00
parent 3e605f0052
commit 812e98355f
12 changed files with 424 additions and 178 deletions

View File

@@ -584,14 +584,8 @@
</p>
</div>
<!-- ClaudeClaude Console和Bedrock的优先级设置 -->
<div
v-if="
form.platform === 'claude' ||
form.platform === 'claude-console' ||
form.platform === 'bedrock'
"
>
<!-- 所有平台的优先级设置 -->
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700"
>调度优先级 (1-100)</label
>
@@ -1019,14 +1013,8 @@
</p>
</div>
<!-- Claude、Claude Console和Bedrock的优先级设置(编辑模式) -->
<div
v-if="
form.platform === 'claude' ||
form.platform === 'claude-console' ||
form.platform === 'bedrock'
"
>
<!-- 所有平台的优先级设置(编辑模式) -->
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700">调度优先级 (1-100)</label>
<input
v-model.number="form.priority"
@@ -1750,6 +1738,8 @@ const handleOAuthSuccess = async (tokenInfo) => {
if (form.value.projectId) {
data.projectId = form.value.projectId
}
// 添加 Gemini 优先级
data.priority = form.value.priority || 50
} else if (form.value.platform === 'openai') {
data.openaiOauth = tokenInfo.tokens || tokenInfo
data.accountInfo = tokenInfo.accountInfo
@@ -1869,7 +1859,7 @@ const createAccount = async () => {
accessToken: form.value.accessToken,
refreshToken: form.value.refreshToken || '',
expiresAt: Date.now() + expiresInMs,
scopes: ['user:inference']
scopes: [] // 手动添加没有 scopes
}
data.priority = form.value.priority || 50
// 添加订阅类型信息
@@ -1896,6 +1886,9 @@ const createAccount = async () => {
if (form.value.projectId) {
data.projectId = form.value.projectId
}
// 添加 Gemini 优先级
data.priority = form.value.priority || 50
} else if (form.value.platform === 'openai') {
// OpenAI手动模式需要构建openaiOauth对象
const expiresInMs = form.value.refreshToken
@@ -2058,7 +2051,7 @@ const updateAccount = async () => {
accessToken: form.value.accessToken || '',
refreshToken: form.value.refreshToken || '',
expiresAt: Date.now() + expiresInMs,
scopes: ['user:inference']
scopes: props.account.scopes || [] // 保持原有的 scopes如果没有则为空数组
}
} else if (props.account.platform === 'gemini') {
// Gemini需要构建geminiOauth对象
@@ -2109,6 +2102,11 @@ const updateAccount = async () => {
data.priority = form.value.priority || 50
}
// Gemini 账号优先级更新
if (props.account.platform === 'gemini') {
data.priority = form.value.priority || 50
}
// Claude Console 特定更新
if (props.account.platform === 'claude-console') {
data.apiUrl = form.value.apiUrl

View File

@@ -436,6 +436,18 @@
platform="openai"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600">Bedrock 专属账号</label>
<AccountSelector
v-model="form.bedrockAccountId"
:accounts="localAccounts.bedrock"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
:groups="[]"
placeholder="请选择Bedrock账号"
platform="bedrock"
/>
</div>
</div>
<p class="mt-2 text-xs text-gray-500">
选择专属账号后此API Key将只使用该账号不选择则使用共享账号池
@@ -618,6 +630,7 @@ const localAccounts = ref({
claude: [],
gemini: [],
openai: [],
bedrock: [], // 添加 Bedrock 账号列表
claudeGroups: [],
geminiGroups: [],
openaiGroups: []
@@ -658,6 +671,7 @@ const form = reactive({
claudeAccountId: '',
geminiAccountId: '',
openaiAccountId: '',
bedrockAccountId: '', // 添加 Bedrock 账号ID
enableModelRestriction: false,
restrictedModels: [],
modelInput: '',
@@ -676,6 +690,7 @@ onMounted(async () => {
claude: props.accounts.claude || [],
gemini: props.accounts.gemini || [],
openai: props.accounts.openai || [],
bedrock: props.accounts.bedrock || [], // 添加 Bedrock 账号
claudeGroups: props.accounts.claudeGroups || [],
geminiGroups: props.accounts.geminiGroups || [],
openaiGroups: props.accounts.openaiGroups || []
@@ -687,13 +702,15 @@ onMounted(async () => {
const refreshAccounts = async () => {
accountsLoading.value = true
try {
const [claudeData, claudeConsoleData, geminiData, openaiData, groupsData] = await Promise.all([
apiClient.get('/admin/claude-accounts'),
apiClient.get('/admin/claude-console-accounts'),
apiClient.get('/admin/gemini-accounts'),
apiClient.get('/admin/openai-accounts'),
apiClient.get('/admin/account-groups')
])
const [claudeData, claudeConsoleData, geminiData, openaiData, bedrockData, groupsData] =
await Promise.all([
apiClient.get('/admin/claude-accounts'),
apiClient.get('/admin/claude-console-accounts'),
apiClient.get('/admin/gemini-accounts'),
apiClient.get('/admin/openai-accounts'),
apiClient.get('/admin/bedrock-accounts'), // 添加 Bedrock 账号获取
apiClient.get('/admin/account-groups')
])
// 合并Claude OAuth账户和Claude Console账户
const claudeAccounts = []
@@ -734,6 +751,13 @@ const refreshAccounts = async () => {
}))
}
if (bedrockData.success) {
localAccounts.value.bedrock = (bedrockData.data || []).map((account) => ({
...account,
isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容
}))
}
// 处理分组数据
if (groupsData.success) {
const allGroups = groupsData.data || []
@@ -939,6 +963,11 @@ const createApiKey = async () => {
baseData.openaiAccountId = form.openaiAccountId
}
// Bedrock账户绑定
if (form.bedrockAccountId) {
baseData.bedrockAccountId = form.bedrockAccountId
}
if (form.createType === 'single') {
// 单个创建
const data = {

View File

@@ -339,6 +339,18 @@
platform="openai"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600">Bedrock 专属账号</label>
<AccountSelector
v-model="form.bedrockAccountId"
:accounts="localAccounts.bedrock"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
:groups="[]"
placeholder="请选择Bedrock账号"
platform="bedrock"
/>
</div>
</div>
<p class="mt-2 text-xs text-gray-500">修改绑定账号将影响此API Key的请求路由</p>
</div>
@@ -522,6 +534,7 @@ const localAccounts = ref({
claude: [],
gemini: [],
openai: [],
bedrock: [], // 添加 Bedrock 账号列表
claudeGroups: [],
geminiGroups: [],
openaiGroups: []
@@ -551,6 +564,7 @@ const form = reactive({
claudeAccountId: '',
geminiAccountId: '',
openaiAccountId: '',
bedrockAccountId: '', // 添加 Bedrock 账号ID
enableModelRestriction: false,
restrictedModels: [],
modelInput: '',
@@ -673,6 +687,13 @@ const updateApiKey = async () => {
data.openaiAccountId = null
}
// Bedrock账户绑定
if (form.bedrockAccountId) {
data.bedrockAccountId = form.bedrockAccountId
} else {
data.bedrockAccountId = null
}
// 模型限制 - 始终提交这些字段
data.enableModelRestriction = form.enableModelRestriction
data.restrictedModels = form.restrictedModels
@@ -703,13 +724,15 @@ const updateApiKey = async () => {
const refreshAccounts = async () => {
accountsLoading.value = true
try {
const [claudeData, claudeConsoleData, geminiData, openaiData, groupsData] = await Promise.all([
apiClient.get('/admin/claude-accounts'),
apiClient.get('/admin/claude-console-accounts'),
apiClient.get('/admin/gemini-accounts'),
apiClient.get('/admin/openai-accounts'),
apiClient.get('/admin/account-groups')
])
const [claudeData, claudeConsoleData, geminiData, openaiData, bedrockData, groupsData] =
await Promise.all([
apiClient.get('/admin/claude-accounts'),
apiClient.get('/admin/claude-console-accounts'),
apiClient.get('/admin/gemini-accounts'),
apiClient.get('/admin/openai-accounts'),
apiClient.get('/admin/bedrock-accounts'), // 添加 Bedrock 账号获取
apiClient.get('/admin/account-groups')
])
// 合并Claude OAuth账户和Claude Console账户
const claudeAccounts = []
@@ -750,6 +773,13 @@ const refreshAccounts = async () => {
}))
}
if (bedrockData.success) {
localAccounts.value.bedrock = (bedrockData.data || []).map((account) => ({
...account,
isDedicated: account.accountType === 'dedicated'
}))
}
// 处理分组数据
if (groupsData.success) {
const allGroups = groupsData.data || []
@@ -778,6 +808,7 @@ onMounted(async () => {
claude: props.accounts.claude || [],
gemini: props.accounts.gemini || [],
openai: props.accounts.openai || [],
bedrock: props.accounts.bedrock || [], // 添加 Bedrock 账号
claudeGroups: props.accounts.claudeGroups || [],
geminiGroups: props.accounts.geminiGroups || [],
openaiGroups: props.accounts.openaiGroups || []
@@ -799,6 +830,7 @@ onMounted(async () => {
}
form.geminiAccountId = props.apiKey.geminiAccountId || ''
form.openaiAccountId = props.apiKey.openaiAccountId || ''
form.bedrockAccountId = props.apiKey.bedrockAccountId || '' // 添加 Bedrock 账号ID初始化
form.restrictedModels = props.apiKey.restrictedModels || []
form.allowedClients = props.apiKey.allowedClients || []
form.tags = props.apiKey.tags || []

View File

@@ -261,7 +261,7 @@
<span class="text-xs font-semibold text-yellow-800">Gemini</span>
<span class="mx-1 h-4 w-px bg-yellow-300" />
<span class="text-xs font-medium text-yellow-700">
{{ account.scopes && account.scopes.length > 0 ? 'OAuth' : '传统' }}
{{ getGeminiAuthType() }}
</span>
</div>
<div
@@ -289,7 +289,7 @@
<div class="fa-openai" />
<span class="text-xs font-semibold text-gray-950">OpenAi</span>
<span class="mx-1 h-4 w-px bg-gray-400" />
<span class="text-xs font-medium text-gray-950">Oauth</span>
<span class="text-xs font-medium text-gray-950">{{ getOpenAIAuthType() }}</span>
</div>
<div
v-else-if="account.platform === 'claude' || account.platform === 'claude-oauth'"
@@ -301,7 +301,7 @@
}}</span>
<span class="mx-1 h-4 w-px bg-indigo-300" />
<span class="text-xs font-medium text-indigo-700">
{{ account.scopes && account.scopes.length > 0 ? 'OAuth' : '传统' }}
{{ getClaudeAuthType(account) }}
</span>
</div>
<div
@@ -391,7 +391,9 @@
v-if="
account.platform === 'claude' ||
account.platform === 'claude-console' ||
account.platform === 'bedrock'
account.platform === 'bedrock' ||
account.platform === 'gemini' ||
account.platform === 'openai'
"
class="flex items-center gap-2"
>
@@ -491,21 +493,6 @@
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm font-medium">
<div class="flex flex-wrap items-center gap-1">
<button
v-if="account.platform === 'claude' && account.scopes"
:class="[
'rounded px-2.5 py-1 text-xs font-medium transition-colors',
account.isRefreshing
? 'cursor-not-allowed bg-gray-100 text-gray-400'
: 'bg-blue-100 text-blue-700 hover:bg-blue-200'
]"
:disabled="account.isRefreshing"
:title="account.isRefreshing ? '刷新中...' : '刷新Token'"
@click="refreshToken(account)"
>
<i :class="['fas fa-sync-alt', account.isRefreshing ? 'animate-spin' : '']" />
<span class="ml-1">刷新</span>
</button>
<button
v-if="
account.platform === 'claude' &&
@@ -709,23 +696,13 @@
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500">优先级</span>
<span class="font-medium text-gray-700">
{{ account.priority || 0 }}
{{ account.priority || 50 }}
</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="mt-3 flex gap-2 border-t border-gray-100 pt-3">
<button
v-if="account.platform === 'claude' && account.type === 'oauth'"
class="flex flex-1 items-center justify-center gap-1 rounded-lg bg-blue-50 px-3 py-2 text-xs text-blue-600 transition-colors hover:bg-blue-100"
:disabled="refreshingTokens[account.id]"
@click="refreshAccountToken(account)"
>
<i :class="['fas fa-sync-alt', { 'animate-spin': refreshingTokens[account.id] }]" />
{{ refreshingTokens[account.id] ? '刷新中' : '刷新' }}
</button>
<button
class="flex flex-1 items-center justify-center gap-1 rounded-lg px-3 py-2 text-xs transition-colors"
:class="
@@ -806,7 +783,6 @@ const accountSortBy = ref('name')
const accountsSortBy = ref('')
const accountsSortOrder = ref('asc')
const apiKeys = ref([])
const refreshingTokens = ref({})
const accountGroups = ref([])
const groupFilter = ref('all')
const platformFilter = ref('all')
@@ -830,7 +806,7 @@ const platformOptions = ref([
{ value: 'all', label: '所有平台', icon: 'fa-globe' },
{ value: 'claude', label: 'Claude', icon: 'fa-brain' },
{ value: 'claude-console', label: 'Claude Console', icon: 'fa-terminal' },
{ value: 'gemini', label: 'Gemini', icon: 'fa-robot' },
{ value: 'gemini', label: 'Gemini', icon: 'fa-google' },
{ value: 'openai', label: 'OpenAi', icon: 'fa-openai' },
{ value: 'bedrock', label: 'Bedrock', icon: 'fab fa-aws' }
])
@@ -1275,26 +1251,6 @@ const deleteAccount = async (account) => {
}
}
// 刷新Token
const refreshToken = async (account) => {
if (account.isRefreshing) return
try {
account.isRefreshing = true
const data = await apiClient.post(`/admin/claude-accounts/${account.id}/refresh`)
if (data.success) {
showToast('Token刷新成功', 'success')
loadAccounts()
} else {
showToast(data.message || 'Token刷新失败', 'error')
}
} catch (error) {
showToast('Token刷新失败', 'error')
} finally {
account.isRefreshing = false
}
}
// 重置账户状态
const resetAccountStatus = async (account) => {
@@ -1387,6 +1343,27 @@ const handleEditSuccess = () => {
loadAccounts()
}
// 获取 Claude 账号的添加方式
const getClaudeAuthType = (account) => {
// 基于 lastRefreshAt 判断:如果为空说明是 Setup Token不能刷新否则是 OAuth
if (!account.lastRefreshAt || account.lastRefreshAt === '') {
return 'Setup' // 缩短显示文本
}
return 'OAuth'
}
// 获取 Gemini 账号的添加方式
const getGeminiAuthType = () => {
// Gemini 统一显示 OAuth
return 'OAuth'
}
// 获取 OpenAI 账号的添加方式
const getOpenAIAuthType = () => {
// OpenAI 统一显示 OAuth
return 'OAuth'
}
// 获取 Claude 账号类型显示
const getClaudeAccountType = (account) => {
// 如果有订阅信息
@@ -1511,26 +1488,6 @@ const formatRelativeTime = (dateString) => {
return formatLastUsed(dateString)
}
// 刷新账户Token
const refreshAccountToken = async (account) => {
if (refreshingTokens.value[account.id]) return
try {
refreshingTokens.value[account.id] = true
const data = await apiClient.post(`/admin/claude-accounts/${account.id}/refresh`)
if (data.success) {
showToast('Token刷新成功', 'success')
loadAccounts()
} else {
showToast(data.message || 'Token刷新失败', 'error')
}
} catch (error) {
showToast('Token刷新失败', 'error')
} finally {
refreshingTokens.value[account.id] = false
}
}
// 切换调度状态
// const toggleDispatch = async (account) => {