Merge branch 'Wei-Shaw:main' into main

This commit is contained in:
jft0m
2025-10-10 21:37:18 +08:00
committed by GitHub
34 changed files with 6599 additions and 697 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -352,7 +352,8 @@ const platformLabelMap = {
'claude-console': 'Claude Console',
openai: 'OpenAI',
'openai-responses': 'OpenAI Responses',
gemini: 'Gemini'
gemini: 'Gemini',
droid: 'Droid'
}
const platformLabel = computed(() => platformLabelMap[props.account?.platform] || '未知平台')

View File

@@ -58,6 +58,10 @@
<input v-model="createForm.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">
<input v-model="createForm.platform" class="mr-2" type="radio" value="droid" />
<span class="text-sm text-gray-700">Droid</span>
</label>
</div>
</div>
@@ -120,7 +124,9 @@
? 'bg-purple-100 text-purple-700'
: group.platform === 'gemini'
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-700'
: group.platform === 'openai'
? 'bg-gray-100 text-gray-700'
: 'bg-cyan-100 text-cyan-700'
]"
>
{{
@@ -128,7 +134,9 @@
? 'Claude'
: group.platform === 'gemini'
? 'Gemini'
: 'OpenAI'
: group.platform === 'openai'
? 'OpenAI'
: 'Droid'
}}
</span>
</div>

View File

@@ -464,6 +464,170 @@
</div>
</div>
<!-- Droid OAuth流程 -->
<div v-else-if="platform === 'droid'">
<div
class="rounded-lg border border-cyan-200 bg-cyan-50 p-6 dark:border-cyan-700 dark:bg-cyan-900/30"
>
<div class="flex items-start gap-4">
<div
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-cyan-500"
>
<i class="fas fa-robot text-white" />
</div>
<div class="flex-1">
<h4 class="mb-3 font-semibold text-cyan-900 dark:text-cyan-200">Droid 账户授权</h4>
<p class="mb-4 text-sm text-cyan-800 dark:text-cyan-300">
请按照以下步骤完成 Factory (Droid) 账户的授权
</p>
<div class="space-y-4">
<!-- 步骤1: 生成授权链接 -->
<div
class="rounded-lg border border-cyan-300 bg-white/80 p-4 dark:border-cyan-600 dark:bg-gray-800/80"
>
<div class="flex items-start gap-3">
<div
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-600 text-xs font-bold text-white"
>
1
</div>
<div class="flex-1">
<p class="mb-2 font-medium text-cyan-900 dark:text-cyan-200">
点击下方按钮生成授权链接
</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-4">
<div class="space-y-2">
<label class="text-xs font-semibold text-gray-600 dark:text-gray-300"
>授权链接</label
>
<div
class="flex flex-col gap-2 rounded-md border border-cyan-200 bg-white p-3 dark:border-cyan-700 dark:bg-gray-800"
>
<div class="flex items-center gap-2">
<input
class="form-input flex-1 bg-gray-50 font-mono text-xs dark:bg-gray-700"
readonly
type="text"
:value="authUrl"
/>
<button
class="rounded-lg bg-gray-100 px-3 py-2 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600"
title="复制链接"
@click="copyAuthUrl"
>
<i :class="copied ? 'fas fa-check text-green-500' : 'fas fa-copy'" />
</button>
</div>
<div class="flex flex-wrap items-center gap-2">
<button
class="inline-flex items-center gap-1 rounded-md border border-cyan-200 bg-white px-3 py-1.5 text-xs font-medium text-cyan-600 shadow-sm transition-colors hover:border-cyan-300 hover:bg-cyan-50 dark:border-cyan-700 dark:bg-cyan-900/40 dark:text-cyan-200 dark:hover:border-cyan-500 dark:hover:bg-cyan-900/60"
@click="openVerificationPage"
>
<i class="fas fa-external-link-alt text-xs" /> 在新标签中打开
</button>
<button
class="inline-flex items-center gap-1 rounded-md px-3 py-1.5 text-xs font-medium text-cyan-600 transition-colors hover:text-cyan-700 dark:text-cyan-300 dark:hover:text-cyan-200"
@click="regenerateAuthUrl"
>
<i class="fas fa-sync-alt text-xs" />重新生成
</button>
</div>
</div>
</div>
<div class="space-y-2">
<label class="text-xs font-semibold text-gray-600 dark:text-gray-300"
>授权验证码</label
>
<div
class="flex items-center justify-between rounded-md border border-cyan-200 bg-cyan-50 px-4 py-3 dark:border-cyan-700 dark:bg-cyan-900/30"
>
<span
class="font-mono text-xl font-semibold text-cyan-700 dark:text-cyan-200"
>
{{ userCode || '------' }}
</span>
<button
class="rounded-lg bg-white px-3 py-1 text-sm text-cyan-600 transition-colors hover:bg-cyan-100 dark:bg-cyan-800 dark:text-cyan-200 dark:hover:bg-cyan-700"
@click="copyUserCode"
>
<i class="fas fa-copy mr-1" />复制
</button>
</div>
</div>
<div
class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"
>
<span>
<i class="fas fa-hourglass-half mr-1 text-cyan-500" />
剩余有效期{{ formattedCountdown }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- 步骤2: 访问链接并授权 -->
<div
class="rounded-lg border border-cyan-300 bg-white/80 p-4 dark:border-cyan-600 dark:bg-gray-800/80"
>
<div class="flex items-start gap-3">
<div
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-600 text-xs font-bold text-white"
>
2
</div>
<div class="flex-1">
<p class="mb-2 font-medium text-cyan-900 dark:text-cyan-200">
在浏览器中打开链接并完成授权
</p>
<div class="space-y-2 text-sm text-cyan-700 dark:text-cyan-300">
<p>
在浏览器中打开授权页面输入上方验证码并登录 Factory / Droid
账户最后点击允许授权
</p>
</div>
</div>
</div>
</div>
<!-- 步骤3: 输入授权结果 -->
<div
class="rounded-lg border border-cyan-300 bg-white/80 p-4 dark:border-cyan-600 dark:bg-gray-800/80"
>
<div class="flex items-start gap-3">
<div
class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-cyan-600 text-xs font-bold text-white"
>
3
</div>
<div class="flex-1">
<p class="mb-2 font-medium text-cyan-900 dark:text-cyan-200">
完成授权后点击下方完成授权按钮系统会自动获取访问令牌
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
若提示授权仍在等待确认请稍候片刻后系统会自动重试
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex gap-3 pt-4">
<button
class="flex-1 rounded-xl bg-gray-100 px-6 py-3 font-semibold text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
@@ -486,7 +650,7 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onBeforeUnmount } from 'vue'
import { showToast } from '@/utils/toast'
import { useAccountsStore } from '@/stores/accounts'
@@ -512,14 +676,57 @@ const authUrl = ref('')
const authCode = ref('')
const copied = ref(false)
const sessionId = ref('') // 保存sessionId用于后续交换
const userCode = ref('')
const verificationUri = ref('')
const verificationUriComplete = ref('')
const remainingSeconds = ref(0)
let countdownTimer = null
// 计算是否可以交换code
const canExchange = computed(() => {
if (props.platform === 'droid') {
return !!sessionId.value
}
return authUrl.value && authCode.value.trim()
})
const formattedCountdown = computed(() => {
if (!remainingSeconds.value || remainingSeconds.value <= 0) {
return '00:00'
}
const minutes = Math.floor(remainingSeconds.value / 60)
const seconds = remainingSeconds.value % 60
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
})
const startCountdown = (seconds) => {
stopCountdown()
if (!seconds || seconds <= 0) {
remainingSeconds.value = 0
return
}
remainingSeconds.value = Math.floor(seconds)
countdownTimer = setInterval(() => {
if (remainingSeconds.value <= 1) {
remainingSeconds.value = 0
stopCountdown()
} else {
remainingSeconds.value -= 1
}
}, 1000)
}
const stopCountdown = () => {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
}
// 监听授权码输入自动提取URL中的code参数
watch(authCode, (newValue) => {
if (props.platform === 'droid') return
if (!newValue || typeof newValue !== 'string') return
const trimmedValue = newValue.trim()
@@ -579,6 +786,15 @@ watch(authCode, (newValue) => {
// 生成授权URL
const generateAuthUrl = async () => {
stopCountdown()
authUrl.value = ''
authCode.value = ''
userCode.value = ''
verificationUri.value = ''
verificationUriComplete.value = ''
remainingSeconds.value = 0
sessionId.value = ''
copied.value = false
loading.value = true
try {
const proxyConfig = props.proxy?.enabled
@@ -605,6 +821,14 @@ const generateAuthUrl = async () => {
const result = await accountsStore.generateOpenAIAuthUrl(proxyConfig)
authUrl.value = result.authUrl
sessionId.value = result.sessionId
} else if (props.platform === 'droid') {
const result = await accountsStore.generateDroidAuthUrl(proxyConfig)
authUrl.value = result.verificationUriComplete || result.verificationUri
verificationUri.value = result.verificationUri
verificationUriComplete.value = result.verificationUriComplete || result.verificationUri
userCode.value = result.userCode
startCountdown(result.expiresIn || 300)
sessionId.value = result.sessionId
}
} catch (error) {
showToast(error.message || '生成授权链接失败', 'error')
@@ -615,13 +839,23 @@ const generateAuthUrl = async () => {
// 重新生成授权URL
const regenerateAuthUrl = () => {
stopCountdown()
authUrl.value = ''
authCode.value = ''
userCode.value = ''
verificationUri.value = ''
verificationUriComplete.value = ''
remainingSeconds.value = 0
sessionId.value = ''
generateAuthUrl()
}
// 复制授权URL
const copyAuthUrl = async () => {
if (!authUrl.value) {
showToast('请先生成授权链接', 'warning')
return
}
try {
await navigator.clipboard.writeText(authUrl.value)
copied.value = true
@@ -645,6 +879,33 @@ const copyAuthUrl = async () => {
}
}
const copyUserCode = async () => {
if (!userCode.value) {
showToast('请先生成授权验证码', 'warning')
return
}
try {
await navigator.clipboard.writeText(userCode.value)
showToast('验证码已复制', 'success')
} catch (error) {
const input = document.createElement('input')
input.value = userCode.value
document.body.appendChild(input)
input.select()
document.execCommand('copy')
document.body.removeChild(input)
showToast('验证码已复制', 'success')
}
}
const openVerificationPage = () => {
if (verificationUriComplete.value) {
window.open(verificationUriComplete.value, '_blank', 'noopener')
} else if (verificationUri.value) {
window.open(verificationUri.value, '_blank', 'noopener')
}
}
// 交换授权码
const exchangeCode = async () => {
if (!canExchange.value) return
@@ -671,6 +932,10 @@ const exchangeCode = async () => {
code: authCode.value.trim(),
sessionId: sessionId.value
}
} else if (props.platform === 'droid') {
data = {
sessionId: sessionId.value
}
}
// 添加代理配置(如果启用)
@@ -691,6 +956,21 @@ const exchangeCode = async () => {
tokenInfo = await accountsStore.exchangeGeminiCode(data)
} else if (props.platform === 'openai') {
tokenInfo = await accountsStore.exchangeOpenAICode(data)
} else if (props.platform === 'droid') {
const response = await accountsStore.exchangeDroidCode(data)
if (!response.success) {
if (response.pending) {
const message = response.message || '授权尚未完成,请在浏览器确认后稍候再次尝试。'
showToast(message, 'info')
if (typeof response.expiresIn === 'number' && response.expiresIn >= 0) {
startCountdown(response.expiresIn)
}
return
}
throw new Error(response.message || '授权失败,请重试')
}
tokenInfo = response.data
stopCountdown()
}
emit('success', tokenInfo)
@@ -700,4 +980,8 @@ const exchangeCode = async () => {
exchanging.value = false
}
}
onBeforeUnmount(() => {
stopCountdown()
})
</script>

View File

@@ -311,6 +311,10 @@
<input v-model="form.permissions" class="mr-2" type="radio" value="openai" />
<span class="text-sm text-gray-700"> OpenAI</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="droid" />
<span class="text-sm text-gray-700"> Droid</span>
</label>
</div>
</div>
@@ -345,7 +349,7 @@
<select
v-model="form.claudeAccountId"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
:disabled="form.permissions && !['all', 'claude'].includes(form.permissions)"
>
<option value="">不修改</option>
<option value="SHARED_POOL">使用共享账号池</option>
@@ -380,7 +384,7 @@
<select
v-model="form.geminiAccountId"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
:disabled="form.permissions === 'claude' || form.permissions === 'openai'"
:disabled="form.permissions && !['all', 'gemini'].includes(form.permissions)"
>
<option value="">不修改</option>
<option value="SHARED_POOL">使用共享账号池</option>
@@ -411,7 +415,7 @@
<select
v-model="form.openaiAccountId"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
:disabled="form.permissions === 'claude' || form.permissions === 'gemini'"
:disabled="form.permissions && !['all', 'openai'].includes(form.permissions)"
>
<option value="">不修改</option>
<option value="SHARED_POOL">使用共享账号池</option>
@@ -442,7 +446,7 @@
<select
v-model="form.bedrockAccountId"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
:disabled="form.permissions && !['all', 'openai'].includes(form.permissions)"
>
<option value="">不修改</option>
<option value="SHARED_POOL">使用共享账号池</option>
@@ -457,6 +461,37 @@
</optgroup>
</select>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>Droid 专属账号</label
>
<select
v-model="form.droidAccountId"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
:disabled="form.permissions && !['all', 'droid'].includes(form.permissions)"
>
<option value="">不修改</option>
<option value="SHARED_POOL">使用共享账号池</option>
<optgroup v-if="localAccounts.droidGroups.length > 0" label="账号分组">
<option
v-for="group in localAccounts.droidGroups"
:key="group.id"
:value="`group:${group.id}`"
>
分组 - {{ group.name }}
</option>
</optgroup>
<optgroup v-if="localAccounts.droid.length > 0" label="专属账号">
<option
v-for="account in localAccounts.droid"
:key="account.id"
:value="account.id"
>
{{ account.name }}
</option>
</optgroup>
</select>
</div>
</div>
</div>
@@ -497,7 +532,17 @@ const props = defineProps({
},
accounts: {
type: Object,
default: () => ({ claude: [], gemini: [], openai: [], bedrock: [] })
default: () => ({
claude: [],
gemini: [],
openai: [],
bedrock: [],
droid: [],
claudeGroups: [],
geminiGroups: [],
openaiGroups: [],
droidGroups: []
})
}
})
@@ -511,9 +556,11 @@ const localAccounts = ref({
gemini: [],
openai: [],
bedrock: [],
droid: [],
claudeGroups: [],
geminiGroups: [],
openaiGroups: []
openaiGroups: [],
droidGroups: []
})
// 标签相关
@@ -542,6 +589,7 @@ const form = reactive({
geminiAccountId: '',
openaiAccountId: '',
bedrockAccountId: '',
droidAccountId: '',
tags: [],
isActive: null // null表示不修改
})
@@ -571,15 +619,23 @@ const removeTag = (index) => {
const refreshAccounts = async () => {
accountsLoading.value = true
try {
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'),
apiClient.get('/admin/account-groups')
])
const [
claudeData,
claudeConsoleData,
geminiData,
openaiData,
bedrockData,
droidData,
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'),
apiClient.get('/admin/droid-accounts'),
apiClient.get('/admin/account-groups')
])
// 合并Claude OAuth账户和Claude Console账户
const claudeAccounts = []
@@ -627,12 +683,21 @@ const refreshAccounts = async () => {
}))
}
if (droidData.success) {
localAccounts.value.droid = (droidData.data || []).map((account) => ({
...account,
platform: 'droid',
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')
localAccounts.value.droidGroups = allGroups.filter((g) => g.platform === 'droid')
}
showToast('账号列表已刷新', 'success')
@@ -720,6 +785,14 @@ const batchUpdateApiKeys = async () => {
}
}
if (form.droidAccountId !== '') {
if (form.droidAccountId === 'SHARED_POOL') {
updates.droidAccountId = null
} else {
updates.droidAccountId = form.droidAccountId
}
}
// 激活状态
if (form.isActive !== null) {
updates.isActive = form.isActive
@@ -774,9 +847,11 @@ onMounted(async () => {
gemini: props.accounts.gemini || [],
openai: props.accounts.openai || [],
bedrock: props.accounts.bedrock || [],
droid: props.accounts.droid || [],
claudeGroups: props.accounts.claudeGroups || [],
geminiGroups: props.accounts.geminiGroups || [],
openaiGroups: props.accounts.openaiGroups || []
openaiGroups: props.accounts.openaiGroups || [],
droidGroups: props.accounts.droidGroups || []
}
}
})

View File

@@ -616,6 +616,15 @@
/>
<span class="text-sm text-gray-700 dark:text-gray-300"> OpenAI</span>
</label>
<label class="flex cursor-pointer items-center">
<input
v-model="form.permissions"
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
type="radio"
value="droid"
/>
<span class="text-sm text-gray-700 dark:text-gray-300"> Droid</span>
</label>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
控制此 API Key 可以访问哪些服务
@@ -653,7 +662,7 @@
v-model="form.claudeAccountId"
:accounts="localAccounts.claude"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
:disabled="form.permissions !== 'all' && form.permissions !== 'claude'"
:groups="localAccounts.claudeGroups"
placeholder="请选择Claude账号"
platform="claude"
@@ -667,7 +676,7 @@
v-model="form.geminiAccountId"
:accounts="localAccounts.gemini"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'claude' || form.permissions === 'openai'"
:disabled="form.permissions !== 'all' && form.permissions !== 'gemini'"
:groups="localAccounts.geminiGroups"
placeholder="请选择Gemini账号"
platform="gemini"
@@ -681,7 +690,7 @@
v-model="form.openaiAccountId"
:accounts="localAccounts.openai"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'claude' || form.permissions === 'gemini'"
:disabled="form.permissions !== 'all' && form.permissions !== 'openai'"
:groups="localAccounts.openaiGroups"
placeholder="请选择OpenAI账号"
platform="openai"
@@ -695,12 +704,26 @@
v-model="form.bedrockAccountId"
:accounts="localAccounts.bedrock"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
:disabled="form.permissions !== 'all' && form.permissions !== 'openai'"
:groups="[]"
placeholder="请选择Bedrock账号"
platform="bedrock"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>Droid 专属账号</label
>
<AccountSelector
v-model="form.droidAccountId"
:accounts="localAccounts.droid"
default-option-text="使用共享账号池"
:disabled="form.permissions !== 'all' && form.permissions !== 'droid'"
:groups="localAccounts.droidGroups"
placeholder="请选择Droid账号"
platform="droid"
/>
</div>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
选择专属账号后此API Key将只使用该账号不选择则使用共享账号池
@@ -875,7 +898,17 @@ import AccountSelector from '@/components/common/AccountSelector.vue'
const props = defineProps({
accounts: {
type: Object,
default: () => ({ claude: [], gemini: [] })
default: () => ({
claude: [],
gemini: [],
openai: [],
bedrock: [],
droid: [],
claudeGroups: [],
geminiGroups: [],
openaiGroups: [],
droidGroups: []
})
}
})
@@ -889,10 +922,12 @@ const localAccounts = ref({
claude: [],
gemini: [],
openai: [],
bedrock: [], // 添加 Bedrock 账号列表
bedrock: [],
droid: [],
claudeGroups: [],
geminiGroups: [],
openaiGroups: []
openaiGroups: [],
droidGroups: []
})
// 表单验证状态
@@ -935,7 +970,8 @@ const form = reactive({
claudeAccountId: '',
geminiAccountId: '',
openaiAccountId: '',
bedrockAccountId: '', // 添加 Bedrock 账号ID
bedrockAccountId: '',
droidAccountId: '',
enableModelRestriction: false,
restrictedModels: [],
modelInput: '',
@@ -973,10 +1009,15 @@ onMounted(async () => {
claude: props.accounts.claude || [],
gemini: props.accounts.gemini || [],
openai: openaiAccounts,
bedrock: props.accounts.bedrock || [], // 添加 Bedrock 账号
bedrock: props.accounts.bedrock || [],
droid: (props.accounts.droid || []).map((account) => ({
...account,
platform: 'droid'
})),
claudeGroups: props.accounts.claudeGroups || [],
geminiGroups: props.accounts.geminiGroups || [],
openaiGroups: props.accounts.openaiGroups || []
openaiGroups: props.accounts.openaiGroups || [],
droidGroups: props.accounts.droidGroups || []
}
}
@@ -995,6 +1036,7 @@ const refreshAccounts = async () => {
openaiData,
openaiResponsesData,
bedrockData,
droidData,
groupsData
] = await Promise.all([
apiClient.get('/admin/claude-accounts'),
@@ -1002,7 +1044,8 @@ const refreshAccounts = async () => {
apiClient.get('/admin/gemini-accounts'),
apiClient.get('/admin/openai-accounts'),
apiClient.get('/admin/openai-responses-accounts'), // 获取 OpenAI-Responses 账号
apiClient.get('/admin/bedrock-accounts'), // 添加 Bedrock 账号获取
apiClient.get('/admin/bedrock-accounts'),
apiClient.get('/admin/droid-accounts'),
apiClient.get('/admin/account-groups')
])
@@ -1070,12 +1113,21 @@ const refreshAccounts = async () => {
}))
}
if (droidData.success) {
localAccounts.value.droid = (droidData.data || []).map((account) => ({
...account,
platform: 'droid',
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')
localAccounts.value.droidGroups = allGroups.filter((g) => g.platform === 'droid')
}
showToast('账号列表已刷新', 'success')
@@ -1346,6 +1398,9 @@ const createApiKey = async () => {
if (form.bedrockAccountId) {
baseData.bedrockAccountId = form.bedrockAccountId
}
if (form.droidAccountId) {
baseData.droidAccountId = form.droidAccountId
}
if (form.createType === 'single') {
// 单个创建

View File

@@ -449,6 +449,15 @@
/>
<span class="text-sm text-gray-700 dark:text-gray-300"> OpenAI</span>
</label>
<label class="flex cursor-pointer items-center">
<input
v-model="form.permissions"
class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
type="radio"
value="droid"
/>
<span class="text-sm text-gray-700 dark:text-gray-300"> Droid</span>
</label>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
控制此 API Key 可以访问哪些服务
@@ -486,7 +495,7 @@
v-model="form.claudeAccountId"
:accounts="localAccounts.claude"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
:disabled="form.permissions !== 'all' && form.permissions !== 'claude'"
:groups="localAccounts.claudeGroups"
placeholder="请选择Claude账号"
platform="claude"
@@ -500,7 +509,7 @@
v-model="form.geminiAccountId"
:accounts="localAccounts.gemini"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'claude' || form.permissions === 'openai'"
:disabled="form.permissions !== 'all' && form.permissions !== 'gemini'"
:groups="localAccounts.geminiGroups"
placeholder="请选择Gemini账号"
platform="gemini"
@@ -514,7 +523,7 @@
v-model="form.openaiAccountId"
:accounts="localAccounts.openai"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'claude' || form.permissions === 'gemini'"
:disabled="form.permissions !== 'all' && form.permissions !== 'openai'"
:groups="localAccounts.openaiGroups"
placeholder="请选择OpenAI账号"
platform="openai"
@@ -528,12 +537,26 @@
v-model="form.bedrockAccountId"
:accounts="localAccounts.bedrock"
default-option-text="使用共享账号池"
:disabled="form.permissions === 'gemini' || form.permissions === 'openai'"
:disabled="form.permissions !== 'all' && form.permissions !== 'openai'"
:groups="[]"
placeholder="请选择Bedrock账号"
platform="bedrock"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>Droid 专属账号</label
>
<AccountSelector
v-model="form.droidAccountId"
:accounts="localAccounts.droid"
default-option-text="使用共享账号池"
:disabled="form.permissions !== 'all' && form.permissions !== 'droid'"
:groups="localAccounts.droidGroups"
placeholder="请选择Droid账号"
platform="droid"
/>
</div>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
修改绑定账号将影响此API Key的请求路由
@@ -717,7 +740,18 @@ const props = defineProps({
},
accounts: {
type: Object,
default: () => ({ claude: [], gemini: [] })
default: () => ({
claude: [],
gemini: [],
openai: [],
bedrock: [],
droid: [],
claudeGroups: [],
geminiGroups: [],
openaiGroups: [],
droidGroups: [],
openaiResponses: []
})
}
})
@@ -732,10 +766,12 @@ const localAccounts = ref({
claude: [],
gemini: [],
openai: [],
bedrock: [], // 添加 Bedrock 账号列表
bedrock: [],
droid: [],
claudeGroups: [],
geminiGroups: [],
openaiGroups: []
openaiGroups: [],
droidGroups: []
})
// 支持的客户端列表
@@ -768,7 +804,8 @@ const form = reactive({
claudeAccountId: '',
geminiAccountId: '',
openaiAccountId: '',
bedrockAccountId: '', // 添加 Bedrock 账号ID
bedrockAccountId: '',
droidAccountId: '',
enableModelRestriction: false,
restrictedModels: [],
modelInput: '',
@@ -930,6 +967,12 @@ const updateApiKey = async () => {
data.bedrockAccountId = null
}
if (form.droidAccountId) {
data.droidAccountId = form.droidAccountId
} else {
data.droidAccountId = null
}
// 模型限制 - 始终提交这些字段
data.enableModelRestriction = form.enableModelRestriction
data.restrictedModels = form.restrictedModels
@@ -972,14 +1015,16 @@ const refreshAccounts = async () => {
openaiData,
openaiResponsesData,
bedrockData,
droidData,
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/openai-responses-accounts'), // 获取 OpenAI-Responses 账号
apiClient.get('/admin/bedrock-accounts'), // 添加 Bedrock 账号获取
apiClient.get('/admin/openai-responses-accounts'),
apiClient.get('/admin/bedrock-accounts'),
apiClient.get('/admin/droid-accounts'),
apiClient.get('/admin/account-groups')
])
@@ -1047,12 +1092,21 @@ const refreshAccounts = async () => {
}))
}
if (droidData.success) {
localAccounts.value.droid = (droidData.data || []).map((account) => ({
...account,
platform: 'droid',
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')
localAccounts.value.droidGroups = allGroups.filter((g) => g.platform === 'droid')
}
showToast('账号列表已刷新', 'success')
@@ -1128,10 +1182,15 @@ onMounted(async () => {
claude: props.accounts.claude || [],
gemini: props.accounts.gemini || [],
openai: openaiAccounts,
bedrock: props.accounts.bedrock || [], // 添加 Bedrock 账号
bedrock: props.accounts.bedrock || [],
droid: (props.accounts.droid || []).map((account) => ({
...account,
platform: 'droid'
})),
claudeGroups: props.accounts.claudeGroups || [],
geminiGroups: props.accounts.geminiGroups || [],
openaiGroups: props.accounts.openaiGroups || []
openaiGroups: props.accounts.openaiGroups || [],
droidGroups: props.accounts.droidGroups || []
}
}
@@ -1168,7 +1227,8 @@ onMounted(async () => {
// 处理 OpenAI 账号 - 直接使用后端传来的值(已包含 responses: 前缀)
form.openaiAccountId = props.apiKey.openaiAccountId || ''
form.bedrockAccountId = props.apiKey.bedrockAccountId || '' // 添加 Bedrock 账号ID初始化
form.bedrockAccountId = props.apiKey.bedrockAccountId || ''
form.droidAccountId = props.apiKey.droidAccountId || ''
form.restrictedModels = props.apiKey.restrictedModels || []
form.allowedClients = props.apiKey.allowedClients || []
form.tags = props.apiKey.tags || []

View File

@@ -93,7 +93,7 @@
</div>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
点击眼睛图标切换显示模式使用下方按钮复制完整 API Key
点击眼睛图标切换显示模式使用下方按钮复制环境变量配置
</p>
</div>
</div>
@@ -105,7 +105,7 @@
@click="copyApiKey"
>
<i class="fas fa-copy" />
复制 API Key
复制配置信息
</button>
<button
class="rounded-xl border border-gray-300 bg-gray-200 px-6 py-3 font-semibold text-gray-800 transition-colors hover:bg-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
@@ -120,7 +120,7 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { showToast } from '@/utils/toast'
const props = defineProps({
@@ -134,6 +134,41 @@ const emit = defineEmits(['close'])
const showFullKey = ref(false)
// 获取 API Base URL 前缀
const getBaseUrlPrefix = () => {
// 优先使用环境变量配置的自定义前缀
const customPrefix = import.meta.env.VITE_API_BASE_PREFIX
if (customPrefix) {
// 去除末尾的斜杠
return customPrefix.replace(/\/$/, '')
}
// 否则使用当前浏览器访问地址
if (typeof window !== 'undefined') {
const protocol = window.location.protocol // http: 或 https:
const host = window.location.host // 域名和端口
// 提取协议和主机部分,去除路径
let origin = protocol + '//' + host
// 如果当前URL包含路径只取协议+主机部分
const currentUrl = window.location.href
const pathStart = currentUrl.indexOf('/', 8) // 跳过 http:// 或 https://
if (pathStart !== -1) {
origin = currentUrl.substring(0, pathStart)
}
return origin
}
// 服务端渲染或其他情况的回退
return ''
}
// 计算完整的 API Base URL
const currentBaseUrl = computed(() => {
return getBaseUrlPrefix() + '/api'
})
// 切换密钥可见性
const toggleKeyVisibility = () => {
showFullKey.value = !showFullKey.value
@@ -155,7 +190,7 @@ const getDisplayedApiKey = () => {
}
}
// 复制 API Key
// 复制配置信息(环境变量格式)
const copyApiKey = async () => {
const key = props.apiKey.apiKey || props.apiKey.key || ''
if (!key) {
@@ -163,19 +198,23 @@ const copyApiKey = async () => {
return
}
// 构建环境变量配置格式
const configText = `ANTHROPIC_BASE_URL="${currentBaseUrl.value}"
ANTHROPIC_AUTH_TOKEN="${key}"`
try {
await navigator.clipboard.writeText(key)
showToast('API Key 已复制到剪贴板', 'success')
await navigator.clipboard.writeText(configText)
showToast('配置信息已复制到剪贴板', 'success')
} catch (error) {
// console.error('Failed to copy:', error)
// 降级方案:创建一个临时文本区域
const textArea = document.createElement('textarea')
textArea.value = key
textArea.value = configText
document.body.appendChild(textArea)
textArea.select()
try {
document.execCommand('copy')
showToast('API Key 已复制到剪贴板', 'success')
showToast('配置信息已复制到剪贴板', 'success')
} catch (fallbackError) {
showToast('复制失败,请手动复制', 'error')
} finally {

View File

@@ -104,7 +104,9 @@
? 'Claude OAuth 专属账号'
: platform === 'openai'
? 'OpenAI 专属账号'
: 'OAuth 专属账号'
: platform === 'droid'
? 'Droid 专属账号'
: 'OAuth 专属账号'
}}
</div>
<div
@@ -241,7 +243,7 @@ const props = defineProps({
platform: {
type: String,
required: true,
validator: (value) => ['claude', 'gemini', 'openai', 'bedrock'].includes(value)
validator: (value) => ['claude', 'gemini', 'openai', 'bedrock', 'droid'].includes(value)
},
accounts: {
type: Array,
@@ -383,6 +385,8 @@ const filteredOAuthAccounts = computed(() => {
} else if (props.platform === 'openai') {
// 对于 OpenAI只显示 openai 类型的账号
accounts = sortedAccounts.value.filter((a) => a.platform === 'openai')
} else if (props.platform === 'droid') {
accounts = sortedAccounts.value.filter((a) => a.platform === 'droid')
} else {
// 其他平台显示所有非特殊类型的账号
accounts = sortedAccounts.value.filter(

View File

@@ -11,6 +11,7 @@ export const useAccountsStore = defineStore('accounts', () => {
const openaiAccounts = ref([])
const azureOpenaiAccounts = ref([])
const openaiResponsesAccounts = ref([])
const droidAccounts = ref([])
const loading = ref(false)
const error = ref(null)
const sortBy = ref('')
@@ -151,6 +152,25 @@ export const useAccountsStore = defineStore('accounts', () => {
}
}
// 获取Droid账户列表
const fetchDroidAccounts = async () => {
loading.value = true
error.value = null
try {
const response = await apiClient.get('/admin/droid-accounts')
if (response.success) {
droidAccounts.value = response.data || []
} else {
throw new Error(response.message || '获取Droid账户失败')
}
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
// 获取所有账户
const fetchAllAccounts = async () => {
loading.value = true
@@ -163,7 +183,8 @@ export const useAccountsStore = defineStore('accounts', () => {
fetchGeminiAccounts(),
fetchOpenAIAccounts(),
fetchAzureOpenAIAccounts(),
fetchOpenAIResponsesAccounts()
fetchOpenAIResponsesAccounts(),
fetchDroidAccounts()
])
} catch (err) {
error.value = err.message
@@ -273,6 +294,46 @@ export const useAccountsStore = defineStore('accounts', () => {
}
}
// 创建Droid账户
const createDroidAccount = async (data) => {
loading.value = true
error.value = null
try {
const response = await apiClient.post('/admin/droid-accounts', data)
if (response.success) {
await fetchDroidAccounts()
return response.data
} else {
throw new Error(response.message || '创建Droid账户失败')
}
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
// 更新Droid账户
const updateDroidAccount = async (id, data) => {
loading.value = true
error.value = null
try {
const response = await apiClient.put(`/admin/droid-accounts/${id}`, data)
if (response.success) {
await fetchDroidAccounts()
return response.data
} else {
throw new Error(response.message || '更新Droid账户失败')
}
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
// 创建Azure OpenAI账户
const createAzureOpenAIAccount = async (data) => {
loading.value = true
@@ -694,6 +755,22 @@ export const useAccountsStore = defineStore('accounts', () => {
}
}
// 生成Droid OAuth URL
const generateDroidAuthUrl = async (proxyConfig) => {
error.value = null
try {
const response = await apiClient.post('/admin/droid-accounts/generate-auth-url', proxyConfig)
if (response.success) {
return response.data
} else {
throw new Error(response.message || '生成授权URL失败')
}
} catch (err) {
error.value = err.message
throw err
}
}
// 交换OpenAI OAuth Code
const exchangeOpenAICode = async (data) => {
try {
@@ -709,6 +786,18 @@ export const useAccountsStore = defineStore('accounts', () => {
}
}
// 交换Droid OAuth Code
const exchangeDroidCode = async (data) => {
error.value = null
try {
const response = await apiClient.post('/admin/droid-accounts/exchange-code', data)
return response
} catch (err) {
error.value = err.message
throw err
}
}
// 排序账户
const sortAccounts = (field) => {
if (sortBy.value === field) {
@@ -728,6 +817,7 @@ export const useAccountsStore = defineStore('accounts', () => {
openaiAccounts.value = []
azureOpenaiAccounts.value = []
openaiResponsesAccounts.value = []
droidAccounts.value = []
loading.value = false
error.value = null
sortBy.value = ''
@@ -743,6 +833,7 @@ export const useAccountsStore = defineStore('accounts', () => {
openaiAccounts,
azureOpenaiAccounts,
openaiResponsesAccounts,
droidAccounts,
loading,
error,
sortBy,
@@ -756,12 +847,15 @@ export const useAccountsStore = defineStore('accounts', () => {
fetchOpenAIAccounts,
fetchAzureOpenAIAccounts,
fetchOpenAIResponsesAccounts,
fetchDroidAccounts,
fetchAllAccounts,
createClaudeAccount,
createClaudeConsoleAccount,
createBedrockAccount,
createGeminiAccount,
createOpenAIAccount,
createDroidAccount,
updateDroidAccount,
createAzureOpenAIAccount,
createOpenAIResponsesAccount,
updateClaudeAccount,
@@ -782,6 +876,8 @@ export const useAccountsStore = defineStore('accounts', () => {
exchangeGeminiCode,
generateOpenAIAuthUrl,
exchangeOpenAICode,
generateDroidAuthUrl,
exchangeDroidCode,
sortAccounts,
reset
}

View File

@@ -537,6 +537,17 @@
<span class="mx-1 h-4 w-px bg-teal-300 dark:bg-teal-600" />
<span class="text-xs font-medium text-teal-700 dark:text-teal-300">Relay</span>
</div>
<div
v-else-if="account.platform === 'droid'"
class="flex items-center gap-1.5 rounded-lg border border-cyan-200 bg-gradient-to-r from-cyan-100 to-sky-100 px-2.5 py-1 dark:border-cyan-700 dark:from-cyan-900/20 dark:to-sky-900/20"
>
<i class="fas fa-robot text-xs text-cyan-700 dark:text-cyan-400" />
<span class="text-xs font-semibold text-cyan-800 dark:text-cyan-300"
>Droid</span
>
<span class="mx-1 h-4 w-px bg-cyan-300 dark:bg-cyan-600" />
<span class="text-xs font-medium text-cyan-700 dark:text-cyan-300">OAuth</span>
</div>
<div
v-else
class="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-gradient-to-r from-gray-100 to-gray-200 px-2.5 py-1"
@@ -656,7 +667,8 @@
account.platform === 'openai' ||
account.platform === 'openai-responses' ||
account.platform === 'azure_openai' ||
account.platform === 'ccr'
account.platform === 'ccr' ||
account.platform === 'droid'
"
class="flex items-center gap-2"
>
@@ -1108,7 +1120,9 @@
? 'bg-gradient-to-br from-gray-600 to-gray-700'
: account.platform === 'ccr'
? 'bg-gradient-to-br from-teal-500 to-emerald-600'
: 'bg-gradient-to-br from-blue-500 to-blue-600'
: account.platform === 'droid'
? 'bg-gradient-to-br from-cyan-500 to-sky-600'
: 'bg-gradient-to-br from-blue-500 to-blue-600'
]"
>
<i
@@ -1124,7 +1138,9 @@
? 'fas fa-openai'
: account.platform === 'ccr'
? 'fas fa-code-branch'
: 'fas fa-robot'
: account.platform === 'droid'
? 'fas fa-robot'
: 'fas fa-robot'
]"
/>
</div>
@@ -1696,7 +1712,14 @@ const accountUsageSummary = ref({})
const accountUsageOverview = ref({})
const accountUsageGeneratedAt = ref('')
const supportedUsagePlatforms = ['claude', 'claude-console', 'openai', 'openai-responses', 'gemini']
const supportedUsagePlatforms = [
'claude',
'claude-console',
'openai',
'openai-responses',
'gemini',
'droid'
]
// 缓存状态标志
const apiKeysLoaded = ref(false)
@@ -1722,7 +1745,8 @@ const platformOptions = ref([
{ value: 'azure_openai', label: 'Azure OpenAI', icon: 'fab fa-microsoft' },
{ value: 'bedrock', label: 'Bedrock', icon: 'fab fa-aws' },
{ value: 'openai-responses', label: 'OpenAI-Responses', icon: 'fa-server' },
{ value: 'ccr', label: 'CCR', icon: 'fa-code-branch' }
{ value: 'ccr', label: 'CCR', icon: 'fa-code-branch' },
{ value: 'droid', label: 'Droid', icon: 'fa-robot' }
])
const groupOptions = computed(() => {
@@ -1733,13 +1757,15 @@ const groupOptions = computed(() => {
accountGroups.value.forEach((group) => {
options.push({
value: group.id,
label: `${group.name} (${group.platform === 'claude' ? 'Claude' : group.platform === 'gemini' ? 'Gemini' : 'OpenAI'})`,
label: `${group.name} (${group.platform === 'claude' ? 'Claude' : group.platform === 'gemini' ? 'Gemini' : group.platform === 'openai' ? 'OpenAI' : 'Droid'})`,
icon:
group.platform === 'claude'
? 'fa-brain'
: group.platform === 'gemini'
? 'fa-robot'
: 'fa-openai'
: group.platform === 'openai'
? 'fa-openai'
: 'fa-robot'
})
})
return options
@@ -2044,7 +2070,8 @@ const loadAccounts = async (forceReload = false) => {
apiClient.get('/admin/openai-accounts', { params }),
apiClient.get('/admin/azure-openai-accounts', { params }),
apiClient.get('/admin/openai-responses-accounts', { params }),
apiClient.get('/admin/ccr-accounts', { params })
apiClient.get('/admin/ccr-accounts', { params }),
apiClient.get('/admin/droid-accounts', { params })
)
} else {
// 只请求指定平台其他平台设为null占位
@@ -2057,7 +2084,9 @@ const loadAccounts = async (forceReload = false) => {
Promise.resolve({ success: true, data: [] }), // gemini 占位
Promise.resolve({ success: true, data: [] }), // openai 占位
Promise.resolve({ success: true, data: [] }), // azure-openai 占位
Promise.resolve({ success: true, data: [] }) // openai-responses 占位
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
Promise.resolve({ success: true, data: [] }), // ccr 占位
Promise.resolve({ success: true, data: [] }) // droid 占位
)
break
case 'claude-console':
@@ -2068,7 +2097,9 @@ const loadAccounts = async (forceReload = false) => {
Promise.resolve({ success: true, data: [] }), // gemini 占位
Promise.resolve({ success: true, data: [] }), // openai 占位
Promise.resolve({ success: true, data: [] }), // azure-openai 占位
Promise.resolve({ success: true, data: [] }) // openai-responses 占位
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
Promise.resolve({ success: true, data: [] }), // ccr 占位
Promise.resolve({ success: true, data: [] }) // droid 占位
)
break
case 'bedrock':
@@ -2079,7 +2110,9 @@ const loadAccounts = async (forceReload = false) => {
Promise.resolve({ success: true, data: [] }), // gemini 占位
Promise.resolve({ success: true, data: [] }), // openai 占位
Promise.resolve({ success: true, data: [] }), // azure-openai 占位
Promise.resolve({ success: true, data: [] }) // openai-responses 占位
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
Promise.resolve({ success: true, data: [] }), // ccr 占位
Promise.resolve({ success: true, data: [] }) // droid 占位
)
break
case 'gemini':
@@ -2090,7 +2123,9 @@ const loadAccounts = async (forceReload = false) => {
apiClient.get('/admin/gemini-accounts', { params }),
Promise.resolve({ success: true, data: [] }), // openai 占位
Promise.resolve({ success: true, data: [] }), // azure-openai 占位
Promise.resolve({ success: true, data: [] }) // openai-responses 占位
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
Promise.resolve({ success: true, data: [] }), // ccr 占位
Promise.resolve({ success: true, data: [] }) // droid 占位
)
break
case 'openai':
@@ -2101,7 +2136,9 @@ const loadAccounts = async (forceReload = false) => {
Promise.resolve({ success: true, data: [] }), // gemini 占位
apiClient.get('/admin/openai-accounts', { params }),
Promise.resolve({ success: true, data: [] }), // azure-openai 占位
Promise.resolve({ success: true, data: [] }) // openai-responses 占位
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
Promise.resolve({ success: true, data: [] }), // ccr 占位
Promise.resolve({ success: true, data: [] }) // droid 占位
)
break
case 'azure_openai':
@@ -2112,7 +2149,9 @@ const loadAccounts = async (forceReload = false) => {
Promise.resolve({ success: true, data: [] }), // gemini 占位
Promise.resolve({ success: true, data: [] }), // openai 占位
apiClient.get('/admin/azure-openai-accounts', { params }),
Promise.resolve({ success: true, data: [] }) // openai-responses 占位
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
Promise.resolve({ success: true, data: [] }), // ccr 占位
Promise.resolve({ success: true, data: [] }) // droid 占位
)
break
case 'openai-responses':
@@ -2123,7 +2162,9 @@ const loadAccounts = async (forceReload = false) => {
Promise.resolve({ success: true, data: [] }), // gemini 占位
Promise.resolve({ success: true, data: [] }), // openai 占位
Promise.resolve({ success: true, data: [] }), // azure-openai 占位
apiClient.get('/admin/openai-responses-accounts', { params })
apiClient.get('/admin/openai-responses-accounts', { params }),
Promise.resolve({ success: true, data: [] }), // ccr 占位
Promise.resolve({ success: true, data: [] }) // droid 占位
)
break
case 'ccr':
@@ -2134,7 +2175,22 @@ const loadAccounts = async (forceReload = false) => {
Promise.resolve({ success: true, data: [] }), // gemini 占位
Promise.resolve({ success: true, data: [] }), // openai 占位
Promise.resolve({ success: true, data: [] }), // azure 占位
apiClient.get('/admin/ccr-accounts', { params })
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
apiClient.get('/admin/ccr-accounts', { params }),
Promise.resolve({ success: true, data: [] }) // droid 占位
)
break
case 'droid':
requests.push(
Promise.resolve({ success: true, data: [] }), // claude 占位
Promise.resolve({ success: true, data: [] }), // claude-console 占位
Promise.resolve({ success: true, data: [] }), // bedrock 占位
Promise.resolve({ success: true, data: [] }), // gemini 占位
Promise.resolve({ success: true, data: [] }), // openai 占位
Promise.resolve({ success: true, data: [] }), // azure 占位
Promise.resolve({ success: true, data: [] }), // openai-responses 占位
Promise.resolve({ success: true, data: [] }), // ccr 占位
apiClient.get('/admin/droid-accounts', { params })
)
break
default:
@@ -2146,6 +2202,8 @@ const loadAccounts = async (forceReload = false) => {
Promise.resolve({ success: true, data: [] }),
Promise.resolve({ success: true, data: [] }),
Promise.resolve({ success: true, data: [] }),
Promise.resolve({ success: true, data: [] }),
Promise.resolve({ success: true, data: [] }),
Promise.resolve({ success: true, data: [] })
)
break
@@ -2166,7 +2224,8 @@ const loadAccounts = async (forceReload = false) => {
openaiData,
azureOpenaiData,
openaiResponsesData,
ccrData
ccrData,
droidData
] = await Promise.all(requests)
const allAccounts = []
@@ -2260,6 +2319,18 @@ const loadAccounts = async (forceReload = false) => {
allAccounts.push(...ccrAccounts)
}
// Droid 账户
if (droidData && droidData.success) {
const droidAccounts = (droidData.data || []).map((acc) => {
return {
...acc,
platform: 'droid',
boundApiKeysCount: acc.boundApiKeysCount ?? 0
}
})
allAccounts.push(...droidAccounts)
}
// 根据分组筛选器过滤账户
let filteredAccounts = allAccounts
if (groupFilter.value !== 'all') {
@@ -2600,6 +2671,8 @@ const resolveAccountDeleteEndpoint = (account) => {
return `/admin/ccr-accounts/${account.id}`
case 'gemini':
return `/admin/gemini-accounts/${account.id}`
case 'droid':
return `/admin/droid-accounts/${account.id}`
default:
return null
}
@@ -2778,6 +2851,8 @@ const resetAccountStatus = async (account) => {
endpoint = `/admin/claude-console-accounts/${account.id}/reset-status`
} else if (account.platform === 'ccr') {
endpoint = `/admin/ccr-accounts/${account.id}/reset-status`
} else if (account.platform === 'droid') {
endpoint = `/admin/droid-accounts/${account.id}/reset-status`
} else {
showToast('不支持的账户类型', 'error')
account.isResetting = false
@@ -2824,6 +2899,8 @@ const toggleSchedulable = async (account) => {
endpoint = `/admin/openai-responses-accounts/${account.id}/toggle-schedulable`
} else if (account.platform === 'ccr') {
endpoint = `/admin/ccr-accounts/${account.id}/toggle-schedulable`
} else if (account.platform === 'droid') {
endpoint = `/admin/droid-accounts/${account.id}/toggle-schedulable`
} else {
showToast('该账户类型暂不支持调度控制', 'warning')
return

View File

@@ -511,6 +511,18 @@
{{ getBedrockBindingInfo(key) }}
</span>
</div>
<!-- Droid 绑定 -->
<div v-if="key.droidAccountId" class="flex items-center gap-1 text-xs">
<span
class="inline-flex items-center rounded bg-cyan-100 px-1.5 py-0.5 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-300"
>
<i class="fas fa-robot mr-1 text-[10px]" />
Droid
</span>
<span class="truncate text-gray-600 dark:text-gray-400">
{{ getDroidBindingInfo(key) }}
</span>
</div>
<!-- 共享池 -->
<div
v-if="
@@ -518,7 +530,8 @@
!key.claudeConsoleAccountId &&
!key.geminiAccountId &&
!key.openaiAccountId &&
!key.bedrockAccountId
!key.bedrockAccountId &&
!key.droidAccountId
"
class="text-xs text-gray-500 dark:text-gray-400"
>
@@ -594,6 +607,47 @@
variant="compact"
/>
<!-- 时间窗口费用限制(无每日和总费用限制时展示) -->
<div
v-else-if="
key.rateLimitWindow > 0 &&
key.rateLimitCost > 0 &&
(!key.dailyCostLimit || key.dailyCostLimit === 0) &&
(!key.totalCostLimit || key.totalCostLimit === 0)
"
class="space-y-1.5"
>
<!-- 费用进度条 -->
<LimitProgressBar
:current="key.currentWindowCost || 0"
label="窗口费用"
:limit="key.rateLimitCost"
type="window"
variant="compact"
/>
<!-- 重置倒计时 -->
<div class="flex items-center justify-between text-[10px]">
<div class="flex items-center gap-1 text-sky-600 dark:text-sky-300">
<i class="fas fa-clock text-[10px]" />
<span class="font-medium">{{ key.rateLimitWindow }}分钟窗口</span>
</div>
<span
class="font-bold"
:class="
key.windowRemainingSeconds > 0
? 'text-sky-700 dark:text-sky-300'
: 'text-gray-400 dark:text-gray-500'
"
>
{{
key.windowRemainingSeconds > 0
? formatWindowTime(key.windowRemainingSeconds)
: '未激活'
}}
</span>
</div>
</div>
<!-- 如果没有任何限制 -->
<div
v-else
@@ -1141,6 +1195,18 @@
{{ getBedrockBindingInfo(key) }}
</span>
</div>
<!-- Droid 绑定 -->
<div v-if="key.droidAccountId" class="flex flex-wrap items-center gap-1 text-xs">
<span
class="inline-flex items-center rounded bg-cyan-100 px-2 py-0.5 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-300"
>
<i class="fas fa-robot mr-1" />
Droid
</span>
<span class="text-gray-600 dark:text-gray-400">
{{ getDroidBindingInfo(key) }}
</span>
</div>
<!-- 无绑定时显示共享池 -->
<div
v-if="
@@ -1148,7 +1214,8 @@
!key.claudeConsoleAccountId &&
!key.geminiAccountId &&
!key.openaiAccountId &&
!key.bedrockAccountId
!key.bedrockAccountId &&
!key.droidAccountId
"
class="text-xs text-gray-500 dark:text-gray-400"
>
@@ -1221,6 +1288,47 @@
variant="compact"
/>
<!-- 时间窗口费用限制(无每日和总费用限制时展示) -->
<div
v-else-if="
key.rateLimitWindow > 0 &&
key.rateLimitCost > 0 &&
(!key.dailyCostLimit || key.dailyCostLimit === 0) &&
(!key.totalCostLimit || key.totalCostLimit === 0)
"
class="space-y-2"
>
<!-- 费用进度条 -->
<LimitProgressBar
:current="key.currentWindowCost || 0"
label="窗口费用"
:limit="key.rateLimitCost"
type="window"
variant="compact"
/>
<!-- 重置倒计时 -->
<div class="flex items-center justify-between text-xs">
<div class="flex items-center gap-1.5 text-sky-600 dark:text-sky-300">
<i class="fas fa-clock text-xs" />
<span class="font-medium">{{ key.rateLimitWindow }}分钟窗口</span>
</div>
<span
class="font-bold"
:class="
key.windowRemainingSeconds > 0
? 'text-sky-700 dark:text-sky-300'
: 'text-gray-400 dark:text-gray-500'
"
>
{{
key.windowRemainingSeconds > 0
? formatWindowTime(key.windowRemainingSeconds)
: '未激活'
}}
</span>
</div>
</div>
<!-- 无限制显示 -->
<div
v-else
@@ -1839,9 +1947,11 @@ const accounts = ref({
openai: [],
openaiResponses: [], // 添加 OpenAI-Responses 账号列表
bedrock: [],
droid: [],
claudeGroups: [],
geminiGroups: [],
openaiGroups: []
openaiGroups: [],
droidGroups: []
})
const editingExpiryKey = ref(null)
const expiryEditModalRef = ref(null)
@@ -1949,12 +2059,17 @@ const getBindingDisplayStrings = (key) => {
appendBindingRow('Bedrock', getBedrockBindingInfo(key))
}
if (key.droidAccountId) {
appendBindingRow('Droid', getDroidBindingInfo(key))
}
if (
!key.claudeAccountId &&
!key.claudeConsoleAccountId &&
!key.geminiAccountId &&
!key.openaiAccountId &&
!key.bedrockAccountId
!key.bedrockAccountId &&
!key.droidAccountId
) {
collect('共享池')
}
@@ -2114,6 +2229,7 @@ const loadAccounts = async () => {
openaiData,
openaiResponsesData,
bedrockData,
droidData,
groupsData
] = await Promise.all([
apiClient.get('/admin/claude-accounts'),
@@ -2122,6 +2238,7 @@ const loadAccounts = async () => {
apiClient.get('/admin/openai-accounts'),
apiClient.get('/admin/openai-responses-accounts'), // 加载 OpenAI-Responses 账号
apiClient.get('/admin/bedrock-accounts'),
apiClient.get('/admin/droid-accounts'),
apiClient.get('/admin/account-groups')
])
@@ -2178,12 +2295,21 @@ const loadAccounts = async () => {
}))
}
if (droidData.success) {
accounts.value.droid = (droidData.data || []).map((account) => ({
...account,
platform: 'droid',
isDedicated: account.accountType === 'dedicated'
}))
}
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')
accounts.value.droidGroups = allGroups.filter((g) => g.platform === 'droid')
}
} catch (error) {
// console.error('加载账户列表失败:', error)
@@ -2299,6 +2425,11 @@ const getBoundAccountName = (accountId) => {
return `分组-${openaiGroup.name}`
}
const droidGroup = accounts.value.droidGroups.find((g) => g.id === groupId)
if (droidGroup) {
return `分组-${droidGroup.name}`
}
// 如果找不到分组返回分组ID的前8位
return `分组-${groupId.substring(0, 8)}`
}
@@ -2346,6 +2477,11 @@ const getBoundAccountName = (accountId) => {
return `${bedrockAccount.name}`
}
const droidAccount = accounts.value.droid.find((acc) => acc.id === accountId)
if (droidAccount) {
return `${droidAccount.name}`
}
// 如果找不到返回账户ID的前8位
return `${accountId.substring(0, 8)}`
}
@@ -2448,6 +2584,24 @@ const getBedrockBindingInfo = (key) => {
return ''
}
const getDroidBindingInfo = (key) => {
if (key.droidAccountId) {
const info = getBoundAccountName(key.droidAccountId)
if (key.droidAccountId.startsWith('group:')) {
return info
}
const account = accounts.value.droid.find((acc) => acc.id === key.droidAccountId)
if (!account) {
return `⚠️ ${info} (账户不存在)`
}
if (account.accountType === 'dedicated') {
return `🔒 专属-${info}`
}
return info
}
return ''
}
// 检查API Key是否过期
const isApiKeyExpired = (expiresAt) => {
if (!expiresAt) return false
@@ -3432,6 +3586,23 @@ const formatDate = (dateString) => {
.replace(/\//g, '-')
}
// 格式化时间窗口倒计时
const formatWindowTime = (seconds) => {
if (seconds === null || seconds === undefined) return '--:--'
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = seconds % 60
if (hours > 0) {
return `${hours}h${minutes}m`
} else if (minutes > 0) {
return `${minutes}m${secs}s`
} else {
return `${secs}s`
}
}
// 获取每日费用进度 - 已移到 LimitProgressBar 组件中
// const getDailyCostProgress = (key) => {
// if (!key.dailyCostLimit || key.dailyCostLimit === 0) return 0
@@ -3555,7 +3726,9 @@ const exportToExcel = () => {
? '仅Gemini'
: key.permissions === 'openai'
? '仅OpenAI'
: key.permissions || '',
: key.permissions === 'droid'
? '仅Droid'
: key.permissions || '',
// 限制配置
令牌限制: key.tokenLimit === '0' || key.tokenLimit === 0 ? '无限制' : key.tokenLimit || '',
@@ -3587,6 +3760,7 @@ const exportToExcel = () => {
OpenAI专属账户: key.openaiAccountId || '',
'Azure OpenAI专属账户': key.azureOpenaiAccountId || '',
Bedrock专属账户: key.bedrockAccountId || '',
Droid专属账户: key.droidAccountId || '',
// 模型和客户端限制
启用模型限制: key.enableModelRestriction ? '是' : '否',

File diff suppressed because it is too large Load Diff