Files
aiagent/frontend/src/views/ModelConfigs.vue

934 lines
36 KiB
Vue
Raw Normal View History

2026-01-19 00:09:36 +08:00
<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" />
2026-01-19 00:09:36 +08:00
<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" />
2026-01-19 00:09:36 +08:00
</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>
2026-01-19 00:09:36 +08:00
</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"
2026-01-19 00:09:36 +08:00
@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 -->
2026-01-19 00:09:36 +08:00
<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>
2026-01-19 00:09:36 +08:00
</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'
2026-01-19 00:09:36 +08:00
import { ElMessage, ElMessageBox } from 'element-plus'
import MainLayout from '@/components/MainLayout.vue'
import {
Plus,
Search,
Refresh,
Edit,
Delete,
Connection,
Loading,
InfoFilled,
Select
2026-01-19 00:09:36 +08:00
} 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()
}
2026-01-19 00:09:36 +08:00
// 测试
const testDialogVisible = ref(false)
const testing = ref(false)
const testResult = ref<any>(null)
const currentTestConfig = ref<ModelConfig | null>(null)
// 表单验证规则(仅编辑模式使用)
2026-01-19 00:09:36 +08:00
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()
2026-01-19 00:09:36 +08:00
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
}
}
// 提交表单(编辑模式)
2026-01-19 00:09:36 +08:00
const handleSubmit = async () => {
if (!formRef.value) return
2026-01-19 00:09:36 +08:00
await formRef.value.validate(async (valid: boolean) => {
if (!valid) return
2026-01-19 00:09:36 +08:00
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
2026-01-19 00:09:36 +08:00
}
if (form.value.api_key) {
updateData.api_key = form.value.api_key
}
await modelConfigStore.updateModelConfig(currentConfigId.value!, updateData)
ElMessage.success('更新成功')
2026-01-19 00:09:36 +08:00
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;
}
2026-01-19 00:09:36 +08:00
</style>