- backend: expand valid_providers from 4 to full frontend provider list (module-level VALID_PROVIDERS constant, used by create + update) - backend: test endpoint gains generic OpenAI-compatible branch (moonshot/qwen/zhipu/siliconflow etc. via AsyncOpenAI direct call) - frontend: Kimi preset models add kimi-k2-0905/0711/turbo + kimi-latest alongside legacy moonshot-v1 series Verified: moonshot config create 201 (was 400), test endpoint reaches api.moonshot.cn and returns provider auth error with dummy key. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
934 lines
36 KiB
Vue
934 lines
36 KiB
Vue
<template>
|
||
<MainLayout>
|
||
<div class="model-configs-page">
|
||
<el-card>
|
||
<template #header>
|
||
<div class="card-header">
|
||
<h2>模型配置管理</h2>
|
||
<el-button type="primary" @click="handleCreate">
|
||
<el-icon><Plus /></el-icon>
|
||
创建模型配置
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 搜索和筛选 -->
|
||
<div class="filters">
|
||
<el-select
|
||
v-model="providerFilter"
|
||
placeholder="提供商筛选"
|
||
style="width: 200px"
|
||
clearable
|
||
@change="handleSearch"
|
||
>
|
||
<el-option label="DeepSeek" value="deepseek" />
|
||
<el-option label="OpenAI" value="openai" />
|
||
<el-option label="Anthropic" value="anthropic" />
|
||
<el-option label="阿里通义千问" value="qwen" />
|
||
<el-option label="智谱 GLM" value="zhipu" />
|
||
<el-option label="百度文心一言" value="baidu" />
|
||
<el-option label="月之暗面 Kimi" value="moonshot" />
|
||
<el-option label="字节豆包" value="bytedance" />
|
||
<el-option label="MiniMax" value="minimax" />
|
||
<el-option label="硅基流动" value="siliconflow" />
|
||
<el-option label="讯飞星火" value="xunfei" />
|
||
<el-option label="腾讯混元" value="hunyuan" />
|
||
<el-option label="零一万物" value="yi" />
|
||
<el-option label="百川" value="baichuan" />
|
||
<el-option label="OpenRouter" value="openrouter" />
|
||
<el-option label="Cohere" value="cohere" />
|
||
<el-option label="xAI" value="xai" />
|
||
<el-option label="Together" value="together" />
|
||
<el-option label="Fireworks" value="fireworks" />
|
||
<el-option label="Perplexity" value="perplexity" />
|
||
<el-option label="DeepInfra" value="deepinfra" />
|
||
<el-option label="Groq" value="groq" />
|
||
<el-option label="Google Gemini" value="google" />
|
||
<el-option label="Mistral AI" value="mistral" />
|
||
<el-option label="本地/其他" value="local" />
|
||
</el-select>
|
||
<el-button type="primary" @click="handleSearch" style="margin-left: 10px">
|
||
<el-icon><Search /></el-icon>
|
||
搜索
|
||
</el-button>
|
||
<el-button @click="handleRefresh">
|
||
<el-icon><Refresh /></el-icon>
|
||
刷新
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 模型配置列表 -->
|
||
<el-table
|
||
v-loading="modelConfigStore.loading"
|
||
:data="modelConfigStore.modelConfigs"
|
||
style="width: 100%; margin-top: 20px"
|
||
stripe
|
||
>
|
||
<el-table-column prop="name" label="名称" width="200" />
|
||
<el-table-column prop="provider" label="提供商" width="120">
|
||
<template #default="{ row }">
|
||
<el-tag>{{ providerDisplay(row.provider) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="model_name" label="模型名称" width="200" />
|
||
<el-table-column prop="base_url" label="API地址" show-overflow-tooltip />
|
||
<el-table-column prop="created_at" label="创建时间" width="180">
|
||
<template #default="{ row }">
|
||
{{ formatDate(row.created_at) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="250" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button link type="primary" @click="handleEdit(row)">
|
||
<el-icon><Edit /></el-icon>
|
||
编辑
|
||
</el-button>
|
||
<el-button link type="success" @click="handleTest(row)">
|
||
<el-icon><Connection /></el-icon>
|
||
测试
|
||
</el-button>
|
||
<el-button link type="danger" @click="handleDelete(row)">
|
||
<el-icon><Delete /></el-icon>
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- 分页 -->
|
||
<div class="pagination">
|
||
<el-pagination
|
||
v-model:current-page="currentPage"
|
||
v-model:page-size="pageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
:total="total"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@size-change="handlePageChange"
|
||
@current-change="handlePageChange"
|
||
/>
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- 创建/编辑对话框 -->
|
||
<el-dialog
|
||
v-model="dialogVisible"
|
||
:title="dialogTitle"
|
||
width="720px"
|
||
@close="handleDialogClose"
|
||
>
|
||
<!-- 编辑模式:单一表单 -->
|
||
<template v-if="currentConfigId">
|
||
<el-form :model="form" label-width="120px" :rules="rules" ref="formRef">
|
||
<el-form-item label="名称" prop="name">
|
||
<el-input v-model="form.name" placeholder="可选,区分个人/公司用时填写" />
|
||
</el-form-item>
|
||
<el-form-item label="提供商" prop="provider">
|
||
<el-select v-model="form.provider" placeholder="选择提供商">
|
||
<el-option label="DeepSeek" value="deepseek" />
|
||
<el-option label="OpenAI" value="openai" />
|
||
<el-option label="Anthropic" value="anthropic" />
|
||
<el-option label="阿里通义千问" value="qwen" />
|
||
<el-option label="智谱 GLM" value="zhipu" />
|
||
<el-option label="百度文心一言" value="baidu" />
|
||
<el-option label="月之暗面 Kimi" value="moonshot" />
|
||
<el-option label="字节豆包" value="bytedance" />
|
||
<el-option label="MiniMax" value="minimax" />
|
||
<el-option label="硅基流动" value="siliconflow" />
|
||
<el-option label="讯飞星火" value="xunfei" />
|
||
<el-option label="腾讯混元" value="hunyuan" />
|
||
<el-option label="零一万物" value="yi" />
|
||
<el-option label="百川" value="baichuan" />
|
||
<el-option label="OpenRouter" value="openrouter" />
|
||
<el-option label="Cohere" value="cohere" />
|
||
<el-option label="xAI" value="xai" />
|
||
<el-option label="Together" value="together" />
|
||
<el-option label="Fireworks" value="fireworks" />
|
||
<el-option label="Perplexity" value="perplexity" />
|
||
<el-option label="DeepInfra" value="deepinfra" />
|
||
<el-option label="Groq" value="groq" />
|
||
<el-option label="Google Gemini" value="google" />
|
||
<el-option label="Mistral AI" value="mistral" />
|
||
<el-option label="本地/其他" value="local" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="模型名称" prop="model_name">
|
||
<el-input v-model="form.model_name" placeholder="模型名称" />
|
||
</el-form-item>
|
||
<el-form-item label="API密钥">
|
||
<el-input v-model="form.api_key" type="password" show-password placeholder="留空则不修改" />
|
||
</el-form-item>
|
||
<el-form-item label="API地址">
|
||
<el-input v-model="form.base_url" placeholder="可选" />
|
||
</el-form-item>
|
||
</el-form>
|
||
</template>
|
||
|
||
<!-- 创建模式:全量模型选择面板 -->
|
||
<template v-else>
|
||
<div class="create-panel">
|
||
<div class="create-hint">
|
||
<el-icon><InfoFilled /></el-icon>
|
||
勾选需要的模型,填写对应提供商的 API 密钥。名称自动生成,可选填前缀以区分。
|
||
</div>
|
||
|
||
<el-form label-width="80px" class="name-prefix-row">
|
||
<el-form-item label="名称前缀">
|
||
<el-input v-model="customNamePrefix" placeholder="可选,如:公司、我的" style="width: 200px" />
|
||
<span class="prefix-hint">留空则自动命名为 "DeepSeek deepseek-chat" 等</span>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<!-- 按提供商分组 -->
|
||
<div v-for="group in modelGroups" :key="group.provider" class="provider-group">
|
||
<div class="provider-header">
|
||
<span class="provider-name">{{ group.label }}</span>
|
||
<span class="provider-count">{{ group.models.filter(m => m.checked).length }}/{{ group.models.length }} 个模型</span>
|
||
</div>
|
||
|
||
<!-- 模型选择网格 -->
|
||
<div class="model-check-grid">
|
||
<el-checkbox
|
||
v-for="model in group.models"
|
||
:key="model.name"
|
||
v-model="model.checked"
|
||
:label="model.name"
|
||
class="model-check-item"
|
||
>
|
||
<span class="model-name">{{ model.name }}</span>
|
||
<span class="model-desc" v-if="model.desc">{{ model.desc }}</span>
|
||
<span class="model-price" v-if="model.price">{{ model.price }}</span>
|
||
</el-checkbox>
|
||
</div>
|
||
|
||
<!-- 该提供商的 API 配置 -->
|
||
<div v-if="group.models.filter(m => m.checked).length > 0" class="provider-config">
|
||
<el-row :gutter="16">
|
||
<el-col :span="12">
|
||
<el-form-item label="API 密钥" :label-width="80" size="small">
|
||
<el-input v-model="group.apiKey" type="password" show-password placeholder="sk-..." size="small" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="地址" :label-width="50" size="small">
|
||
<el-input v-model="group.baseUrl" placeholder="默认地址" size="small" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 统计 -->
|
||
<div class="create-summary" v-if="totalSelected > 0">
|
||
<el-icon><Select /></el-icon>
|
||
共选择 <b>{{ totalSelected }}</b> 个模型,将为它们创建配置
|
||
</div>
|
||
<el-empty v-else description="未选择任何模型" :image-size="48" />
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 统一 Footer -->
|
||
<template #footer>
|
||
<template v-if="currentConfigId">
|
||
<el-button @click="dialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
|
||
</template>
|
||
<template v-else>
|
||
<el-button @click="dialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="handleBatchCreate" :loading="submitting" :disabled="totalSelected === 0">
|
||
创建 {{ totalSelected }} 个配置
|
||
</el-button>
|
||
</template>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 测试对话框 -->
|
||
<el-dialog v-model="testDialogVisible" title="测试模型配置" width="500px">
|
||
<div v-if="testing">
|
||
<el-icon class="is-loading"><Loading /></el-icon>
|
||
正在测试模型配置...
|
||
</div>
|
||
<div v-else-if="testResult">
|
||
<el-alert
|
||
:type="testResult.status === 'success' ? 'success' : 'error'"
|
||
:title="testResult.message"
|
||
:closable="false"
|
||
/>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</MainLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, computed } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import MainLayout from '@/components/MainLayout.vue'
|
||
import {
|
||
Plus,
|
||
Search,
|
||
Refresh,
|
||
Edit,
|
||
Delete,
|
||
Connection,
|
||
Loading,
|
||
InfoFilled,
|
||
Select
|
||
} from '@element-plus/icons-vue'
|
||
import { useModelConfigStore } from '@/stores/modelConfig'
|
||
import type { ModelConfig } from '@/stores/modelConfig'
|
||
|
||
const modelConfigStore = useModelConfigStore()
|
||
|
||
// 搜索和筛选
|
||
const providerFilter = ref('')
|
||
const currentPage = ref(1)
|
||
const pageSize = ref(20)
|
||
const total = ref(0)
|
||
|
||
// 对话框
|
||
const dialogVisible = ref(false)
|
||
const dialogTitle = ref('创建模型配置')
|
||
const formRef = ref()
|
||
const submitting = ref(false)
|
||
const form = ref({
|
||
name: '',
|
||
provider: 'openai',
|
||
model_name: '',
|
||
api_key: '',
|
||
base_url: ''
|
||
})
|
||
const currentConfigId = ref<string | null>(null)
|
||
const customNamePrefix = ref('')
|
||
|
||
// 全量模型列表,按提供商分组
|
||
interface ModelItem { name: string; desc?: string; price?: string; checked: boolean }
|
||
interface ProviderGroup { provider: string; label: string; defaultBaseUrl: string; models: ModelItem[]; apiKey: string; baseUrl: string }
|
||
|
||
function createModelGroups(): ProviderGroup[] {
|
||
return [
|
||
{
|
||
provider: 'deepseek', label: 'DeepSeek', defaultBaseUrl: 'https://api.deepseek.com/v1',
|
||
models: [
|
||
{ name: 'deepseek-chat', desc: '通用对话', price: '¥1/2', checked: false },
|
||
{ name: 'deepseek-coder', desc: '代码专用', price: '¥1/2', checked: false },
|
||
{ name: 'deepseek-reasoner', desc: '深度推理', price: '¥1/4', checked: false },
|
||
{ name: 'deepseek-v4-pro', desc: '最新旗舰', price: '¥1/4', checked: false },
|
||
{ name: 'deepseek-v4-flash', desc: '极速响应', price: '¥0.5/1', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'openai', label: 'OpenAI', defaultBaseUrl: 'https://api.openai.com/v1',
|
||
models: [
|
||
{ name: 'gpt-4o', desc: '最新多模态', price: '$2.5/10', checked: false },
|
||
{ name: 'gpt-4o-mini', desc: '轻量快速', price: '$0.15/0.6', checked: false },
|
||
{ name: 'gpt-4-turbo', desc: 'Turbo 版', price: '$10/30', checked: false },
|
||
{ name: 'o1', desc: '深度推理', price: '$15/60', checked: false },
|
||
{ name: 'o1-mini', desc: '轻量推理', price: '$1.1/4.4', checked: false },
|
||
{ name: 'o3-mini', desc: '最新推理', price: '$0.55/2.2', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'anthropic', label: 'Anthropic', defaultBaseUrl: 'https://api.anthropic.com',
|
||
models: [
|
||
{ name: 'claude-sonnet-4-6', desc: '最新 Sonnet', price: '$3/15', checked: false },
|
||
{ name: 'claude-opus-4-6', desc: '最新 Opus', price: '$15/75', checked: false },
|
||
{ name: 'claude-haiku-4-5-20251001', desc: '最快速度', price: '$0.8/4', checked: false },
|
||
{ name: 'claude-3-5-sonnet-latest', desc: '稳定版 Sonnet', price: '$3/15', checked: false },
|
||
{ name: 'claude-3-opus-latest', desc: '稳定版 Opus', price: '$15/75', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'qwen', label: '阿里通义千问', defaultBaseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||
models: [
|
||
{ name: 'qwen-turbo', desc: '通用轻量', price: '¥0.8/0.8', checked: false },
|
||
{ name: 'qwen-plus', desc: '均衡性能', price: '¥2/2', checked: false },
|
||
{ name: 'qwen-max', desc: '最强旗舰', price: '¥20/20', checked: false },
|
||
{ name: 'qwen-long', desc: '超长上下文', price: '¥4/4', checked: false },
|
||
{ name: 'qwen-coder-plus', desc: '代码专用', price: '¥4/4', checked: false },
|
||
{ name: 'qwq-plus', desc: '深度推理', price: '¥4/4', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'zhipu', label: '智谱 GLM', defaultBaseUrl: 'https://open.bigmodel.cn/api/paas/v4',
|
||
models: [
|
||
{ name: 'glm-4-flash', desc: '极速免费', price: '免费', checked: false },
|
||
{ name: 'glm-4-air', desc: '轻量高性价比', price: '¥0.1/0.1', checked: false },
|
||
{ name: 'glm-4-plus', desc: '最强旗舰', price: '¥5/5', checked: false },
|
||
{ name: 'glm-4-long', desc: '超长上下文', price: '¥5/5', checked: false },
|
||
{ name: 'glm-4.5', desc: '最新升级', price: '¥1/1', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'baidu', label: '百度文心一言', defaultBaseUrl: 'https://qianfan.baidubce.com/v2',
|
||
models: [
|
||
{ name: 'ernie-speed-128k', desc: '极速大容量', price: '免费', checked: false },
|
||
{ name: 'ernie-3.5-128k', desc: '均衡通用', price: '¥0.8/0.8', checked: false },
|
||
{ name: 'ernie-4.0-turbo-128k', desc: '旗舰推理', price: '¥30/30', checked: false },
|
||
{ name: 'ernie-4.5', desc: '最新升级', price: '¥4/4', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'moonshot', label: '月之暗面 Kimi', defaultBaseUrl: 'https://api.moonshot.cn/v1',
|
||
models: [
|
||
{ name: 'kimi-k2-0905-preview', desc: 'K2 增强版', price: '¥4/16', checked: false },
|
||
{ name: 'kimi-k2-0711-preview', desc: 'K2 旗舰', price: '¥4/16', checked: false },
|
||
{ name: 'kimi-k2-turbo-preview', desc: 'K2 高速版', price: '¥8/32', checked: false },
|
||
{ name: 'kimi-latest', desc: '自动指向最新', price: '¥4/16', checked: false },
|
||
{ name: 'moonshot-v1-8k', desc: '标准长度', price: '¥0.15/0.15', checked: false },
|
||
{ name: 'moonshot-v1-32k', desc: '中长文本', price: '¥0.3/0.3', checked: false },
|
||
{ name: 'moonshot-v1-128k', desc: '超长文本', price: '¥0.6/0.6', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'bytedance', label: '字节豆包', defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
|
||
models: [
|
||
{ name: 'doubao-lite-128k', desc: '轻量快速', price: '¥0.3/0.3', checked: false },
|
||
{ name: 'doubao-pro-256k', desc: '主力旗舰', price: '¥0.8/0.8', checked: false },
|
||
{ name: 'doubao-pro-32k', desc: '高并发版', price: '¥0.8/0.8', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'minimax', label: 'MiniMax', defaultBaseUrl: 'https://api.minimax.chat/v1',
|
||
models: [
|
||
{ name: 'abab6.5s-chat', desc: '轻量标准', price: '¥0.3/0.3', checked: false },
|
||
{ name: 'abab6.5t-chat', desc: '深度推理', price: '¥1/1', checked: false },
|
||
{ name: 'abab7-chat-preview', desc: '最新旗舰', price: '¥1/1', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'xunfei', label: '讯飞星火', defaultBaseUrl: 'https://spark-api-open.xf-yun.com/v1',
|
||
models: [
|
||
{ name: '4.0Ultra', desc: '最强旗舰', price: '¥15/15', checked: false },
|
||
{ name: '3.5Max', desc: '高性价比', price: '¥3/3', checked: false },
|
||
{ name: 'lite', desc: '极速轻量', price: '免费', checked: false },
|
||
{ name: 'pro-128k', desc: '超长上下文', price: '¥3/3', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'hunyuan', label: '腾讯混元', defaultBaseUrl: 'https://api.hunyuan.cloud.tencent.com/v1',
|
||
models: [
|
||
{ name: 'hunyuan-turbo', desc: '通用Turbo', price: '¥0.8/0.8', checked: false },
|
||
{ name: 'hunyuan-pro', desc: '旗舰Pro', price: '¥2/2', checked: false },
|
||
{ name: 'hunyuan-lite', desc: '免费轻量', price: '免费', checked: false },
|
||
{ name: 'hunyuan-code', desc: '代码专用', price: '¥2/2', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'yi', label: '零一万物 Yi', defaultBaseUrl: 'https://api.lingyiwanwu.com/v1',
|
||
models: [
|
||
{ name: 'yi-large', desc: '旗舰大模型', price: '¥20/20', checked: false },
|
||
{ name: 'yi-medium', desc: '均衡中杯', price: '¥4/4', checked: false },
|
||
{ name: 'yi-lightning', desc: '极速闪电', price: '¥0.8/0.8', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'baichuan', label: '百川智能', defaultBaseUrl: 'https://api.baichuan-ai.com/v1',
|
||
models: [
|
||
{ name: 'baichuan4', desc: '最新旗舰', price: '¥10/10', checked: false },
|
||
{ name: 'baichuan3-turbo', desc: '极速Turbo', price: '¥1/1', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'cohere', label: 'Cohere', defaultBaseUrl: 'https://api.cohere.ai/v1',
|
||
models: [
|
||
{ name: 'command-r-plus', desc: '最强旗舰', price: '$2.5/10', checked: false },
|
||
{ name: 'command-r', desc: '高性价比', price: '$0.5/1.5', checked: false },
|
||
{ name: 'command-a', desc: '最新多模态', price: '$1/4', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'xai', label: 'xAI (Grok)', defaultBaseUrl: 'https://api.x.ai/v1',
|
||
models: [
|
||
{ name: 'grok-3', desc: '最新旗舰', price: '$3/15', checked: false },
|
||
{ name: 'grok-3-mini', desc: '极速轻量', price: '$0.3/1.5', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'together', label: 'Together AI', defaultBaseUrl: 'https://api.together.xyz/v1',
|
||
models: [
|
||
{ name: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', desc: 'Llama 3.3 70B', price: '$0.88/0.88', checked: false },
|
||
{ name: 'deepseek-ai/DeepSeek-V3', desc: 'DeepSeek V3', price: '$1.25/1.25', checked: false },
|
||
{ name: 'Qwen/Qwen2.5-72B-Instruct-Turbo', desc: '千问2.5 72B', price: '$0.88/0.88', checked: false },
|
||
{ name: 'mistralai/Mixtral-8x22B-Instruct-v0.1', desc: 'Mixtral 8x22B', price: '$1.2/1.2', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'fireworks', label: 'Fireworks AI', defaultBaseUrl: 'https://api.fireworks.ai/inference/v1',
|
||
models: [
|
||
{ name: 'accounts/fireworks/models/llama-v3p3-70b-instruct', desc: 'Llama 3.3 70B', price: '$0.9/0.9', checked: false },
|
||
{ name: 'accounts/fireworks/models/deepseek-v3', desc: 'DeepSeek V3', price: '$0.9/0.9', checked: false },
|
||
{ name: 'accounts/fireworks/models/qwen2p5-72b-instruct', desc: '千问2.5 72B', price: '$0.9/0.9', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'perplexity', label: 'Perplexity', defaultBaseUrl: 'https://api.perplexity.ai',
|
||
models: [
|
||
{ name: 'sonar-reasoning-pro', desc: '深度推理', price: '$5/15', checked: false },
|
||
{ name: 'sonar-pro', desc: '专业搜索', price: '$3/15', checked: false },
|
||
{ name: 'sonar', desc: '轻量搜索', price: '$1/1', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'deepinfra', label: 'DeepInfra', defaultBaseUrl: 'https://api.deepinfra.com/v1/openai',
|
||
models: [
|
||
{ name: 'meta-llama/Llama-3.3-70B-Instruct', desc: 'Llama 3.3 70B', price: '$0.35/0.35', checked: false },
|
||
{ name: 'deepseek-ai/DeepSeek-V3', desc: 'DeepSeek V3', price: '$0.89/0.89', checked: false },
|
||
{ name: 'Qwen/Qwen2.5-72B-Instruct', desc: '千问2.5 72B', price: '$0.35/0.35', checked: false },
|
||
{ name: 'microsoft/WizardLM-2-8x22B', desc: 'WizardLM 8x22B', price: '$0.5/0.5', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'groq', label: 'Groq (极速推理)', defaultBaseUrl: 'https://api.groq.com/openai/v1',
|
||
models: [
|
||
{ name: 'llama-3.3-70b-versatile', desc: 'Llama 3.3 70B', price: '$0.59/0.79', checked: false },
|
||
{ name: 'llama-3.1-8b-instant', desc: 'Llama 3.1 8B', price: '$0.05/0.08', checked: false },
|
||
{ name: 'mixtral-8x7b-32768', desc: 'Mixtral 8x7B', price: '$0.24/0.24', checked: false },
|
||
{ name: 'gemma2-9b-it', desc: 'Gemma 2 9B', price: '$0.2/0.2', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'google', label: 'Google Gemini', defaultBaseUrl: 'https://generativelanguage.googleapis.com/v1beta',
|
||
models: [
|
||
{ name: 'gemini-2.5-flash', desc: '极速响应', price: '$0.15/0.6', checked: false },
|
||
{ name: 'gemini-2.5-pro', desc: '最强旗舰', price: '$1.25/5', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'openrouter', label: 'OpenRouter (代理)', defaultBaseUrl: 'https://openrouter.ai/api/v1',
|
||
models: [
|
||
// Claude 系列
|
||
{ name: 'anthropic/claude-sonnet-4-6', desc: 'Claude Sonnet 4.6', price: '$3/15', checked: false },
|
||
{ name: 'anthropic/claude-opus-4-6', desc: 'Claude Opus 4.6', price: '$15/75', checked: false },
|
||
{ name: 'anthropic/claude-3.5-sonnet', desc: 'Claude 3.5 Sonnet', price: '$3/15', checked: false },
|
||
// GPT 系列
|
||
{ name: 'openai/gpt-4o', desc: 'GPT-4o 多模态', price: '$2.5/10', checked: false },
|
||
{ name: 'openai/gpt-4o-mini', desc: 'GPT-4o Mini', price: '$0.15/0.6', checked: false },
|
||
{ name: 'openai/o3-mini', desc: 'o3-mini 推理', price: '$0.55/2.2', checked: false },
|
||
{ name: 'openai/o1', desc: 'o1 深度推理', price: '$15/60', checked: false },
|
||
// Gemini 系列
|
||
{ name: 'google/gemini-2.5-pro', desc: 'Gemini 2.5 Pro', price: '$1.25/5', checked: false },
|
||
{ name: 'google/gemini-2.5-flash', desc: 'Gemini 2.5 Flash', price: '$0.15/0.6', checked: false },
|
||
// 开源旗舰
|
||
{ name: 'meta-llama/llama-4-maverick', desc: 'Llama 4 Maverick', price: '$0.2/0.6', checked: false },
|
||
{ name: 'deepseek/deepseek-r1', desc: 'DeepSeek R1', price: '$0.55/2.19', checked: false },
|
||
{ name: 'deepseek/deepseek-chat', desc: 'DeepSeek V3', price: '$0.89/0.89', checked: false },
|
||
{ name: 'qwen/qwen3-235b-a22b', desc: '千问3 235B', price: '$0.3/0.4', checked: false },
|
||
{ name: 'mistralai/mistral-large', desc: 'Mistral Large', price: '$2/6', checked: false },
|
||
{ name: 'cohere/command-r-plus', desc: 'Command R+', price: '$2.5/10', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'siliconflow', label: '硅基流动', defaultBaseUrl: 'https://api.siliconflow.cn/v1',
|
||
models: [
|
||
// 国产精选
|
||
{ name: 'deepseek-ai/DeepSeek-V3.2', desc: 'DeepSeek 最新旗舰', price: '¥1.33/1.33', checked: false },
|
||
{ name: 'deepseek-ai/DeepSeek-R1', desc: 'DeepSeek R1 推理', price: '¥4/16', checked: false },
|
||
{ name: 'Pro/zai-org/GLM-4.7', desc: 'GLM-4.7 最新', price: '¥1.33/1.33', checked: false },
|
||
{ name: 'Qwen/Qwen3-235B-A22B-Thinking', desc: '千问3 超大推理', price: '¥2.8/2.8', checked: false },
|
||
{ name: 'Qwen/Qwen2.5-72B-Instruct', desc: '千问2.5 72B', price: '¥1.33/1.33', checked: false },
|
||
{ name: 'Qwen/Qwen2.5-Coder-32B-Instruct', desc: '千问代码 32B', price: '¥1.33/1.33', checked: false },
|
||
// 国外开源旗舰
|
||
{ name: 'meta-llama/Meta-Llama-3.1-405B-Instruct', desc: 'Llama 3.1 405B 最强开源', price: '¥4/4', checked: false },
|
||
{ name: 'meta-llama/Llama-3.3-70B-Instruct', desc: 'Llama 3.3 70B', price: '¥1.33/1.33', checked: false },
|
||
{ name: 'meta-llama/Llama-3.2-90B-Vision-Instruct', desc: 'Llama 3.2 90B 多模态', price: '¥1.33/1.33', checked: false },
|
||
{ name: 'mistralai/Mixtral-8x22B-Instruct-v0.1', desc: 'Mixtral 8x22B MoE', price: '¥1.33/1.33', checked: false },
|
||
{ name: 'mistralai/Mixtral-8x7B-Instruct-v0.1', desc: 'Mixtral 8x7B MoE', price: '¥1.33/1.33', checked: false },
|
||
{ name: 'google/gemma-2-27b-it', desc: 'Gemma 2 27B Google', price: '¥1.33/1.33', checked: false },
|
||
{ name: 'google/gemma-2-9b-it', desc: 'Gemma 2 9B 免费', price: '免费', checked: false },
|
||
{ name: 'meta-llama/Meta-Llama-3.1-70B-Instruct', desc: 'Llama 3.1 70B', price: '¥1.33/1.33', checked: false },
|
||
{ name: 'meta-llama/Meta-Llama-3.1-8B-Instruct', desc: 'Llama 3.1 8B 轻量', price: '¥1.33/1.33', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
{
|
||
provider: 'mistral', label: 'Mistral AI', defaultBaseUrl: 'https://api.mistral.ai/v1',
|
||
models: [
|
||
{ name: 'mistral-small', desc: '轻量快速', price: '$0.2/0.6', checked: false },
|
||
{ name: 'mistral-large', desc: '旗舰模型', price: '$2/6', checked: false },
|
||
{ name: 'codestral', desc: '代码专用', price: '$0.3/0.9', checked: false },
|
||
],
|
||
apiKey: '', baseUrl: ''
|
||
},
|
||
]
|
||
}
|
||
|
||
const modelGroups = ref<ProviderGroup[]>(createModelGroups())
|
||
|
||
const totalSelected = computed(() => {
|
||
let n = 0
|
||
for (const g of modelGroups.value) n += g.models.filter(m => m.checked).length
|
||
return n
|
||
})
|
||
|
||
const providerDisplayMap: Record<string, string> = {
|
||
deepseek: 'DeepSeek', openai: 'OpenAI', anthropic: 'Anthropic',
|
||
qwen: '通义千问', zhipu: '智谱 GLM', baidu: '文心一言',
|
||
moonshot: 'Kimi', bytedance: '豆包', minimax: 'MiniMax',
|
||
openrouter: 'OpenRouter', siliconflow: '硅基流动', xunfei: '讯飞星火', hunyuan: '腾讯混元',
|
||
yi: '零一万物', baichuan: '百川',
|
||
cohere: 'Cohere', xai: 'xAI', together: 'Together', fireworks: 'Fireworks',
|
||
perplexity: 'Perplexity', deepinfra: 'DeepInfra', groq: 'Groq',
|
||
google: 'Gemini', mistral: 'Mistral', local: '本地',
|
||
}
|
||
function providerDisplay(p: string) {
|
||
return providerDisplayMap[p] || p.toUpperCase()
|
||
}
|
||
|
||
// 测试
|
||
const testDialogVisible = ref(false)
|
||
const testing = ref(false)
|
||
const testResult = ref<any>(null)
|
||
const currentTestConfig = ref<ModelConfig | null>(null)
|
||
|
||
|
||
// 表单验证规则(仅编辑模式使用)
|
||
const rules = {
|
||
provider: [
|
||
{ required: true, message: '请选择提供商', trigger: 'change' }
|
||
],
|
||
model_name: [
|
||
{ required: true, message: '请输入模型名称', trigger: 'blur' }
|
||
]
|
||
}
|
||
|
||
// 格式化日期
|
||
const formatDate = (dateStr: string) => {
|
||
if (!dateStr) return ''
|
||
const date = new Date(dateStr)
|
||
return date.toLocaleString('zh-CN')
|
||
}
|
||
|
||
// 搜索
|
||
const handleSearch = async () => {
|
||
currentPage.value = 1
|
||
await loadModelConfigs()
|
||
}
|
||
|
||
// 刷新
|
||
const handleRefresh = async () => {
|
||
providerFilter.value = ''
|
||
currentPage.value = 1
|
||
await loadModelConfigs()
|
||
}
|
||
|
||
// 加载模型配置列表
|
||
const loadModelConfigs = async () => {
|
||
try {
|
||
const configs = await modelConfigStore.fetchModelConfigs({
|
||
provider: providerFilter.value || undefined,
|
||
skip: (currentPage.value - 1) * pageSize.value,
|
||
limit: pageSize.value
|
||
})
|
||
total.value = configs.length
|
||
} catch (error: any) {
|
||
ElMessage.error(error.response?.data?.detail || '加载模型配置列表失败')
|
||
}
|
||
}
|
||
|
||
// 创建
|
||
const handleCreate = () => {
|
||
dialogTitle.value = '创建模型配置'
|
||
currentConfigId.value = null
|
||
customNamePrefix.value = ''
|
||
modelGroups.value = createModelGroups()
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
// 编辑
|
||
const handleEdit = (config: ModelConfig) => {
|
||
dialogTitle.value = '编辑模型配置'
|
||
currentConfigId.value = config.id
|
||
form.value = {
|
||
name: config.name,
|
||
provider: config.provider,
|
||
model_name: config.model_name,
|
||
api_key: '', // 不显示已有密钥
|
||
base_url: config.base_url || ''
|
||
}
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
// 测试
|
||
const handleTest = async (config: ModelConfig) => {
|
||
currentTestConfig.value = config
|
||
testDialogVisible.value = true
|
||
testing.value = true
|
||
testResult.value = null
|
||
|
||
try {
|
||
const result = await modelConfigStore.testModelConfig(config.id)
|
||
testResult.value = result
|
||
if (result.status === 'success') {
|
||
ElMessage.success('模型配置测试成功')
|
||
} else {
|
||
ElMessage.error(result.message || '模型配置测试失败')
|
||
}
|
||
} catch (error: any) {
|
||
testResult.value = {
|
||
status: 'error',
|
||
message: error.response?.data?.detail || '模型配置测试失败'
|
||
}
|
||
ElMessage.error(testResult.value.message)
|
||
} finally {
|
||
testing.value = false
|
||
}
|
||
}
|
||
|
||
// 删除
|
||
const handleDelete = async (config: ModelConfig) => {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确定要删除模型配置 "${config.name}" 吗?此操作不可恢复。`,
|
||
'确认删除',
|
||
{
|
||
type: 'error'
|
||
}
|
||
)
|
||
await modelConfigStore.deleteModelConfig(config.id)
|
||
ElMessage.success('删除成功')
|
||
await loadModelConfigs()
|
||
} catch (error: any) {
|
||
if (error !== 'cancel') {
|
||
ElMessage.error(error.response?.data?.detail || '删除失败')
|
||
}
|
||
}
|
||
}
|
||
|
||
// 批量创建
|
||
const handleBatchCreate = async () => {
|
||
if (totalSelected.value === 0) {
|
||
ElMessage.warning('请至少选择一个模型')
|
||
return
|
||
}
|
||
submitting.value = true
|
||
let created = 0
|
||
try {
|
||
const prefix = customNamePrefix.value.trim()
|
||
for (const group of modelGroups.value) {
|
||
const selected = group.models.filter(m => m.checked)
|
||
if (selected.length === 0) continue
|
||
const apiKey = group.apiKey.trim()
|
||
if (!apiKey) {
|
||
ElMessage.warning(`请填写 ${group.label} 的 API 密钥`)
|
||
submitting.value = false
|
||
return
|
||
}
|
||
const baseUrl = group.baseUrl.trim() || group.defaultBaseUrl
|
||
for (const model of selected) {
|
||
const name = prefix ? `${prefix} ${group.label} ${model.name}` : `${group.label} ${model.name}`
|
||
await modelConfigStore.createModelConfig({
|
||
name,
|
||
provider: group.provider,
|
||
model_name: model.name,
|
||
api_key: apiKey,
|
||
base_url: baseUrl,
|
||
})
|
||
created++
|
||
}
|
||
}
|
||
ElMessage.success(`成功创建 ${created} 个模型配置`)
|
||
dialogVisible.value = false
|
||
await loadModelConfigs()
|
||
} catch (error: any) {
|
||
ElMessage.error(error.response?.data?.detail || '批量创建失败')
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
// 提交表单(编辑模式)
|
||
const handleSubmit = async () => {
|
||
if (!formRef.value) return
|
||
|
||
await formRef.value.validate(async (valid: boolean) => {
|
||
if (!valid) return
|
||
|
||
submitting.value = true
|
||
try {
|
||
const updateData: any = {
|
||
name: form.value.name,
|
||
provider: form.value.provider,
|
||
model_name: form.value.model_name,
|
||
base_url: form.value.base_url || null
|
||
}
|
||
if (form.value.api_key) {
|
||
updateData.api_key = form.value.api_key
|
||
}
|
||
await modelConfigStore.updateModelConfig(currentConfigId.value!, updateData)
|
||
ElMessage.success('更新成功')
|
||
dialogVisible.value = false
|
||
await loadModelConfigs()
|
||
} catch (error: any) {
|
||
ElMessage.error(error.response?.data?.detail || '操作失败')
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
// 对话框关闭
|
||
const handleDialogClose = () => {
|
||
formRef.value?.resetFields()
|
||
currentConfigId.value = null
|
||
}
|
||
|
||
// 分页变化
|
||
const handlePageChange = () => {
|
||
loadModelConfigs()
|
||
}
|
||
|
||
// 初始化
|
||
onMounted(() => {
|
||
loadModelConfigs()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.model-configs-page {
|
||
padding: 20px;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.card-header h2 {
|
||
margin: 0;
|
||
}
|
||
|
||
.filters {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.pagination {
|
||
margin-top: 20px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
/* ── 创建面板 ── */
|
||
.create-panel {
|
||
max-height: 60vh;
|
||
overflow-y: auto;
|
||
}
|
||
.create-hint {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 10px 14px;
|
||
background: #f0f7ff;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
color: #409eff;
|
||
margin-bottom: 16px;
|
||
}
|
||
.name-prefix-row {
|
||
margin-bottom: 8px;
|
||
}
|
||
.prefix-hint {
|
||
margin-left: 12px;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
.provider-group {
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.provider-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
.provider-name {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: #303133;
|
||
}
|
||
.provider-count {
|
||
font-size: 13px;
|
||
color: #909399;
|
||
}
|
||
.model-check-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
}
|
||
.model-check-item {
|
||
margin-right: 0 !important;
|
||
padding: 6px 12px;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 6px;
|
||
background: #fafafa;
|
||
transition: all 0.2s;
|
||
}
|
||
.model-check-item:hover {
|
||
border-color: #409eff;
|
||
background: #f0f7ff;
|
||
}
|
||
.model-check-item.is-checked {
|
||
border-color: #409eff;
|
||
background: #ecf5ff;
|
||
}
|
||
.model-name {
|
||
font-weight: 500;
|
||
font-size: 13px;
|
||
font-family: monospace;
|
||
}
|
||
.model-desc {
|
||
margin-left: 6px;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
.model-price {
|
||
margin-left: 6px;
|
||
font-size: 11px;
|
||
color: #e6a23c;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
}
|
||
.provider-config {
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px dashed #dcdfe6;
|
||
}
|
||
.create-summary {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 12px 16px;
|
||
background: #ecf5ff;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
color: #409eff;
|
||
}
|
||
</style>
|