Files
aiagent/frontend/src/views/ModelConfigs.vue
renjianbo 769dcc828e feat: add Kimi (moonshot) and full provider support in model configs
- 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>
2026-07-26 18:33:51 +08:00

934 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>