chore: commit all changes

This commit is contained in:
千羽
2025-08-10 17:46:31 +09:00
parent f07efecb10
commit 11fc856999
9 changed files with 728 additions and 13 deletions

View File

@@ -817,6 +817,30 @@ class RedisClient {
const key = `claude:account:${accountId}` const key = `claude:account:${accountId}`
return await this.client.del(key) return await this.client.del(key)
} }
async setOpenAiAccount(accountId, accountData) {
const key = `openai:account:${accountId}`
await this.client.hset(key, accountData)
}
async getOpenAiAccount(accountId) {
const key = `openai:account:${accountId}`
return await this.client.hgetall(key)
}
async deleteOpenAiAccount(accountId) {
const key = `openai:account:${accountId}`
return await this.client.del(key)
}
async getAllOpenAIAccounts() {
const keys = await this.client.keys('openai:account:*')
const accounts = []
for (const key of keys) {
const accountData = await this.client.hgetall(key)
if (accountData && Object.keys(accountData).length > 0) {
accounts.push({ id: key.replace('claude:account:', ''), ...accountData })
}
}
return accounts
}
// 🔐 会话管理(用于管理员登录等) // 🔐 会话管理(用于管理员登录等)
async setSession(sessionId, sessionData, ttl = 86400) { async setSession(sessionId, sessionData, ttl = 86400) {

View File

@@ -13,9 +13,11 @@ const CostCalculator = require('../utils/costCalculator')
const pricingService = require('../services/pricingService') const pricingService = require('../services/pricingService')
const claudeCodeHeadersService = require('../services/claudeCodeHeadersService') const claudeCodeHeadersService = require('../services/claudeCodeHeadersService')
const axios = require('axios') const axios = require('axios')
const crypto = require('crypto')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const config = require('../../config/config') const config = require('../../config/config')
const { v4: uuidv4 } = require('uuid')
const router = express.Router() const router = express.Router()
@@ -4298,4 +4300,399 @@ router.put('/oem-settings', authenticateAdmin, async (req, res) => {
} }
}) })
// 🤖 OpenAI 账户管理
// OpenAI OAuth 配置
const OPENAI_CONFIG = {
BASE_URL: 'https://auth.openai.com',
CLIENT_ID: 'app_EMoamEEZ73f0CkXaXp7hrann',
REDIRECT_URI: 'http://localhost:1455/auth/callback',
SCOPE: 'openid profile email offline_access'
}
// 生成 PKCE 参数
function generateOpenAIPKCE() {
const codeVerifier = crypto.randomBytes(64).toString('hex')
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url')
return {
codeVerifier,
codeChallenge
}
}
// 生成 OpenAI OAuth 授权 URL
router.post('/openai-accounts/generate-auth-url', authenticateAdmin, async (req, res) => {
try {
const { proxy } = req.body
// 生成 PKCE 参数
const pkce = generateOpenAIPKCE()
// 生成随机 state
const state = crypto.randomBytes(32).toString('hex')
// 创建会话 ID
const sessionId = crypto.randomUUID()
// 将 PKCE 参数和代理配置存储到 Redis
await redis.setOAuthSession(sessionId, {
codeVerifier: pkce.codeVerifier,
codeChallenge: pkce.codeChallenge,
state,
proxy: proxy || null,
platform: 'openai',
createdAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString()
})
// 构建授权 URL 参数
const params = new URLSearchParams({
response_type: 'code',
client_id: OPENAI_CONFIG.CLIENT_ID,
redirect_uri: OPENAI_CONFIG.REDIRECT_URI,
scope: OPENAI_CONFIG.SCOPE,
code_challenge: pkce.codeChallenge,
code_challenge_method: 'S256',
state,
id_token_add_organizations: 'true',
codex_cli_simplified_flow: 'true'
})
const authUrl = `${OPENAI_CONFIG.BASE_URL}/oauth/authorize?${params.toString()}`
logger.success('🔗 Generated OpenAI OAuth authorization URL')
return res.json({
success: true,
data: {
authUrl,
sessionId,
instructions: [
'1. 复制上面的链接到浏览器中打开',
'2. 登录您的 OpenAI 账户',
'3. 同意应用权限',
'4. 复制浏览器地址栏中的完整 URL包含 code 参数)',
'5. 在添加账户表单中粘贴完整的回调 URL'
]
}
})
} catch (error) {
logger.error('生成 OpenAI OAuth URL 失败:', error)
return res.status(500).json({
success: false,
message: '生成授权链接失败',
error: error.message
})
}
})
// 交换 OpenAI 授权码
router.post('/openai-accounts/exchange-code', authenticateAdmin, async (req, res) => {
try {
const { code, sessionId } = req.body
if (!code || !sessionId) {
return res.status(400).json({
success: false,
message: '缺少必要参数'
})
}
// 从 Redis 获取会话数据
const sessionData = await redis.getOAuthSession(sessionId)
if (!sessionData) {
return res.status(400).json({
success: false,
message: '会话已过期或无效'
})
}
// 准备 token 交换请求
const tokenData = {
grant_type: 'authorization_code',
code: code.trim(),
redirect_uri: OPENAI_CONFIG.REDIRECT_URI,
client_id: OPENAI_CONFIG.CLIENT_ID,
code_verifier: sessionData.codeVerifier
}
logger.info('Exchanging OpenAI authorization code:', {
sessionId,
codeLength: code.length,
hasCodeVerifier: !!sessionData.codeVerifier
})
// 配置代理(如果有)
const axiosConfig = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
if (sessionData.proxy) {
const { type, host, port, username, password } = sessionData.proxy
if (type === 'http' || type === 'https') {
axiosConfig.proxy = {
host,
port: parseInt(port),
auth: username && password ? { username, password } : undefined
}
}
}
// 交换 authorization code 获取 tokens
const tokenResponse = await axios.post(
`${OPENAI_CONFIG.BASE_URL}/oauth/token`,
new URLSearchParams(tokenData).toString(),
axiosConfig
)
const { id_token, access_token, refresh_token, expires_in } = tokenResponse.data
// 解析 ID token 获取用户信息
const idTokenParts = id_token.split('.')
if (idTokenParts.length !== 3) {
throw new Error('Invalid ID token format')
}
// 解码 JWT payload
const payload = JSON.parse(Buffer.from(idTokenParts[1], 'base64url').toString())
// 获取 OpenAI 特定的声明
const authClaims = payload['https://api.openai.com/auth'] || {}
const accountId = authClaims.chatgpt_account_id || ''
const chatgptUserId = authClaims.chatgpt_user_id || authClaims.user_id || ''
const planType = authClaims.chatgpt_plan_type || ''
// 获取组织信息
const organizations = authClaims.organizations || []
const defaultOrg = organizations.find((org) => org.is_default) || organizations[0] || {}
const organizationId = defaultOrg.id || ''
const organizationRole = defaultOrg.role || ''
const organizationTitle = defaultOrg.title || ''
// 清理 Redis 会话
await redis.deleteOAuthSession(sessionId)
logger.success('✅ OpenAI OAuth token exchange successful')
return res.json({
success: true,
data: {
tokens: {
idToken: id_token,
accessToken: access_token,
refreshToken: refresh_token,
expires_in
},
accountInfo: {
accountId,
chatgptUserId,
organizationId,
organizationRole,
organizationTitle,
planType,
email: payload.email || '',
name: payload.name || '',
emailVerified: payload.email_verified || false,
organizations
}
}
})
} catch (error) {
logger.error('OpenAI OAuth token exchange failed:', error)
return res.status(500).json({
success: false,
message: '交换授权码失败',
error: error.message
})
}
})
// 获取所有 OpenAI 账户
router.get('/openai-accounts', authenticateAdmin, async (req, res) => {
try {
const accounts = await redis.getAllOpenAIAccounts()
logger.info(`获取 OpenAI 账户列表: ${accounts.length} 个账户`)
return res.json({
success: true,
data: accounts
})
} catch (error) {
logger.error('获取 OpenAI 账户列表失败:', error)
return res.status(500).json({
success: false,
message: '获取账户列表失败',
error: error.message
})
}
})
// 创建 OpenAI 账户
router.post('/openai-accounts', authenticateAdmin, async (req, res) => {
try {
const {
name,
description,
openaiOauth,
accountInfo,
proxy,
accountType,
groupId,
dedicatedApiKeys,
rateLimitDuration,
priority
} = req.body
if (!name) {
return res.status(400).json({
success: false,
message: '账户名称不能为空'
})
}
const id = uuidv4()
// 创建账户数据
const accountData = {
id,
name,
description: description || '',
platform: 'openai',
accountType: accountType || 'shared',
groupId: groupId || null,
dedicatedApiKeys: dedicatedApiKeys || [],
priority: priority || 50,
rateLimitDuration: rateLimitDuration || 60,
enabled: true,
idToken: claudeAccountService._encryptSensitiveData(openaiOauth.idToken),
accessToken: claudeAccountService._encryptSensitiveData(openaiOauth.accessToken),
refreshToken: claudeAccountService._encryptSensitiveData(openaiOauth.refreshToken),
accountId: accountInfo?.accountId || '',
expiresAt: (Math.floor(Date.now() / 1000) + openaiOauth.expires_in) * 1000,
chatgptUserId: accountInfo?.chatgptUserId || '',
organizationId: accountInfo?.organizationId || '',
organizationRole: accountInfo?.organizationRole || '',
organizationTitle: accountInfo?.organizationTitle || '',
planType: accountInfo?.planType || '',
email: claudeAccountService._encryptSensitiveData(accountInfo?.email || ''),
emailVerified: accountInfo?.emailVerified || false,
isActive: true,
status: 'active',
lastRefresh: new Date().toISOString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
// 存储代理配置(如果提供)
if (proxy?.enabled) {
accountData.proxy = {
type: proxy.type,
host: proxy.host,
port: proxy.port,
username: proxy.username || null,
password: proxy.password || null
}
}
// 保存到 Redis
const accountId = await redis.setOpenAiAccount(id, accountData)
logger.success(`✅ 创建 OpenAI 账户成功: ${name} (ID: ${accountId})`)
return res.json({
success: true,
data: {
id: accountId,
...accountData
}
})
} catch (error) {
logger.error('创建 OpenAI 账户失败:', error)
return res.status(500).json({
success: false,
message: '创建账户失败',
error: error.message
})
}
})
// 更新 OpenAI 账户
router.put('/openai-accounts/:id', authenticateAdmin, async (req, res) =>
//TODO:
res.json({
success: true
})
)
// 删除 OpenAI 账户
router.delete('/openai-accounts/:id', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
const account = await redis.getOpenAiAccount(id)
if (!account) {
return res.status(404).json({
success: false,
message: '账户不存在'
})
}
await redis.deleteOpenAiAccount(id)
logger.success(`✅ 删除 OpenAI 账户成功: ${account.name} (ID: ${id})`)
return res.json({
success: true,
message: '账户删除成功'
})
} catch (error) {
logger.error('删除 OpenAI 账户失败:', error)
return res.status(500).json({
success: false,
message: '删除账户失败',
error: error.message
})
}
})
// 切换 OpenAI 账户状态
router.put('/openai-accounts/:id/toggle', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
const account = await redis.getOpenAiAccount(id)
if (!account) {
return res.status(404).json({
success: false,
message: '账户不存在'
})
}
// 切换启用状态
account.enabled = !account.enabled
account.updatedAt = new Date().toISOString()
// TODO: 更新方法
// await redis.updateOpenAiAccount(id, account)
logger.success(
`${account.enabled ? '启用' : '禁用'} OpenAI 账户: ${account.name} (ID: ${id})`
)
return res.json({
success: true,
data: account
})
} catch (error) {
logger.error('切换 OpenAI 账户状态失败:', error)
return res.status(500).json({
success: false,
message: '切换账户状态失败',
error: error.message
})
}
})
module.exports = router module.exports = router

View File

@@ -319,7 +319,6 @@ async function handleLoadCodeAssist(req, res) {
requestedModel requestedModel
) )
const { accessToken, refreshToken } = await geminiAccountService.getAccount(accountId) const { accessToken, refreshToken } = await geminiAccountService.getAccount(accountId)
logger.info(`accessToken: ${accessToken}`)
const { metadata, cloudaicompanionProject } = req.body const { metadata, cloudaicompanionProject } = req.body

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -19,8 +20,10 @@
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net"> <link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com"> <link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

View File

@@ -481,3 +481,10 @@
0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05); 0 4px 6px -2px rgba(0, 0, 0, 0.05);
} }
.fa-openai {
width: 16px;
height: 16px;
background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgNjQwIj48IS0tIUZvbnQgQXdlc29tZSBGcmVlIHY3LjAuMCBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZS9mcmVlIENvcHlyaWdodCAyMDI1IEZvbnRpY29ucywgSW5jLi0tPjxwYXRoIGQ9Ik0yNjAuNCAyNDkuOHYtNDguNmMwLTQuMSAxLjUtNy4yIDUuMS05LjJsOTcuOC01Ni4zYzEzLjMtNy43IDI5LjItMTEuMyA0NS42LTExLjMgNjEuNCAwIDEwMC40IDQ3LjYgMTAwLjQgOTguMyAwIDMuNiAwIDcuNy0uNSAxMS44bC0xMDEuNS01OS40Yy02LjEtMy42LTEyLjMtMy42LTE4LjQgMGwtMTI4LjUgNzQuN3ptMjI4LjMgMTg5LjRWMzIzYzAtNy4yLTMuMS0xMi4zLTkuMi0xNS45TDM1MSAyMzIuNGw0Mi0yNC4xYzMuNi0yIDYuNy0yIDEwLjIgMGw5Ny44IDU2LjRjMjguMiAxNi40IDQ3LjEgNTEuMiA0Ny4xIDg1IDAgMzguOS0yMyA3NC44LTU5LjQgODkuNnpNMjMwLjIgMzM2LjhsLTQyLTI0LjZjLTMuNi0yLTUuMS01LjEtNS4xLTkuMlYxOTAuNGMwLTU0LjggNDItOTYuMyA5OC44LTk2LjMgMjEuNSAwIDQxLjUgNy4yIDU4LjQgMjBsLTEwMC45IDU4LjRjLTYuMSAzLjYtOS4yIDguNy05LjIgMTUuOXYxNDguNXptOTAuNCA1Mi4ybC02MC4yLTMzLjh2LTcxLjdsNjAuMi0zMy44IDYwLjIgMzMuOHY3MS43TDMyMC42IDM4OXptMzguNyAxNTUuN2MtMjEuNSAwLTQxLjUtNy4yLTU4LjQtMjBsMTAwLjktNTguNGM2LjEtMy42IDkuMi04LjcgOS4yLTE1LjlWMzAxLjlsNDIuNSAyNC42YzMuNiAyIDUuMSA1LjEgNS4xIDkuMnYxMTIuNmMwIDU0LjgtNDIuNSA5Ni4zLTk5LjMgOTYuM3pNMjM3LjggNDMwLjVsLTk3LjctNTYuM0MxMTEuOSAzNTcuOCA5MyAzMjMgOTMgMjg5LjJjMC0zOS40IDIzLjYtNzQuOCA1OS45LTg5LjZ2MTE2LjdjMCA3LjIgMy4xIDEyLjMgOS4yIDE1LjlsMTI4IDc0LjItNDIgMjQuMWMtMy42IDItNi43IDItMTAuMiAwem0tNS42IDg0Yy01Ny45IDAtMTAwLjQtNDMuNS0xMDAuNC05Ny4zIDAtNC4xLjUtOC4yIDEtMTIuM2wxMDAuOSA1OC40YzYuMSAzLjYgMTIuMyAzLjYgMTguNCAwbDEyOC41LTc0LjJ2NDguNmMwIDQuMS0xLjUgNy4yLTUuMSA5LjJsLTk3LjggNTYuM2MtMTMuMyA3LjctMjkuMiAxMS4zLTQ1LjYgMTEuM3ptMTI3IDYwLjljNjIgMCAxMTMuNy00NCAxMjUuNC0xMDIuNCA1Ny4zLTE0LjkgOTQuMi02OC42IDk0LjItMTIzLjQgMC0zNS44LTE1LjQtNzAuNy00My05NS43IDIuNi0xMC44IDQuMS0yMS41IDQuMS0zMi4zIDAtNzMuMi01OS40LTEyOC0xMjgtMTI4LTEzLjggMC0yNy4xIDItNDAuNCA2LjctMjMtMjIuNS01NC44LTM2LjktODkuNi0zNi45LTYyIDAtMTEzLjcgNDQtMTI1LjQgMTAyLjQtNTcuMyAxNC44LTk0LjIgNjguNi05NC4yIDEyMy40IDAgMzUuOCAxNS40IDcwLjcgNDMgOTUuNy0yLjYgMTAuOC00LjEgMjEuNS00LjEgMzIuMyAwIDczLjIgNTkuNCAxMjggMTI4IDEyOCAxMy44IDAgMjcuMS0yIDQwLjQtNi43IDIzIDIyLjUgNTQuOCAzNi45IDg5LjYgMzYuOXoiLz48L3N2Zz4=)
no-repeat center/100%;
}

View File

@@ -77,6 +77,10 @@
<input v-model="form.platform" class="mr-2" type="radio" value="gemini" /> <input v-model="form.platform" class="mr-2" type="radio" value="gemini" />
<span class="text-sm text-gray-700">Gemini</span> <span class="text-sm text-gray-700">Gemini</span>
</label> </label>
<label class="flex cursor-pointer items-center">
<input v-model="form.platform" class="mr-2" type="radio" value="openai" />
<span class="text-sm text-gray-700">OpenAI</span>
</label>
<label class="flex cursor-pointer items-center"> <label class="flex cursor-pointer items-center">
<input v-model="form.platform" class="mr-2" type="radio" value="bedrock" /> <input v-model="form.platform" class="mr-2" type="radio" value="bedrock" />
<span class="text-sm text-gray-700">Bedrock</span> <span class="text-sm text-gray-700">Bedrock</span>
@@ -568,6 +572,10 @@
请输入有效的 Gemini Access Token。如果您有 Refresh 请输入有效的 Gemini Access Token。如果您有 Refresh
Token建议也一并填写以支持自动刷新。 Token建议也一并填写以支持自动刷新。
</p> </p>
<p v-else-if="form.platform === 'openai'" class="mb-2 text-sm text-blue-800">
请输入有效的 OpenAI Access Token。如果您有 Refresh
Token建议也一并填写以支持自动刷新。
</p>
<div class="mb-2 mt-2 rounded-lg border border-blue-300 bg-white/80 p-3"> <div class="mb-2 mt-2 rounded-lg border border-blue-300 bg-white/80 p-3">
<p class="mb-1 text-sm font-medium text-blue-900"> <p class="mb-1 text-sm font-medium text-blue-900">
<i class="fas fa-folder-open mr-1" /> <i class="fas fa-folder-open mr-1" />
@@ -587,6 +595,10 @@
> >
文件中的凭证。 文件中的凭证。
</p> </p>
<p v-else-if="form.platform === 'openai'" class="text-xs text-blue-800">
请从已登录 OpenAI 账户的机器上获取认证凭证, 或通过 OAuth 授权流程获取 Access
Token。
</p>
</div> </div>
<p class="text-xs text-blue-600"> <p class="text-xs text-blue-600">
💡 如果未填写 Refresh TokenToken 过期后需要手动更新。 💡 如果未填写 Refresh TokenToken 过期后需要手动更新。
@@ -1580,11 +1592,16 @@ const handleOAuthSuccess = async (tokenInfo) => {
if (form.value.projectId) { if (form.value.projectId) {
data.projectId = form.value.projectId data.projectId = form.value.projectId
} }
} else if (form.value.platform === 'openai') {
data.openaiOauth = tokenInfo.tokens || tokenInfo
data.accountInfo = tokenInfo.accountInfo
} }
let result let result
if (form.value.platform === 'claude') { if (form.value.platform === 'claude') {
result = await accountsStore.createClaudeAccount(data) result = await accountsStore.createClaudeAccount(data)
} else if (form.value.platform === 'openai') {
result = await accountsStore.createOpenAIAccount(data)
} else { } else {
result = await accountsStore.createGeminiAccount(data) result = await accountsStore.createGeminiAccount(data)
} }
@@ -1734,6 +1751,8 @@ const createAccount = async () => {
result = await accountsStore.createClaudeConsoleAccount(data) result = await accountsStore.createClaudeConsoleAccount(data)
} else if (form.value.platform === 'bedrock') { } else if (form.value.platform === 'bedrock') {
result = await accountsStore.createBedrockAccount(data) result = await accountsStore.createBedrockAccount(data)
} else if (form.value.platform === 'openai') {
result = await accountsStore.createOpenAIAccount(data)
} else { } else {
result = await accountsStore.createGeminiAccount(data) result = await accountsStore.createGeminiAccount(data)
} }
@@ -1882,6 +1901,8 @@ const updateAccount = async () => {
await accountsStore.updateClaudeConsoleAccount(props.account.id, data) await accountsStore.updateClaudeConsoleAccount(props.account.id, data)
} else if (props.account.platform === 'bedrock') { } else if (props.account.platform === 'bedrock') {
await accountsStore.updateBedrockAccount(props.account.id, data) await accountsStore.updateBedrockAccount(props.account.id, data)
} else if (props.account.platform === 'openai') {
await accountsStore.updateOpenAIAccount(props.account.id, data)
} else { } else {
await accountsStore.updateGeminiAccount(props.account.id, data) await accountsStore.updateGeminiAccount(props.account.id, data)
} }

View File

@@ -251,6 +251,131 @@
</div> </div>
</div> </div>
<!-- OpenAI OAuth流程 -->
<div v-else-if="platform === 'openai'">
<div class="rounded-lg border border-orange-200 bg-orange-50 p-6">
<div class="flex items-start gap-4">
<div
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-orange-500"
>
<i class="fas fa-brain text-white" />
</div>
<div class="flex-1">
<h4 class="mb-3 font-semibold text-orange-900">OpenAI 账户授权</h4>
<p class="mb-4 text-sm text-orange-800">请按照以下步骤完成 OpenAI 账户的授权</p>
<div class="space-y-4">
<!-- 步骤1: 生成授权链接 -->
<div class="rounded-lg border border-orange-300 bg-white/80 p-4">
<div class="flex items-start gap-3">
<div
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-orange-600 text-xs font-bold text-white"
>
1
</div>
<div class="flex-1">
<p class="mb-2 font-medium text-orange-900">点击下方按钮生成授权链接</p>
<button
v-if="!authUrl"
class="btn btn-primary px-4 py-2 text-sm"
:disabled="loading"
@click="generateAuthUrl"
>
<i v-if="!loading" class="fas fa-link mr-2" />
<div v-else class="loading-spinner mr-2" />
{{ loading ? '生成中...' : '生成授权链接' }}
</button>
<div v-else class="space-y-3">
<div class="flex items-center gap-2">
<input
class="form-input flex-1 bg-gray-50 font-mono text-xs"
readonly
type="text"
:value="authUrl"
/>
<button
class="rounded-lg bg-gray-100 px-3 py-2 transition-colors hover:bg-gray-200"
title="复制链接"
@click="copyAuthUrl"
>
<i :class="copied ? 'fas fa-check text-green-500' : 'fas fa-copy'" />
</button>
</div>
<button
class="text-xs text-orange-600 hover:text-orange-700"
@click="regenerateAuthUrl"
>
<i class="fas fa-sync-alt mr-1" />重新生成
</button>
</div>
</div>
</div>
</div>
<!-- 步骤2: 访问链接并授权 -->
<div class="rounded-lg border border-orange-300 bg-white/80 p-4">
<div class="flex items-start gap-3">
<div
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-orange-600 text-xs font-bold text-white"
>
2
</div>
<div class="flex-1">
<p class="mb-2 font-medium text-orange-900">在浏览器中打开链接并完成授权</p>
<p class="mb-2 text-sm text-orange-700">
请在新标签页中打开授权链接登录您的 OpenAI 账户并授权
</p>
<div class="rounded border border-yellow-300 bg-yellow-50 p-3">
<p class="text-xs text-yellow-800">
<i class="fas fa-exclamation-triangle mr-1" />
<strong>注意</strong
>如果您设置了代理请确保浏览器也使用相同的代理访问授权页面
</p>
</div>
</div>
</div>
</div>
<!-- 步骤3: 输入授权码 -->
<div class="rounded-lg border border-orange-300 bg-white/80 p-4">
<div class="flex items-start gap-3">
<div
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-orange-600 text-xs font-bold text-white"
>
3
</div>
<div class="flex-1">
<p class="mb-2 font-medium text-orange-900">输入 Authorization Code</p>
<p class="mb-3 text-sm text-orange-700">
授权完成后页面会显示一个
<strong>Authorization Code</strong>请将其复制并粘贴到下方输入框
</p>
<div class="space-y-3">
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700">
<i class="fas fa-key mr-2 text-orange-500" />Authorization Code
</label>
<textarea
v-model="authCode"
class="form-input w-full resize-none font-mono text-sm"
placeholder="粘贴从OpenAI页面获取的Authorization Code..."
rows="3"
/>
</div>
<p class="mt-2 text-xs text-gray-500">
<i class="fas fa-info-circle mr-1" />
请粘贴从OpenAI页面复制的Authorization Code
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex gap-3 pt-4"> <div class="flex gap-3 pt-4">
<button <button
class="flex-1 rounded-xl bg-gray-100 px-6 py-3 font-semibold text-gray-700 transition-colors hover:bg-gray-200" class="flex-1 rounded-xl bg-gray-100 px-6 py-3 font-semibold text-gray-700 transition-colors hover:bg-gray-200"
@@ -339,8 +464,8 @@ watch(authCode, (newValue) => {
console.error('Failed to parse URL:', error) console.error('Failed to parse URL:', error)
showToast('链接格式错误,请检查是否为完整的 URL', 'error') showToast('链接格式错误,请检查是否为完整的 URL', 'error')
} }
} else if (props.platform === 'gemini') { } else if (props.platform === 'gemini' || props.platform === 'openai') {
// Gemini 平台可能使用不同的回调URL // Gemini 和 OpenAI 平台可能使用不同的回调URL
// 尝试从任何URL中提取code参数 // 尝试从任何URL中提取code参数
try { try {
const url = new URL(trimmedValue) const url = new URL(trimmedValue)
@@ -385,6 +510,10 @@ const generateAuthUrl = async () => {
const result = await accountsStore.generateGeminiAuthUrl(proxyConfig) const result = await accountsStore.generateGeminiAuthUrl(proxyConfig)
authUrl.value = result.authUrl authUrl.value = result.authUrl
sessionId.value = result.sessionId sessionId.value = result.sessionId
} else if (props.platform === 'openai') {
const result = await accountsStore.generateOpenAIAuthUrl(proxyConfig)
authUrl.value = result.authUrl
sessionId.value = result.sessionId
} }
} catch (error) { } catch (error) {
showToast(error.message || '生成授权链接失败', 'error') showToast(error.message || '生成授权链接失败', 'error')
@@ -445,6 +574,12 @@ const exchangeCode = async () => {
code: authCode.value.trim(), code: authCode.value.trim(),
sessionId: sessionId.value sessionId: sessionId.value
} }
} else if (props.platform === 'openai') {
// OpenAI使用code和sessionId
data = {
code: authCode.value.trim(),
sessionId: sessionId.value
}
} }
// 添加代理配置(如果启用) // 添加代理配置(如果启用)
@@ -463,6 +598,8 @@ const exchangeCode = async () => {
tokenInfo = await accountsStore.exchangeClaudeCode(data) tokenInfo = await accountsStore.exchangeClaudeCode(data)
} else if (props.platform === 'gemini') { } else if (props.platform === 'gemini') {
tokenInfo = await accountsStore.exchangeGeminiCode(data) tokenInfo = await accountsStore.exchangeGeminiCode(data)
} else if (props.platform === 'openai') {
tokenInfo = await accountsStore.exchangeOpenAICode(data)
} }
emit('success', tokenInfo) emit('success', tokenInfo)

View File

@@ -8,6 +8,7 @@ export const useAccountsStore = defineStore('accounts', () => {
const claudeConsoleAccounts = ref([]) const claudeConsoleAccounts = ref([])
const bedrockAccounts = ref([]) const bedrockAccounts = ref([])
const geminiAccounts = ref([]) const geminiAccounts = ref([])
const openaiAccounts = ref([])
const loading = ref(false) const loading = ref(false)
const error = ref(null) const error = ref(null)
const sortBy = ref('') const sortBy = ref('')
@@ -91,6 +92,25 @@ export const useAccountsStore = defineStore('accounts', () => {
} }
} }
// 获取OpenAI账户列表
const fetchOpenAIAccounts = async () => {
loading.value = true
error.value = null
try {
const response = await apiClient.get('/admin/openai-accounts')
if (response.success) {
openaiAccounts.value = response.data || []
} else {
throw new Error(response.message || '获取OpenAI账户失败')
}
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
// 获取所有账户 // 获取所有账户
const fetchAllAccounts = async () => { const fetchAllAccounts = async () => {
loading.value = true loading.value = true
@@ -100,7 +120,8 @@ export const useAccountsStore = defineStore('accounts', () => {
fetchClaudeAccounts(), fetchClaudeAccounts(),
fetchClaudeConsoleAccounts(), fetchClaudeConsoleAccounts(),
fetchBedrockAccounts(), fetchBedrockAccounts(),
fetchGeminiAccounts() fetchGeminiAccounts(),
fetchOpenAIAccounts()
]) ])
} catch (err) { } catch (err) {
error.value = err.message error.value = err.message
@@ -190,6 +211,26 @@ export const useAccountsStore = defineStore('accounts', () => {
} }
} }
// 创建OpenAI账户
const createOpenAIAccount = async (data) => {
loading.value = true
error.value = null
try {
const response = await apiClient.post('/admin/openai-accounts', data)
if (response.success) {
await fetchOpenAIAccounts()
return response.data
} else {
throw new Error(response.message || '创建OpenAI账户失败')
}
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
// 更新Claude账户 // 更新Claude账户
const updateClaudeAccount = async (id, data) => { const updateClaudeAccount = async (id, data) => {
loading.value = true loading.value = true
@@ -270,6 +311,26 @@ export const useAccountsStore = defineStore('accounts', () => {
} }
} }
// 更新OpenAI账户
const updateOpenAIAccount = async (id, data) => {
loading.value = true
error.value = null
try {
const response = await apiClient.put(`/admin/openai-accounts/${id}`, data)
if (response.success) {
await fetchOpenAIAccounts()
return response
} else {
throw new Error(response.message || '更新OpenAI账户失败')
}
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
// 切换账户状态 // 切换账户状态
const toggleAccount = async (platform, id) => { const toggleAccount = async (platform, id) => {
loading.value = true loading.value = true
@@ -282,8 +343,10 @@ export const useAccountsStore = defineStore('accounts', () => {
endpoint = `/admin/claude-console-accounts/${id}/toggle` endpoint = `/admin/claude-console-accounts/${id}/toggle`
} else if (platform === 'bedrock') { } else if (platform === 'bedrock') {
endpoint = `/admin/bedrock-accounts/${id}/toggle` endpoint = `/admin/bedrock-accounts/${id}/toggle`
} else { } else if (platform === 'gemini') {
endpoint = `/admin/gemini-accounts/${id}/toggle` endpoint = `/admin/gemini-accounts/${id}/toggle`
} else {
endpoint = `/admin/openai-accounts/${id}/toggle`
} }
const response = await apiClient.put(endpoint) const response = await apiClient.put(endpoint)
@@ -294,8 +357,10 @@ export const useAccountsStore = defineStore('accounts', () => {
await fetchClaudeConsoleAccounts() await fetchClaudeConsoleAccounts()
} else if (platform === 'bedrock') { } else if (platform === 'bedrock') {
await fetchBedrockAccounts() await fetchBedrockAccounts()
} else { } else if (platform === 'gemini') {
await fetchGeminiAccounts() await fetchGeminiAccounts()
} else {
await fetchOpenAIAccounts()
} }
return response return response
} else { } else {
@@ -321,8 +386,10 @@ export const useAccountsStore = defineStore('accounts', () => {
endpoint = `/admin/claude-console-accounts/${id}` endpoint = `/admin/claude-console-accounts/${id}`
} else if (platform === 'bedrock') { } else if (platform === 'bedrock') {
endpoint = `/admin/bedrock-accounts/${id}` endpoint = `/admin/bedrock-accounts/${id}`
} else { } else if (platform === 'gemini') {
endpoint = `/admin/gemini-accounts/${id}` endpoint = `/admin/gemini-accounts/${id}`
} else {
endpoint = `/admin/openai-accounts/${id}`
} }
const response = await apiClient.delete(endpoint) const response = await apiClient.delete(endpoint)
@@ -333,8 +400,10 @@ export const useAccountsStore = defineStore('accounts', () => {
await fetchClaudeConsoleAccounts() await fetchClaudeConsoleAccounts()
} else if (platform === 'bedrock') { } else if (platform === 'bedrock') {
await fetchBedrockAccounts() await fetchBedrockAccounts()
} else { } else if (platform === 'gemini') {
await fetchGeminiAccounts() await fetchGeminiAccounts()
} else {
await fetchOpenAIAccounts()
} }
return response return response
} else { } else {
@@ -464,6 +533,36 @@ export const useAccountsStore = defineStore('accounts', () => {
} }
} }
// 生成OpenAI OAuth URL
const generateOpenAIAuthUrl = async (proxyConfig) => {
try {
const response = await apiClient.post('/admin/openai-accounts/generate-auth-url', proxyConfig)
if (response.success) {
return response.data // 返回整个对象包含authUrl和sessionId
} else {
throw new Error(response.message || '生成授权URL失败')
}
} catch (err) {
error.value = err.message
throw err
}
}
// 交换OpenAI OAuth Code
const exchangeOpenAICode = async (data) => {
try {
const response = await apiClient.post('/admin/openai-accounts/exchange-code', data)
if (response.success) {
return response.data
} else {
throw new Error(response.message || '交换授权码失败')
}
} catch (err) {
error.value = err.message
throw err
}
}
// 排序账户 // 排序账户
const sortAccounts = (field) => { const sortAccounts = (field) => {
if (sortBy.value === field) { if (sortBy.value === field) {
@@ -480,6 +579,7 @@ export const useAccountsStore = defineStore('accounts', () => {
claudeConsoleAccounts.value = [] claudeConsoleAccounts.value = []
bedrockAccounts.value = [] bedrockAccounts.value = []
geminiAccounts.value = [] geminiAccounts.value = []
openaiAccounts.value = []
loading.value = false loading.value = false
error.value = null error.value = null
sortBy.value = '' sortBy.value = ''
@@ -492,6 +592,7 @@ export const useAccountsStore = defineStore('accounts', () => {
claudeConsoleAccounts, claudeConsoleAccounts,
bedrockAccounts, bedrockAccounts,
geminiAccounts, geminiAccounts,
openaiAccounts,
loading, loading,
error, error,
sortBy, sortBy,
@@ -502,15 +603,18 @@ export const useAccountsStore = defineStore('accounts', () => {
fetchClaudeConsoleAccounts, fetchClaudeConsoleAccounts,
fetchBedrockAccounts, fetchBedrockAccounts,
fetchGeminiAccounts, fetchGeminiAccounts,
fetchOpenAIAccounts,
fetchAllAccounts, fetchAllAccounts,
createClaudeAccount, createClaudeAccount,
createClaudeConsoleAccount, createClaudeConsoleAccount,
createBedrockAccount, createBedrockAccount,
createGeminiAccount, createGeminiAccount,
createOpenAIAccount,
updateClaudeAccount, updateClaudeAccount,
updateClaudeConsoleAccount, updateClaudeConsoleAccount,
updateBedrockAccount, updateBedrockAccount,
updateGeminiAccount, updateGeminiAccount,
updateOpenAIAccount,
toggleAccount, toggleAccount,
deleteAccount, deleteAccount,
refreshClaudeToken, refreshClaudeToken,
@@ -520,6 +624,8 @@ export const useAccountsStore = defineStore('accounts', () => {
exchangeClaudeSetupTokenCode, exchangeClaudeSetupTokenCode,
generateGeminiAuthUrl, generateGeminiAuthUrl,
exchangeGeminiCode, exchangeGeminiCode,
generateOpenAIAuthUrl,
exchangeOpenAICode,
sortAccounts, sortAccounts,
reset reset
} }

View File

@@ -282,6 +282,15 @@
<span class="mx-1 h-4 w-px bg-orange-300" /> <span class="mx-1 h-4 w-px bg-orange-300" />
<span class="text-xs font-medium text-orange-700">AWS</span> <span class="text-xs font-medium text-orange-700">AWS</span>
</div> </div>
<div
v-else-if="account.platform === 'openai'"
class="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-100 bg-gradient-to-r from-gray-100 to-gray-100 px-2.5 py-1"
>
<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>
</div>
<div <div
v-else v-else
class="flex items-center gap-1.5 rounded-lg border border-indigo-200 bg-gradient-to-r from-indigo-100 to-blue-100 px-2.5 py-1" class="flex items-center gap-1.5 rounded-lg border border-indigo-200 bg-gradient-to-r from-indigo-100 to-blue-100 px-2.5 py-1"
@@ -813,6 +822,7 @@ const platformOptions = ref([
{ value: 'claude', label: 'Claude', icon: 'fa-brain' }, { value: 'claude', label: 'Claude', icon: 'fa-brain' },
{ value: 'claude-console', label: 'Claude Console', icon: 'fa-terminal' }, { value: 'claude-console', label: 'Claude Console', icon: 'fa-terminal' },
{ value: 'gemini', label: 'Gemini', icon: 'fa-robot' }, { value: 'gemini', label: 'Gemini', icon: 'fa-robot' },
{ value: 'openai', label: 'OpenAi', icon: 'fa-robot' },
{ value: 'bedrock', label: 'Bedrock', icon: 'fab fa-aws' } { value: 'bedrock', label: 'Bedrock', icon: 'fab fa-aws' }
]) ])
@@ -899,7 +909,8 @@ const loadAccounts = async (forceReload = false) => {
apiClient.get('/admin/claude-accounts', { params }), apiClient.get('/admin/claude-accounts', { params }),
apiClient.get('/admin/claude-console-accounts', { params }), apiClient.get('/admin/claude-console-accounts', { params }),
apiClient.get('/admin/bedrock-accounts', { params }), apiClient.get('/admin/bedrock-accounts', { params }),
apiClient.get('/admin/gemini-accounts', { params }) apiClient.get('/admin/gemini-accounts', { params }),
apiClient.get('/admin/openai-accounts', { params })
) )
} else { } else {
// 只请求指定平台其他平台设为null占位 // 只请求指定平台其他平台设为null占位
@@ -945,7 +956,8 @@ const loadAccounts = async (forceReload = false) => {
// 加载分组成员关系(需要在分组数据加载完成后) // 加载分组成员关系(需要在分组数据加载完成后)
await loadGroupMembers(forceReload) await loadGroupMembers(forceReload)
const [claudeData, claudeConsoleData, bedrockData, geminiData] = await Promise.all(requests) const [claudeData, claudeConsoleData, bedrockData, geminiData, openaiData] =
await Promise.all(requests)
const allAccounts = [] const allAccounts = []
@@ -991,6 +1003,13 @@ const loadAccounts = async (forceReload = false) => {
}) })
allAccounts.push(...geminiAccounts) allAccounts.push(...geminiAccounts)
} }
if (openaiData.success) {
const openaiAccounts = (openaiData.data || []).map((acc) => {
const groupInfo = accountGroupMap.value.get(acc.id) || null
return { ...acc, platform: 'openai', boundApiKeysCount: 0, groupInfo }
})
allAccounts.push(...openaiAccounts)
}
accounts.value = allAccounts accounts.value = allAccounts
} catch (error) { } catch (error) {
@@ -1214,6 +1233,8 @@ const deleteAccount = async (account) => {
endpoint = `/admin/claude-console-accounts/${account.id}` endpoint = `/admin/claude-console-accounts/${account.id}`
} else if (account.platform === 'bedrock') { } else if (account.platform === 'bedrock') {
endpoint = `/admin/bedrock-accounts/${account.id}` endpoint = `/admin/bedrock-accounts/${account.id}`
} else if (account.platform === 'openai') {
endpoint = `/admin/openai-accounts/${account.id}`
} else { } else {
endpoint = `/admin/gemini-accounts/${account.id}` endpoint = `/admin/gemini-accounts/${account.id}`
} }