feat(account): 账号测试支持选择模型

- 新增 GET /api/v1/admin/accounts/:id/models 接口获取账号可用模型
- 账号测试弹窗新增模型选择下拉框
- 测试时支持传入 model_id 参数,不传则默认使用 Sonnet
- API Key 账号支持根据 model_mapping 映射测试模型
- 将模型常量提取到 claude 包统一管理
This commit is contained in:
shaw
2025-12-19 15:59:39 +08:00
parent 733d4c2b85
commit ee86dbca9d
10 changed files with 212 additions and 40 deletions

View File

@@ -11,6 +11,7 @@ import type {
PaginatedResponse,
AccountUsageInfo,
WindowStats,
ClaudeModel,
} from '@/types';
/**
@@ -247,6 +248,16 @@ export async function setSchedulable(id: number, schedulable: boolean): Promise<
return data;
}
/**
* Get available models for an account
* @param id - Account ID
* @returns List of available models for this account
*/
export async function getAvailableModels(id: number): Promise<ClaudeModel[]> {
const { data } = await apiClient.get<ClaudeModel[]>(`/admin/accounts/${id}/models`);
return data;
}
export const accountsAPI = {
list,
getById,
@@ -262,6 +273,7 @@ export const accountsAPI = {
getTodayStats,
clearRateLimit,
setSchedulable,
getAvailableModels,
generateAuthUrl,
exchangeCode,
batchCreate,

View File

@@ -36,6 +36,23 @@
</span>
</div>
<!-- Model Selection -->
<div class="space-y-1.5">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('admin.accounts.selectTestModel') }}
</label>
<select
v-model="selectedModelId"
:disabled="loadingModels || status === 'connecting'"
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-dark-500 bg-white dark:bg-dark-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
<option v-if="loadingModels" value="">{{ t('common.loading') }}...</option>
<option v-for="model in availableModels" :key="model.id" :value="model.id">
{{ model.display_name }} ({{ model.id }})
</option>
</select>
</div>
<!-- Terminal Output -->
<div class="relative group">
<div
@@ -125,10 +142,10 @@
</button>
<button
@click="startTest"
:disabled="status === 'connecting'"
:disabled="status === 'connecting' || !selectedModelId"
:class="[
'px-4 py-2 text-sm font-medium rounded-lg transition-all flex items-center gap-2',
status === 'connecting'
status === 'connecting' || !selectedModelId
? 'bg-primary-400 text-white cursor-not-allowed'
: status === 'success'
? 'bg-green-500 hover:bg-green-600 text-white'
@@ -161,7 +178,8 @@
import { ref, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import Modal from '@/components/common/Modal.vue'
import type { Account } from '@/types'
import { adminAPI } from '@/api/admin'
import type { Account, ClaudeModel } from '@/types'
const { t } = useI18n()
@@ -184,17 +202,44 @@ const status = ref<'idle' | 'connecting' | 'success' | 'error'>('idle')
const outputLines = ref<OutputLine[]>([])
const streamingContent = ref('')
const errorMessage = ref('')
const availableModels = ref<ClaudeModel[]>([])
const selectedModelId = ref('')
const loadingModels = ref(false)
let eventSource: EventSource | null = null
// Reset state when modal opens
watch(() => props.show, (newVal) => {
if (newVal) {
// Load available models when modal opens
watch(() => props.show, async (newVal) => {
if (newVal && props.account) {
resetState()
await loadAvailableModels()
} else {
closeEventSource()
}
})
const loadAvailableModels = async () => {
if (!props.account) return
loadingModels.value = true
selectedModelId.value = '' // Reset selection before loading
try {
availableModels.value = await adminAPI.accounts.getAvailableModels(props.account.id)
// Default to first model (usually Sonnet)
if (availableModels.value.length > 0) {
// Try to select Sonnet as default, otherwise use first model
const sonnetModel = availableModels.value.find(m => m.id.includes('sonnet'))
selectedModelId.value = sonnetModel?.id || availableModels.value[0].id
}
} catch (error) {
console.error('Failed to load available models:', error)
// Fallback to empty list
availableModels.value = []
selectedModelId.value = ''
} finally {
loadingModels.value = false
}
}
const resetState = () => {
status.value = 'idle'
outputLines.value = []
@@ -227,7 +272,7 @@ const scrollToBottom = async () => {
}
const startTest = async () => {
if (!props.account) return
if (!props.account || !selectedModelId.value) return
resetState()
status.value = 'connecting'
@@ -247,7 +292,8 @@ const startTest = async () => {
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
'Content-Type': 'application/json'
}
},
body: JSON.stringify({ model_id: selectedModelId.value })
})
if (!response.ok) {

View File

@@ -777,6 +777,7 @@ export default {
copyOutput: 'Copy output',
startingTestForAccount: 'Starting test for account: {name}',
testAccountTypeLabel: 'Account type: {type}',
selectTestModel: 'Select Test Model',
testModel: 'claude-sonnet-4-5-20250929',
testPrompt: 'Prompt: "hi"',
},

View File

@@ -865,6 +865,7 @@ export default {
copyOutput: '复制输出',
startingTestForAccount: '开始测试账号:{name}',
testAccountTypeLabel: '账号类型:{type}',
selectTestModel: '选择测试模型',
testModel: 'claude-sonnet-4-5-20250929',
testPrompt: '提示词:"hi"',
},

View File

@@ -285,6 +285,14 @@ export type AccountType = 'oauth' | 'setup-token' | 'apikey';
export type OAuthAddMethod = 'oauth' | 'setup-token';
export type ProxyProtocol = 'http' | 'https' | 'socks5';
// Claude Model type (returned by /v1/models and account models API)
export interface ClaudeModel {
id: string;
type: string;
display_name: string;
created_at: string;
}
export interface Proxy {
id: number;
name: string;