mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
merge: 合并远程 dev 分支,整合 CCR 和 OpenAI-Responses 功能
## 合并内容 - 成功合并远程 dev 分支的 CCR (Claude Connector) 功能 - 保留本地的 OpenAI-Responses 账户管理功能 - 解决所有合并冲突,保留双方功能 ## UI 调整 - 将 CCR 平台归类到 Claude 分组中 - 保留新的平台分组选择器设计 - 支持所有平台类型:Claude、CCR、OpenAI、OpenAI-Responses、Gemini、Azure OpenAI、Bedrock 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -280,6 +280,37 @@
|
||||
<i class="fas fa-check text-xs text-white"></i>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label
|
||||
class="group relative flex cursor-pointer items-center rounded-md border p-2 transition-all"
|
||||
:class="[
|
||||
form.platform === 'ccr'
|
||||
? 'border-cyan-500 bg-cyan-50 dark:border-cyan-400 dark:bg-cyan-900/30'
|
||||
: 'border-gray-300 bg-white hover:border-cyan-400 hover:bg-cyan-50/50 dark:border-gray-600 dark:bg-gray-700 dark:hover:border-cyan-500 dark:hover:bg-cyan-900/20'
|
||||
]"
|
||||
>
|
||||
<input
|
||||
v-model="form.platform"
|
||||
class="sr-only"
|
||||
type="radio"
|
||||
value="ccr"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas fa-code-branch text-sm text-cyan-600 dark:text-cyan-400"></i>
|
||||
<div>
|
||||
<span class="block text-xs font-medium text-gray-900 dark:text-gray-100"
|
||||
>CCR</span
|
||||
>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">Claude Connector</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="form.platform === 'ccr'"
|
||||
class="absolute right-1 top-1 flex h-4 w-4 items-center justify-center rounded-full bg-cyan-500"
|
||||
>
|
||||
<i class="fas fa-check text-xs text-white"></i>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<!-- OpenAI 子选项 -->
|
||||
@@ -428,7 +459,8 @@
|
||||
form.platform !== 'claude-console' &&
|
||||
form.platform !== 'bedrock' &&
|
||||
form.platform !== 'azure_openai' &&
|
||||
form.platform !== 'openai-responses'
|
||||
form.platform !== 'openai-responses' &&
|
||||
form.platform !== 'ccr'
|
||||
"
|
||||
>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
@@ -2709,7 +2741,7 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
const emit = defineEmits(['close', 'success', 'platform-changed'])
|
||||
|
||||
const accountsStore = useAccountsStore()
|
||||
const { showConfirmModal, confirmOptions, showConfirm, handleConfirm, handleCancel } = useConfirm()
|
||||
@@ -3953,6 +3985,17 @@ watch(setupTokenAuthCode, (newValue) => {
|
||||
// 如果不是 URL,保持原值(兼容直接输入授权码)
|
||||
})
|
||||
|
||||
// 监听平台变化
|
||||
watch(
|
||||
() => form.value.platform,
|
||||
(newPlatform) => {
|
||||
// 当选择 CCR 平台时,通知父组件
|
||||
if (!isEdit.value) {
|
||||
emit('platform-changed', newPlatform)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 监听账户类型变化
|
||||
watch(
|
||||
() => form.value.accountType,
|
||||
|
||||
454
web/admin-spa/src/components/accounts/CcrAccountForm.vue
Normal file
454
web/admin-spa/src/components/accounts/CcrAccountForm.vue
Normal file
@@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="show" class="modal fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4">
|
||||
<div
|
||||
class="modal-content custom-scrollbar mx-auto max-h-[90vh] w-full max-w-2xl overflow-y-auto p-4 sm:p-6 md:p-8"
|
||||
>
|
||||
<div class="mb-4 flex items-center justify-between sm:mb-6">
|
||||
<div class="flex items-center gap-2 sm:gap-3">
|
||||
<div
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-teal-500 to-emerald-600 sm:h-10 sm:w-10 sm:rounded-xl"
|
||||
>
|
||||
<i class="fas fa-code-branch text-sm text-white sm:text-base" />
|
||||
</div>
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl">
|
||||
{{ isEdit ? '编辑 CCR 账户' : '添加 CCR 账户' }}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
class="p-1 text-gray-400 transition-colors hover:text-gray-600"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<i class="fas fa-times text-lg sm:text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- 基本信息 -->
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>账户名称 *</label
|
||||
>
|
||||
<input
|
||||
v-model="form.name"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
:class="{ 'border-red-500': errors.name }"
|
||||
placeholder="为账户设置一个易识别的名称"
|
||||
required
|
||||
type="text"
|
||||
/>
|
||||
<p v-if="errors.name" class="mt-1 text-xs text-red-500">{{ errors.name }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>描述 (可选)</label
|
||||
>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
class="form-input w-full resize-none border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
placeholder="账户用途说明..."
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>API URL *</label
|
||||
>
|
||||
<input
|
||||
v-model="form.apiUrl"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
:class="{ 'border-red-500': errors.apiUrl }"
|
||||
placeholder="例如:https://api.example.com/v1/messages"
|
||||
required
|
||||
type="text"
|
||||
/>
|
||||
<p v-if="errors.apiUrl" class="mt-1 text-xs text-red-500">{{ errors.apiUrl }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>API Key {{ isEdit ? '(留空不更新)' : '*' }}</label
|
||||
>
|
||||
<input
|
||||
v-model="form.apiKey"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
:class="{ 'border-red-500': errors.apiKey }"
|
||||
:placeholder="isEdit ? '留空表示不更新' : '必填'"
|
||||
:required="!isEdit"
|
||||
type="password"
|
||||
/>
|
||||
<p v-if="errors.apiKey" class="mt-1 text-xs text-red-500">{{ errors.apiKey }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>优先级</label
|
||||
>
|
||||
<input
|
||||
v-model.number="form.priority"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
max="100"
|
||||
min="1"
|
||||
placeholder="默认50,数字越小优先级越高"
|
||||
type="number"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
建议范围:1-100,数字越小优先级越高
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>自定义 User-Agent (可选)</label
|
||||
>
|
||||
<input
|
||||
v-model="form.userAgent"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
placeholder="留空则透传客户端 User-Agent"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 限流设置 -->
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>限流机制</label
|
||||
>
|
||||
<div class="mb-3">
|
||||
<label class="inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
v-model="enableRateLimit"
|
||||
class="mr-2 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300"
|
||||
>启用限流机制(429 时暂停调度)</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="enableRateLimit">
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>限流时间 (分钟)</label
|
||||
>
|
||||
<input
|
||||
v-model.number="form.rateLimitDuration"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
min="1"
|
||||
placeholder="默认60分钟"
|
||||
type="number"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
账号被限流后暂停调度的时间(分钟)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 额度管理 -->
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>每日额度限制 ($)</label
|
||||
>
|
||||
<input
|
||||
v-model.number="form.dailyQuota"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
min="0"
|
||||
placeholder="0 表示不限制"
|
||||
step="0.01"
|
||||
type="number"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
设置每日使用额度,0 表示不限制
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>额度重置时间</label
|
||||
>
|
||||
<input
|
||||
v-model="form.quotaResetTime"
|
||||
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
|
||||
placeholder="00:00"
|
||||
type="time"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">每日自动重置额度的时间</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型映射表(可选) -->
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>模型映射表 (可选)</label
|
||||
>
|
||||
<div class="mb-3 rounded-lg bg-blue-50 p-3 dark:bg-blue-900/30">
|
||||
<p class="text-xs text-blue-700 dark:text-blue-400">
|
||||
<i class="fas fa-info-circle mr-1" />
|
||||
留空表示支持所有模型且不修改请求。配置映射后,左侧模型会被识别为支持的模型,右侧是实际发送的模型。
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-3 space-y-2">
|
||||
<div
|
||||
v-for="(mapping, index) in modelMappings"
|
||||
:key="index"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<input
|
||||
v-model="mapping.from"
|
||||
class="form-input flex-1 border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="原始模型名称"
|
||||
type="text"
|
||||
/>
|
||||
<i class="fas fa-arrow-right text-gray-400 dark:text-gray-500" />
|
||||
<input
|
||||
v-model="mapping.to"
|
||||
class="form-input flex-1 border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
|
||||
placeholder="映射后的模型名称"
|
||||
type="text"
|
||||
/>
|
||||
<button
|
||||
class="rounded-lg p-2 text-red-500 transition-colors hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||
type="button"
|
||||
@click="removeModelMapping(index)"
|
||||
>
|
||||
<i class="fas fa-trash" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-gray-600 transition-colors hover:border-gray-400 hover:text-gray-700 dark:border-gray-600 dark:text-gray-400 dark:hover:border-gray-500 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
@click="addModelMapping"
|
||||
>
|
||||
<i class="fas fa-plus mr-2" /> 添加模型映射
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 代理配置 -->
|
||||
<div>
|
||||
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>代理设置 (可选)</label
|
||||
>
|
||||
<ProxyConfig v-model="form.proxy" />
|
||||
</div>
|
||||
|
||||
<!-- 操作区 -->
|
||||
<div class="mt-2 flex gap-3">
|
||||
<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"
|
||||
type="button"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary flex-1 px-6 py-3 font-semibold"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
@click="submit"
|
||||
>
|
||||
<div v-if="loading" class="loading-spinner mr-2" />
|
||||
{{ loading ? (isEdit ? '保存中...' : '创建中...') : isEdit ? '保存' : '创建' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { apiClient } from '@/config/api'
|
||||
import { showToast } from '@/utils/toast'
|
||||
import ProxyConfig from '@/components/accounts/ProxyConfig.vue'
|
||||
|
||||
const props = defineProps({
|
||||
account: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'success'])
|
||||
|
||||
const show = ref(true)
|
||||
const isEdit = computed(() => !!props.account)
|
||||
const loading = ref(false)
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
apiUrl: '',
|
||||
apiKey: '',
|
||||
priority: 50,
|
||||
userAgent: '',
|
||||
rateLimitDuration: 60,
|
||||
dailyQuota: 0,
|
||||
quotaResetTime: '00:00',
|
||||
proxy: null,
|
||||
supportedModels: {}
|
||||
})
|
||||
|
||||
const enableRateLimit = ref(true)
|
||||
const errors = ref({})
|
||||
|
||||
const modelMappings = ref([]) // [{from,to}]
|
||||
|
||||
const buildSupportedModels = () => {
|
||||
const map = {}
|
||||
for (const m of modelMappings.value) {
|
||||
const from = (m.from || '').trim()
|
||||
const to = (m.to || '').trim()
|
||||
if (from && to) map[from] = to
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
const addModelMapping = () => {
|
||||
modelMappings.value.push({ from: '', to: '' })
|
||||
}
|
||||
|
||||
const removeModelMapping = (index) => {
|
||||
modelMappings.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
const e = {}
|
||||
if (!form.value.name || form.value.name.trim().length === 0) e.name = '名称不能为空'
|
||||
if (!form.value.apiUrl || form.value.apiUrl.trim().length === 0) e.apiUrl = 'API URL 不能为空'
|
||||
if (!isEdit.value && (!form.value.apiKey || form.value.apiKey.trim().length === 0))
|
||||
e.apiKey = 'API Key 不能为空'
|
||||
errors.value = e
|
||||
return Object.keys(e).length === 0
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if (!validate()) return
|
||||
loading.value = true
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
// 更新
|
||||
const updates = {
|
||||
name: form.value.name,
|
||||
description: form.value.description,
|
||||
apiUrl: form.value.apiUrl,
|
||||
priority: form.value.priority,
|
||||
userAgent: form.value.userAgent,
|
||||
rateLimitDuration: enableRateLimit.value ? Number(form.value.rateLimitDuration || 60) : 0,
|
||||
dailyQuota: Number(form.value.dailyQuota || 0),
|
||||
quotaResetTime: form.value.quotaResetTime || '00:00',
|
||||
proxy: form.value.proxy || null,
|
||||
supportedModels: buildSupportedModels()
|
||||
}
|
||||
if (form.value.apiKey && form.value.apiKey.trim().length > 0) {
|
||||
updates.apiKey = form.value.apiKey
|
||||
}
|
||||
const res = await apiClient.put(`/admin/ccr-accounts/${props.account.id}`, updates)
|
||||
if (res.success) {
|
||||
showToast('保存成功', 'success')
|
||||
emit('success')
|
||||
} else {
|
||||
showToast(res.message || '保存失败', 'error')
|
||||
}
|
||||
} else {
|
||||
// 创建
|
||||
const payload = {
|
||||
name: form.value.name,
|
||||
description: form.value.description,
|
||||
apiUrl: form.value.apiUrl,
|
||||
apiKey: form.value.apiKey,
|
||||
priority: Number(form.value.priority || 50),
|
||||
supportedModels: buildSupportedModels(),
|
||||
userAgent: form.value.userAgent,
|
||||
rateLimitDuration: enableRateLimit.value ? Number(form.value.rateLimitDuration || 60) : 0,
|
||||
proxy: form.value.proxy,
|
||||
accountType: 'shared',
|
||||
dailyQuota: Number(form.value.dailyQuota || 0),
|
||||
quotaResetTime: form.value.quotaResetTime || '00:00'
|
||||
}
|
||||
const res = await apiClient.post('/admin/ccr-accounts', payload)
|
||||
if (res.success) {
|
||||
showToast('创建成功', 'success')
|
||||
emit('success')
|
||||
} else {
|
||||
showToast(res.message || '创建失败', 'error')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || '请求失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const populateFromAccount = () => {
|
||||
if (!props.account) return
|
||||
const a = props.account
|
||||
form.value.name = a.name || ''
|
||||
form.value.description = a.description || ''
|
||||
form.value.apiUrl = a.apiUrl || ''
|
||||
form.value.priority = Number(a.priority || 50)
|
||||
form.value.userAgent = a.userAgent || ''
|
||||
form.value.rateLimitDuration = Number(a.rateLimitDuration || 60)
|
||||
form.value.dailyQuota = Number(a.dailyQuota || 0)
|
||||
form.value.quotaResetTime = a.quotaResetTime || '00:00'
|
||||
form.value.proxy = a.proxy || null
|
||||
enableRateLimit.value = form.value.rateLimitDuration > 0
|
||||
|
||||
// supportedModels 对象转为数组
|
||||
modelMappings.value = []
|
||||
const mapping = a.supportedModels || {}
|
||||
if (mapping && typeof mapping === 'object') {
|
||||
for (const k of Object.keys(mapping)) {
|
||||
modelMappings.value.push({ from: k, to: mapping[k] })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (isEdit.value) populateFromAccount()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.account,
|
||||
() => {
|
||||
if (isEdit.value) populateFromAccount()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-content {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:global(.dark) .modal-content {
|
||||
background: rgba(17, 24, 39, 0.85);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-top: 2px solid #14b8a6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@
|
||||
账户管理
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 sm:text-base">
|
||||
管理您的 Claude、Gemini、OpenAI、Azure OpenAI 和 OpenAI-Responses 账户及代理配置
|
||||
管理您的 Claude、Gemini、OpenAI、Azure OpenAI、OpenAI-Responses 与 CCR 账户及代理配置
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
@@ -376,6 +376,15 @@
|
||||
{{ getClaudeAuthType(account) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="account.platform === 'ccr'"
|
||||
class="flex items-center gap-1.5 rounded-lg border border-teal-200 bg-gradient-to-r from-teal-100 to-emerald-100 px-2.5 py-1 dark:border-teal-700 dark:from-teal-900/20 dark:to-emerald-900/20"
|
||||
>
|
||||
<i class="fas fa-code-branch text-xs text-teal-700 dark:text-teal-400" />
|
||||
<span class="text-xs font-semibold text-teal-800 dark:text-teal-300">CCR</span>
|
||||
<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
|
||||
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"
|
||||
@@ -483,7 +492,8 @@
|
||||
account.platform === 'bedrock' ||
|
||||
account.platform === 'gemini' ||
|
||||
account.platform === 'openai' ||
|
||||
account.platform === 'azure_openai'
|
||||
account.platform === 'azure_openai' ||
|
||||
account.platform === 'ccr'
|
||||
"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
@@ -737,7 +747,9 @@
|
||||
? 'bg-gradient-to-br from-blue-500 to-cyan-600'
|
||||
: account.platform === 'openai'
|
||||
? 'bg-gradient-to-br from-gray-600 to-gray-700'
|
||||
: 'bg-gradient-to-br from-blue-500 to-blue-600'
|
||||
: account.platform === 'ccr'
|
||||
? 'bg-gradient-to-br from-teal-500 to-emerald-600'
|
||||
: 'bg-gradient-to-br from-blue-500 to-blue-600'
|
||||
]"
|
||||
>
|
||||
<i
|
||||
@@ -751,7 +763,9 @@
|
||||
? 'fab fa-microsoft'
|
||||
: account.platform === 'openai'
|
||||
? 'fas fa-openai'
|
||||
: 'fas fa-robot'
|
||||
: account.platform === 'ccr'
|
||||
? 'fas fa-code-branch'
|
||||
: 'fas fa-robot'
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
@@ -946,14 +960,26 @@
|
||||
|
||||
<!-- 添加账户模态框 -->
|
||||
<AccountForm
|
||||
v-if="showCreateAccountModal"
|
||||
@close="showCreateAccountModal = false"
|
||||
v-if="showCreateAccountModal && (!newAccountPlatform || newAccountPlatform !== 'ccr')"
|
||||
@close="closeCreateAccountModal"
|
||||
@platform-changed="newAccountPlatform = $event"
|
||||
@success="handleCreateSuccess"
|
||||
/>
|
||||
<CcrAccountForm
|
||||
v-else-if="showCreateAccountModal && newAccountPlatform === 'ccr'"
|
||||
@close="closeCreateAccountModal"
|
||||
@success="handleCreateSuccess"
|
||||
/>
|
||||
|
||||
<!-- 编辑账户模态框 -->
|
||||
<CcrAccountForm
|
||||
v-if="showEditAccountModal && editingAccount && editingAccount.platform === 'ccr'"
|
||||
:account="editingAccount"
|
||||
@close="showEditAccountModal = false"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
<AccountForm
|
||||
v-if="showEditAccountModal"
|
||||
v-else-if="showEditAccountModal"
|
||||
:account="editingAccount"
|
||||
@close="showEditAccountModal = false"
|
||||
@success="handleEditSuccess"
|
||||
@@ -978,6 +1004,7 @@ import { showToast } from '@/utils/toast'
|
||||
import { apiClient } from '@/config/api'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import AccountForm from '@/components/accounts/AccountForm.vue'
|
||||
import CcrAccountForm from '@/components/accounts/CcrAccountForm.vue'
|
||||
import ConfirmModal from '@/components/common/ConfirmModal.vue'
|
||||
import CustomDropdown from '@/components/common/CustomDropdown.vue'
|
||||
|
||||
@@ -1018,7 +1045,8 @@ const platformOptions = ref([
|
||||
{ value: 'openai', label: 'OpenAi', icon: 'fa-openai' },
|
||||
{ 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: 'openai-responses', label: 'OpenAI-Responses', icon: 'fa-server' },
|
||||
{ value: 'ccr', label: 'CCR', icon: 'fa-code-branch' }
|
||||
])
|
||||
|
||||
const groupOptions = computed(() => {
|
||||
@@ -1043,6 +1071,7 @@ const groupOptions = computed(() => {
|
||||
|
||||
// 模态框状态
|
||||
const showCreateAccountModal = ref(false)
|
||||
const newAccountPlatform = ref(null) // 跟踪新建账户选择的平台
|
||||
const showEditAccountModal = ref(false)
|
||||
const editingAccount = ref(null)
|
||||
|
||||
@@ -1124,7 +1153,8 @@ const loadAccounts = async (forceReload = false) => {
|
||||
apiClient.get('/admin/gemini-accounts', { params }),
|
||||
apiClient.get('/admin/openai-accounts', { params }),
|
||||
apiClient.get('/admin/azure-openai-accounts', { params }),
|
||||
apiClient.get('/admin/openai-responses-accounts', { params })
|
||||
apiClient.get('/admin/openai-responses-accounts', { params }),
|
||||
apiClient.get('/admin/ccr-accounts', { params })
|
||||
)
|
||||
} else {
|
||||
// 只请求指定平台,其他平台设为null占位
|
||||
@@ -1206,6 +1236,17 @@ const loadAccounts = async (forceReload = false) => {
|
||||
apiClient.get('/admin/openai-responses-accounts', { params })
|
||||
)
|
||||
break
|
||||
case 'ccr':
|
||||
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 占位
|
||||
apiClient.get('/admin/ccr-accounts', { params })
|
||||
)
|
||||
break
|
||||
default:
|
||||
// 默认情况下返回空数组
|
||||
requests.push(
|
||||
@@ -1234,7 +1275,8 @@ const loadAccounts = async (forceReload = false) => {
|
||||
geminiData,
|
||||
openaiData,
|
||||
azureOpenaiData,
|
||||
openaiResponsesData
|
||||
openaiResponsesData,
|
||||
ccrData
|
||||
] = await Promise.all(requests)
|
||||
|
||||
const allAccounts = []
|
||||
@@ -1316,6 +1358,15 @@ const loadAccounts = async (forceReload = false) => {
|
||||
allAccounts.push(...openaiResponsesAccounts)
|
||||
}
|
||||
|
||||
// CCR 账户
|
||||
if (ccrData && ccrData.success) {
|
||||
const ccrAccounts = (ccrData.data || []).map((acc) => {
|
||||
// CCR 不支持 API Key 绑定,固定为 0
|
||||
return { ...acc, platform: 'ccr', boundApiKeysCount: 0 }
|
||||
})
|
||||
allAccounts.push(...ccrAccounts)
|
||||
}
|
||||
|
||||
// 根据分组筛选器过滤账户
|
||||
let filteredAccounts = allAccounts
|
||||
if (groupFilter.value !== 'all') {
|
||||
@@ -1521,9 +1572,16 @@ const formatRateLimitTime = (minutes) => {
|
||||
|
||||
// 打开创建账户模态框
|
||||
const openCreateAccountModal = () => {
|
||||
newAccountPlatform.value = null // 重置选择的平台
|
||||
showCreateAccountModal.value = true
|
||||
}
|
||||
|
||||
// 关闭创建账户模态框
|
||||
const closeCreateAccountModal = () => {
|
||||
showCreateAccountModal.value = false
|
||||
newAccountPlatform.value = null
|
||||
}
|
||||
|
||||
// 编辑账户
|
||||
const editAccount = (account) => {
|
||||
editingAccount.value = account
|
||||
@@ -1571,6 +1629,8 @@ const deleteAccount = async (account) => {
|
||||
endpoint = `/admin/azure-openai-accounts/${account.id}`
|
||||
} else if (account.platform === 'openai-responses') {
|
||||
endpoint = `/admin/openai-responses-accounts/${account.id}`
|
||||
} else if (account.platform === 'ccr') {
|
||||
endpoint = `/admin/ccr-accounts/${account.id}`
|
||||
} else {
|
||||
endpoint = `/admin/gemini-accounts/${account.id}`
|
||||
}
|
||||
@@ -1621,6 +1681,8 @@ const resetAccountStatus = async (account) => {
|
||||
endpoint = `/admin/claude-accounts/${account.id}/reset-status`
|
||||
} else if (account.platform === 'claude-console') {
|
||||
endpoint = `/admin/claude-console-accounts/${account.id}/reset-status`
|
||||
} else if (account.platform === 'ccr') {
|
||||
endpoint = `/admin/ccr-accounts/${account.id}/reset-status`
|
||||
} else {
|
||||
showToast('不支持的账户类型', 'error')
|
||||
account.isResetting = false
|
||||
@@ -1665,6 +1727,8 @@ const toggleSchedulable = async (account) => {
|
||||
endpoint = `/admin/azure-openai-accounts/${account.id}/toggle-schedulable`
|
||||
} else if (account.platform === 'openai-responses') {
|
||||
endpoint = `/admin/openai-responses-accounts/${account.id}/toggle-schedulable`
|
||||
} else if (account.platform === 'ccr') {
|
||||
endpoint = `/admin/ccr-accounts/${account.id}/toggle-schedulable`
|
||||
} else {
|
||||
showToast('该账户类型暂不支持调度控制', 'warning')
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user