feat: 实现OpenAI账户管理和统一调度系统

- 新增 OpenAI 账户管理服务,支持多账户轮询和负载均衡
- 实现统一的 OpenAI API 调度器,智能选择最优账户
- 优化成本计算器,支持更精确的 token 计算
- 更新模型定价数据,包含最新的 OpenAI 模型价格
- 增强 API Key 管理,支持更灵活的配额控制
- 改进管理界面,添加教程视图和账户分组管理
- 优化限流配置组件,提供更直观的用户体验

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-08-11 13:58:43 +08:00
parent f22a38d24a
commit f462684f97
22 changed files with 6163 additions and 3134 deletions

View File

@@ -606,6 +606,25 @@
</div>
</div>
<!-- OpenAI 平台需要 ID Token -->
<div v-if="form.platform === 'openai'">
<label class="mb-3 block text-sm font-semibold text-gray-700">ID Token *</label>
<textarea
v-model="form.idToken"
class="form-input w-full resize-none font-mono text-xs"
:class="{ 'border-red-500': errors.idToken }"
placeholder="请输入 ID Token (JWT 格式)..."
required
rows="4"
/>
<p v-if="errors.idToken" class="mt-1 text-xs text-red-500">
{{ errors.idToken }}
</p>
<p class="mt-2 text-xs text-gray-500">
ID Token 是 OpenAI OAuth 认证返回的 JWT token包含用户信息和组织信息
</p>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700">Access Token *</label>
<textarea
@@ -1332,6 +1351,7 @@ const form = ref({
accountType: props.account?.accountType || 'shared',
groupId: '',
projectId: props.account?.projectId || '',
idToken: '',
accessToken: '',
refreshToken: '',
proxy: initProxyConfig(),
@@ -1391,6 +1411,7 @@ const initModelMappings = () => {
// 表单验证错误
const errors = ref({
name: '',
idToken: '',
accessToken: '',
apiUrl: '',
apiKey: '',
@@ -1653,12 +1674,20 @@ const createAccount = async () => {
errors.value.region = '请选择 AWS 区域'
hasError = true
}
} else if (
form.value.addType === 'manual' &&
(!form.value.accessToken || form.value.accessToken.trim() === '')
) {
errors.value.accessToken = '请填写 Access Token'
hasError = true
} else if (form.value.addType === 'manual') {
// 手动模式验证
if (!form.value.accessToken || form.value.accessToken.trim() === '') {
errors.value.accessToken = '请填写 Access Token'
hasError = true
}
// OpenAI 平台需要验证 ID Token
if (
form.value.platform === 'openai' &&
(!form.value.idToken || form.value.idToken.trim() === '')
) {
errors.value.idToken = '请填写 ID Token'
hasError = true
}
}
// 分组类型验证
@@ -1722,6 +1751,57 @@ const createAccount = async () => {
if (form.value.projectId) {
data.projectId = form.value.projectId
}
} else if (form.value.platform === 'openai') {
// OpenAI手动模式需要构建openaiOauth对象
const expiresInMs = form.value.refreshToken
? 10 * 60 * 1000 // 10分钟
: 365 * 24 * 60 * 60 * 1000 // 1年
data.openaiOauth = {
idToken: form.value.idToken, // 使用用户输入的 ID Token
accessToken: form.value.accessToken,
refreshToken: form.value.refreshToken || '',
expires_in: Math.floor(expiresInMs / 1000) // 转换为秒
}
// 手动模式下,尝试从 ID Token 解析用户信息
let accountInfo = {
accountId: '',
chatgptUserId: '',
organizationId: '',
organizationRole: '',
organizationTitle: '',
planType: '',
email: '',
emailVerified: false
}
// 尝试解析 ID Token (JWT)
if (form.value.idToken) {
try {
const idTokenParts = form.value.idToken.split('.')
if (idTokenParts.length === 3) {
const payload = JSON.parse(atob(idTokenParts[1]))
const authClaims = payload['https://api.openai.com/auth'] || {}
accountInfo = {
accountId: authClaims.accountId || '',
chatgptUserId: authClaims.chatgptUserId || '',
organizationId: authClaims.organizationId || '',
organizationRole: authClaims.organizationRole || '',
organizationTitle: authClaims.organizationTitle || '',
planType: authClaims.planType || '',
email: payload.email || '',
emailVerified: payload.email_verified || false
}
}
} catch (e) {
console.warn('Failed to parse ID Token:', e)
}
}
data.accountInfo = accountInfo
data.priority = form.value.priority || 50
} else if (form.value.platform === 'claude-console') {
// Claude Console 账户特定数据
data.apiUrl = form.value.apiUrl
@@ -1846,6 +1926,18 @@ const updateAccount = async () => {
token_type: 'Bearer',
expiry_date: Date.now() + expiresInMs
}
} else if (props.account.platform === 'openai') {
// OpenAI需要构建openaiOauth对象
const expiresInMs = form.value.refreshToken
? 10 * 60 * 1000 // 10分钟
: 365 * 24 * 60 * 60 * 1000 // 1年
data.openaiOauth = {
idToken: form.value.idToken || '', // 更新时使用用户输入的 ID Token
accessToken: form.value.accessToken || '',
refreshToken: form.value.refreshToken || '',
expires_in: Math.floor(expiresInMs / 1000) // 转换为秒
}
}
}
@@ -1858,6 +1950,11 @@ const updateAccount = async () => {
data.priority = form.value.priority || 50
}
// OpenAI 账号优先级更新
if (props.account.platform === 'openai') {
data.priority = form.value.priority || 50
}
// Claude Console 特定更新
if (props.account.platform === 'claude-console') {
data.apiUrl = form.value.apiUrl
@@ -2140,13 +2237,19 @@ watch(
password: ''
}
// 获取分组ID - 可能来自 groupId 字段或 groupInfo 对象
let groupId = ''
if (newAccount.accountType === 'group') {
groupId = newAccount.groupId || (newAccount.groupInfo && newAccount.groupInfo.id) || ''
}
form.value = {
platform: newAccount.platform,
addType: 'oauth',
name: newAccount.name,
description: newAccount.description || '',
accountType: newAccount.accountType || 'shared',
groupId: '',
groupId: groupId,
projectId: newAccount.projectId || '',
accessToken: '',
refreshToken: '',

View File

@@ -54,6 +54,10 @@
<input v-model="createForm.platform" class="mr-2" type="radio" value="gemini" />
<span class="text-sm text-gray-700">Gemini</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="createForm.platform" class="mr-2" type="radio" value="openai" />
<span class="text-sm text-gray-700">OpenAI</span>
</label>
</div>
</div>
@@ -114,10 +118,18 @@
'rounded-full px-2 py-1 text-xs font-medium',
group.platform === 'claude'
? 'bg-purple-100 text-purple-700'
: 'bg-blue-100 text-blue-700'
: group.platform === 'gemini'
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-700'
]"
>
{{ group.platform === 'claude' ? 'Claude' : 'Gemini' }}
{{
group.platform === 'claude'
? 'Claude'
: group.platform === 'gemini'
? 'Gemini'
: 'OpenAI'
}}
</span>
</div>
</div>
@@ -184,7 +196,13 @@
<div>
<label class="mb-2 block text-sm font-semibold text-gray-700">平台类型</label>
<div class="rounded-lg bg-gray-100 px-3 py-2 text-sm text-gray-600">
{{ editForm.platform === 'claude' ? 'Claude' : 'Gemini' }}
{{
editForm.platform === 'claude'
? 'Claude'
: editForm.platform === 'gemini'
? 'Gemini'
: 'OpenAI'
}}
<span class="ml-2 text-xs text-gray-500">(不可修改)</span>
</div>
</div>

View File

@@ -371,6 +371,10 @@
<input v-model="form.permissions" class="mr-2" type="radio" value="gemini" />
<span class="text-sm text-gray-700"> Gemini</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="openai" />
<span class="text-sm text-gray-700"> OpenAI</span>
</label>
</div>
<p class="mt-2 text-xs text-gray-500">控制此 API Key 可以访问哪些服务</p>
</div>
@@ -402,7 +406,7 @@
v-model="form.claudeAccountId"
:accounts="localAccounts.claude"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'gemini'"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
:groups="localAccounts.claudeGroups"
placeholder="请选择Claude账号"
platform="claude"
@@ -414,12 +418,24 @@
v-model="form.geminiAccountId"
:accounts="localAccounts.gemini"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'claude'"
:disabled="form.permissions === 'claude' || form.permissions === 'openai'"
:groups="localAccounts.geminiGroups"
placeholder="请选择Gemini账号"
platform="gemini"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600">OpenAI 专属账号</label>
<AccountSelector
v-model="form.openaiAccountId"
:accounts="localAccounts.openai"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'claude' || form.permissions === 'gemini'"
:groups="localAccounts.openaiGroups"
placeholder="请选择OpenAI账号"
platform="openai"
/>
</div>
</div>
<p class="mt-2 text-xs text-gray-500">
选择专属账号后此API Key将只使用该账号不选择则使用共享账号池
@@ -598,7 +614,14 @@ const clientsStore = useClientsStore()
const apiKeysStore = useApiKeysStore()
const loading = ref(false)
const accountsLoading = ref(false)
const localAccounts = ref({ claude: [], gemini: [], claudeGroups: [], geminiGroups: [] })
const localAccounts = ref({
claude: [],
gemini: [],
openai: [],
claudeGroups: [],
geminiGroups: [],
openaiGroups: []
})
// 表单验证状态
const errors = ref({
@@ -634,6 +657,7 @@ const form = reactive({
permissions: 'all',
claudeAccountId: '',
geminiAccountId: '',
openaiAccountId: '',
enableModelRestriction: false,
restrictedModels: [],
modelInput: '',
@@ -651,8 +675,10 @@ onMounted(async () => {
localAccounts.value = {
claude: props.accounts.claude || [],
gemini: props.accounts.gemini || [],
openai: props.accounts.openai || [],
claudeGroups: props.accounts.claudeGroups || [],
geminiGroups: props.accounts.geminiGroups || []
geminiGroups: props.accounts.geminiGroups || [],
openaiGroups: props.accounts.openaiGroups || []
}
}
})
@@ -661,10 +687,11 @@ onMounted(async () => {
const refreshAccounts = async () => {
accountsLoading.value = true
try {
const [claudeData, claudeConsoleData, geminiData, groupsData] = await Promise.all([
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')
])
@@ -700,11 +727,19 @@ const refreshAccounts = async () => {
}))
}
if (openaiData.success) {
localAccounts.value.openai = (openaiData.data || []).map((account) => ({
...account,
isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容
}))
}
// 处理分组数据
if (groupsData.success) {
const allGroups = groupsData.data || []
localAccounts.value.claudeGroups = allGroups.filter((g) => g.platform === 'claude')
localAccounts.value.geminiGroups = allGroups.filter((g) => g.platform === 'gemini')
localAccounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
}
showToast('账号列表已刷新', 'success')
@@ -899,6 +934,11 @@ const createApiKey = async () => {
baseData.geminiAccountId = form.geminiAccountId
}
// OpenAI账户绑定
if (form.openaiAccountId) {
baseData.openaiAccountId = form.openaiAccountId
}
if (form.createType === 'single') {
// 单个创建
const data = {

View File

@@ -274,6 +274,10 @@
<input v-model="form.permissions" class="mr-2" type="radio" value="gemini" />
<span class="text-sm text-gray-700"> Gemini</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="openai" />
<span class="text-sm text-gray-700"> OpenAI</span>
</label>
</div>
<p class="mt-2 text-xs text-gray-500">控制此 API Key 可以访问哪些服务</p>
</div>
@@ -305,7 +309,7 @@
v-model="form.claudeAccountId"
:accounts="localAccounts.claude"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'gemini'"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
:groups="localAccounts.claudeGroups"
placeholder="请选择Claude账号"
platform="claude"
@@ -317,12 +321,24 @@
v-model="form.geminiAccountId"
:accounts="localAccounts.gemini"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'claude'"
:disabled="form.permissions === 'claude' || form.permissions === 'openai'"
:groups="localAccounts.geminiGroups"
placeholder="请选择Gemini账号"
platform="gemini"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600">OpenAI 专属账号</label>
<AccountSelector
v-model="form.openaiAccountId"
:accounts="localAccounts.openai"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'claude' || form.permissions === 'gemini'"
:groups="localAccounts.openaiGroups"
placeholder="请选择OpenAI账号"
platform="openai"
/>
</div>
</div>
<p class="mt-2 text-xs text-gray-500">修改绑定账号将影响此API Key的请求路由</p>
</div>
@@ -502,7 +518,14 @@ const clientsStore = useClientsStore()
const apiKeysStore = useApiKeysStore()
const loading = ref(false)
const accountsLoading = ref(false)
const localAccounts = ref({ claude: [], gemini: [], claudeGroups: [], geminiGroups: [] })
const localAccounts = ref({
claude: [],
gemini: [],
openai: [],
claudeGroups: [],
geminiGroups: [],
openaiGroups: []
})
// 支持的客户端列表
const supportedClients = ref([])
@@ -527,6 +550,7 @@ const form = reactive({
permissions: 'all',
claudeAccountId: '',
geminiAccountId: '',
openaiAccountId: '',
enableModelRestriction: false,
restrictedModels: [],
modelInput: '',
@@ -642,6 +666,13 @@ const updateApiKey = async () => {
data.geminiAccountId = null
}
// OpenAI账户绑定
if (form.openaiAccountId) {
data.openaiAccountId = form.openaiAccountId
} else {
data.openaiAccountId = null
}
// 模型限制 - 始终提交这些字段
data.enableModelRestriction = form.enableModelRestriction
data.restrictedModels = form.restrictedModels
@@ -672,10 +703,11 @@ const updateApiKey = async () => {
const refreshAccounts = async () => {
accountsLoading.value = true
try {
const [claudeData, claudeConsoleData, geminiData, groupsData] = await Promise.all([
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')
])
@@ -711,11 +743,19 @@ const refreshAccounts = async () => {
}))
}
if (openaiData.success) {
localAccounts.value.openai = (openaiData.data || []).map((account) => ({
...account,
isDedicated: account.accountType === 'dedicated'
}))
}
// 处理分组数据
if (groupsData.success) {
const allGroups = groupsData.data || []
localAccounts.value.claudeGroups = allGroups.filter((g) => g.platform === 'claude')
localAccounts.value.geminiGroups = allGroups.filter((g) => g.platform === 'gemini')
localAccounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
}
showToast('账号列表已刷新', 'success')
@@ -737,8 +777,10 @@ onMounted(async () => {
localAccounts.value = {
claude: props.accounts.claude || [],
gemini: props.accounts.gemini || [],
openai: props.accounts.openai || [],
claudeGroups: props.accounts.claudeGroups || [],
geminiGroups: props.accounts.geminiGroups || []
geminiGroups: props.accounts.geminiGroups || [],
openaiGroups: props.accounts.openaiGroups || []
}
}
@@ -756,6 +798,7 @@ onMounted(async () => {
form.claudeAccountId = props.apiKey.claudeAccountId || ''
}
form.geminiAccountId = props.apiKey.geminiAccountId || ''
form.openaiAccountId = props.apiKey.openaiAccountId || ''
form.restrictedModels = props.apiKey.restrictedModels || []
form.allowedClients = props.apiKey.allowedClients || []
form.tags = props.apiKey.tags || []

View File

@@ -44,47 +44,19 @@
(statsData.limits.rateLimitRequests > 0 || statsData.limits.tokenLimit > 0)
"
>
<div class="mb-2 flex items-center justify-between">
<span class="text-sm font-medium text-gray-600 md:text-base">
时间窗口限制 ({{ statsData.limits.rateLimitWindow }}分钟)
</span>
</div>
<!-- 请求次数限制 -->
<div v-if="statsData.limits.rateLimitRequests > 0" class="mb-3 space-y-1.5">
<div class="flex items-center justify-between text-xs md:text-sm">
<span class="text-gray-500">请求次数</span>
<span class="text-gray-700">
{{ formatNumber(statsData.limits.currentWindowRequests) }} /
{{ formatNumber(statsData.limits.rateLimitRequests) }}
</span>
</div>
<div class="h-1.5 w-full rounded-full bg-gray-200">
<div
class="h-1.5 rounded-full transition-all duration-300"
:class="getWindowRequestProgressColor()"
:style="{ width: getWindowRequestProgress() + '%' }"
/>
</div>
</div>
<!-- Token使用量限制 -->
<div v-if="statsData.limits.tokenLimit > 0" class="space-y-1.5">
<div class="flex items-center justify-between text-xs md:text-sm">
<span class="text-gray-500">Token 使用量</span>
<span class="text-gray-700">
{{ formatNumber(statsData.limits.currentWindowTokens) }} /
{{ formatNumber(statsData.limits.tokenLimit) }}
</span>
</div>
<div class="h-1.5 w-full rounded-full bg-gray-200">
<div
class="h-1.5 rounded-full transition-all duration-300"
:class="getWindowTokenProgressColor()"
:style="{ width: getWindowTokenProgress() + '%' }"
/>
</div>
</div>
<WindowCountdown
:current-requests="statsData.limits.currentWindowRequests"
:current-tokens="statsData.limits.currentWindowTokens"
label="时间窗口限制"
:rate-limit-window="statsData.limits.rateLimitWindow"
:request-limit="statsData.limits.rateLimitRequests"
:show-progress="true"
:show-tooltip="true"
:token-limit="statsData.limits.tokenLimit"
:window-end-time="statsData.limits.windowEndTime"
:window-remaining-seconds="statsData.limits.windowRemainingSeconds"
:window-start-time="statsData.limits.windowStartTime"
/>
<div class="mt-2 text-xs text-gray-500">
<i class="fas fa-info-circle mr-1" />
@@ -226,28 +198,11 @@
<script setup>
import { storeToRefs } from 'pinia'
import { useApiStatsStore } from '@/stores/apistats'
import WindowCountdown from '@/components/apikeys/WindowCountdown.vue'
const apiStatsStore = useApiStatsStore()
const { statsData } = storeToRefs(apiStatsStore)
// 格式化数字
const formatNumber = (num) => {
if (typeof num !== 'number') {
num = parseInt(num) || 0
}
if (num === 0) return '0'
// 大数字使用简化格式
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M'
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K'
} else {
return num.toLocaleString()
}
}
// 获取每日费用进度
const getDailyCostProgress = () => {
if (!statsData.value.limits.dailyCostLimit || statsData.value.limits.dailyCostLimit === 0)
@@ -264,39 +219,6 @@ const getDailyCostProgressColor = () => {
if (progress >= 80) return 'bg-yellow-500'
return 'bg-green-500'
}
// 获取窗口请求进度
const getWindowRequestProgress = () => {
if (!statsData.value.limits.rateLimitRequests || statsData.value.limits.rateLimitRequests === 0)
return 0
const percentage =
(statsData.value.limits.currentWindowRequests / statsData.value.limits.rateLimitRequests) * 100
return Math.min(percentage, 100)
}
// 获取窗口请求进度条颜色
const getWindowRequestProgressColor = () => {
const progress = getWindowRequestProgress()
if (progress >= 100) return 'bg-red-500'
if (progress >= 80) return 'bg-yellow-500'
return 'bg-blue-500'
}
// 获取窗口Token进度
const getWindowTokenProgress = () => {
if (!statsData.value.limits.tokenLimit || statsData.value.limits.tokenLimit === 0) return 0
const percentage =
(statsData.value.limits.currentWindowTokens / statsData.value.limits.tokenLimit) * 100
return Math.min(percentage, 100)
}
// 获取窗口Token进度条颜色
const getWindowTokenProgressColor = () => {
const progress = getWindowTokenProgress()
if (progress >= 100) return 'bg-red-500'
if (progress >= 80) return 'bg-yellow-500'
return 'bg-purple-500'
}
</script>
<style scoped>

View File

@@ -822,7 +822,7 @@ const platformOptions = ref([
{ value: 'claude', label: 'Claude', icon: 'fa-brain' },
{ value: 'claude-console', label: 'Claude Console', icon: 'fa-terminal' },
{ value: 'gemini', label: 'Gemini', icon: 'fa-robot' },
{ value: 'openai', label: 'OpenAi', icon: 'fa-robot' },
{ value: 'openai', label: 'OpenAi', icon: 'fa-openai' },
{ value: 'bedrock', label: 'Bedrock', icon: 'fab fa-aws' }
])
@@ -834,8 +834,13 @@ const groupOptions = computed(() => {
accountGroups.value.forEach((group) => {
options.push({
value: group.id,
label: `${group.name} (${group.platform === 'claude' ? 'Claude' : 'Gemini'})`,
icon: group.platform === 'claude' ? 'fa-brain' : 'fa-robot'
label: `${group.name} (${group.platform === 'claude' ? 'Claude' : group.platform === 'gemini' ? 'Gemini' : 'OpenAI'})`,
icon:
group.platform === 'claude'
? 'fa-brain'
: group.platform === 'gemini'
? 'fa-robot'
: 'fa-openai'
})
})
return options
@@ -1326,6 +1331,8 @@ const toggleSchedulable = async (account) => {
endpoint = `/admin/bedrock-accounts/${account.id}/toggle-schedulable`
} else if (account.platform === 'gemini') {
endpoint = `/admin/gemini-accounts/${account.id}/toggle-schedulable`
} else if (account.platform === 'openai') {
endpoint = `/admin/openai-accounts/${account.id}/toggle-schedulable`
} else {
showToast('该账户类型暂不支持调度控制', 'warning')
return

View File

@@ -1052,7 +1052,14 @@ const expandedApiKeys = ref({})
const apiKeyModelStats = ref({})
const apiKeyDateFilters = ref({})
const defaultTime = ref([new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)])
const accounts = ref({ claude: [], gemini: [], claudeGroups: [], geminiGroups: [] })
const accounts = ref({
claude: [],
gemini: [],
openai: [],
claudeGroups: [],
geminiGroups: [],
openaiGroups: []
})
const editingExpiryKey = ref(null)
const expiryEditModalRef = ref(null)
const showUsageDetailModal = ref(false)
@@ -1185,10 +1192,11 @@ const paginatedApiKeys = computed(() => {
// 加载账户列表
const loadAccounts = async () => {
try {
const [claudeData, claudeConsoleData, geminiData, groupsData] = await Promise.all([
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')
])
@@ -1209,11 +1217,16 @@ const loadAccounts = async () => {
accounts.value.gemini = geminiData.data || []
}
if (openaiData.success) {
accounts.value.openai = openaiData.data || []
}
if (groupsData.success) {
// 处理分组数据
const allGroups = groupsData.data || []
accounts.value.claudeGroups = allGroups.filter((g) => g.platform === 'claude')
accounts.value.geminiGroups = allGroups.filter((g) => g.platform === 'gemini')
accounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai')
}
} catch (error) {
console.error('加载账户列表失败:', error)

View File

@@ -85,6 +85,20 @@
dashboardData.accountsByPlatform.bedrock.total
}}</span>
</div>
<!-- OpenAI账户 -->
<div
v-if="
dashboardData.accountsByPlatform.openai &&
dashboardData.accountsByPlatform.openai.total > 0
"
class="inline-flex items-center gap-0.5"
:title="`OpenAI: ${dashboardData.accountsByPlatform.openai.total} 个 (正常: ${dashboardData.accountsByPlatform.openai.normal})`"
>
<i class="fas fa-openai text-xs text-gray-100" />
<span class="text-xs font-medium text-gray-700">{{
dashboardData.accountsByPlatform.openai.total
}}</span>
</div>
</div>
</div>
<p class="mt-1 text-xs text-gray-500">

View File

@@ -382,6 +382,76 @@
</div>
</div>
</div>
<!-- Codex 环境变量设置 -->
<div class="mt-8">
<h5
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg"
>
<i class="fas fa-code mr-2 text-indigo-600" />
配置 Codex 环境变量
</h5>
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base">
如果你使用支持 OpenAI API 的工具 Codex需要设置以下环境变量
</p>
<div class="space-y-4">
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">
PowerShell 设置方法
</h6>
<p class="mb-3 text-sm text-gray-600"> PowerShell 中运行以下命令</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">
$env:OPENAI_BASE_URL = "{{ openaiBaseUrl }}"
</div>
<div class="whitespace-nowrap text-gray-300">
$env:OPENAI_API_KEY = "你的API密钥"
</div>
</div>
<p class="mt-2 text-xs text-yellow-700">
💡 使用与 Claude Code 相同的 API 密钥即可格式如 cr_xxxxxxxxxx
</p>
</div>
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">
PowerShell 永久设置用户级
</h6>
<p class="mb-3 text-sm text-gray-600"> PowerShell 中运行以下命令</p>
<div
class="mb-3 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="mb-2"># 设置用户级环境变量永久生效</div>
<div class="whitespace-nowrap text-gray-300">
[System.Environment]::SetEnvironmentVariable("OPENAI_BASE_URL", "{{
openaiBaseUrl
}}", [System.EnvironmentVariableTarget]::User)
</div>
<div class="whitespace-nowrap text-gray-300">
[System.Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "你的API密钥",
[System.EnvironmentVariableTarget]::User)
</div>
</div>
<p class="mt-2 text-xs text-blue-700">
💡 设置后需要重新打开 PowerShell 窗口才能生效
</p>
</div>
<div class="rounded-lg border border-indigo-200 bg-indigo-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-indigo-800">验证 Codex 环境变量</h6>
<p class="mb-3 text-sm text-indigo-700"> PowerShell 中验证</p>
<div
class="space-y-1 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">echo $env:OPENAI_BASE_URL</div>
<div class="whitespace-nowrap text-gray-300">echo $env:OPENAI_API_KEY</div>
</div>
</div>
</div>
</div>
</div>
<!-- 第四步开始使用 -->
@@ -790,6 +860,79 @@
</div>
</div>
</div>
<!-- Codex 环境变量设置 -->
<div class="mt-8">
<h5
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg"
>
<i class="fas fa-code mr-2 text-indigo-600" />
配置 Codex 环境变量
</h5>
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base">
如果你使用支持 OpenAI API 的工具 Codex需要设置以下环境变量
</p>
<div class="space-y-4">
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">Terminal 设置方法</h6>
<p class="mb-3 text-sm text-gray-600"> Terminal 中运行以下命令</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">
export OPENAI_BASE_URL="{{ openaiBaseUrl }}"
</div>
<div class="whitespace-nowrap text-gray-300">
export OPENAI_API_KEY="你的API密钥"
</div>
</div>
<p class="mt-2 text-xs text-yellow-700">
💡 使用与 Claude Code 相同的 API 密钥即可格式如 cr_xxxxxxxxxx
</p>
</div>
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">永久设置方法</h6>
<p class="mb-3 text-sm text-gray-600">添加到你的 shell 配置文件</p>
<div
class="mb-3 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="mb-2"># 对于 zsh (默认)</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_BASE_URL="{{ openaiBaseUrl }}"' >> ~/.zshrc
</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_API_KEY="你的API密钥"' >> ~/.zshrc
</div>
<div class="whitespace-nowrap text-gray-300">source ~/.zshrc</div>
</div>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="mb-2"># 对于 bash</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_BASE_URL="{{ openaiBaseUrl }}"' >> ~/.bash_profile
</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_API_KEY="你的API密钥"' >> ~/.bash_profile
</div>
<div class="whitespace-nowrap text-gray-300">source ~/.bash_profile</div>
</div>
</div>
<div class="rounded-lg border border-indigo-200 bg-indigo-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-indigo-800">验证 Codex 环境变量</h6>
<p class="mb-3 text-sm text-indigo-700"> Terminal 中验证</p>
<div
class="space-y-1 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">echo $OPENAI_BASE_URL</div>
<div class="whitespace-nowrap text-gray-300">echo $OPENAI_API_KEY</div>
</div>
</div>
</div>
</div>
</div>
<!-- 第四步开始使用 -->
@@ -1191,6 +1334,79 @@
</div>
</div>
</div>
<!-- Codex 环境变量设置 -->
<div class="mt-8">
<h5
class="mb-2 flex items-center text-base font-semibold text-gray-800 sm:mb-3 sm:text-lg"
>
<i class="fas fa-code mr-2 text-indigo-600" />
配置 Codex 环境变量
</h5>
<p class="mb-3 text-sm text-gray-700 sm:mb-4 sm:text-base">
如果你使用支持 OpenAI API 的工具 Codex需要设置以下环境变量
</p>
<div class="space-y-4">
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">终端设置方法</h6>
<p class="mb-3 text-sm text-gray-600">在终端中运行以下命令</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">
export OPENAI_BASE_URL="{{ openaiBaseUrl }}"
</div>
<div class="whitespace-nowrap text-gray-300">
export OPENAI_API_KEY="你的API密钥"
</div>
</div>
<p class="mt-2 text-xs text-yellow-700">
💡 使用与 Claude Code 相同的 API 密钥即可格式如 cr_xxxxxxxxxx
</p>
</div>
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 sm:text-base">永久设置方法</h6>
<p class="mb-3 text-sm text-gray-600">添加到你的 shell 配置文件</p>
<div
class="mb-3 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="mb-2"># 对于 bash (默认)</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_BASE_URL="{{ openaiBaseUrl }}"' >> ~/.bashrc
</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_API_KEY="你的API密钥"' >> ~/.bashrc
</div>
<div class="whitespace-nowrap text-gray-300">source ~/.bashrc</div>
</div>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="mb-2"># 对于 zsh</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_BASE_URL="{{ openaiBaseUrl }}"' >> ~/.zshrc
</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_API_KEY="你的API密钥"' >> ~/.zshrc
</div>
<div class="whitespace-nowrap text-gray-300">source ~/.zshrc</div>
</div>
</div>
<div class="rounded-lg border border-indigo-200 bg-indigo-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-indigo-800">验证 Codex 环境变量</h6>
<p class="mb-3 text-sm text-indigo-700">在终端中验证</p>
<div
class="space-y-1 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">echo $OPENAI_BASE_URL</div>
<div class="whitespace-nowrap text-gray-300">echo $OPENAI_API_KEY</div>
</div>
</div>
</div>
</div>
</div>
<!-- 第四步开始使用 -->
@@ -1395,6 +1611,11 @@ const currentBaseUrl = computed(() => {
const geminiBaseUrl = computed(() => {
return getBaseUrlPrefix() + '/gemini'
})
// OpenAI/Codex 基础URL
const openaiBaseUrl = computed(() => {
return getBaseUrlPrefix() + '/openai'
})
</script>
<style scoped>