|
|
|
@ -1,139 +1,339 @@ |
|
|
|
<template> |
|
|
|
<div class="model-provider-manager"> |
|
|
|
<h2 style="margin-bottom: 16px;">模型供应商管理</h2> |
|
|
|
<h2 style="margin-bottom: 16px;">模型管理</h2> |
|
|
|
|
|
|
|
<div style="margin-bottom: 16px; display: flex; gap: 12px;"> |
|
|
|
<el-button type="primary" @click="openProviderDialog()">添加供应商</el-button> |
|
|
|
<!-- 操作栏 --> |
|
|
|
<div style="margin-bottom: 16px; display: flex; gap: 12px; align-items: center;"> |
|
|
|
<el-button type="primary" @click="openAddModelDialog()"> |
|
|
|
<el-icon><Plus /></el-icon> 添加模型 |
|
|
|
</el-button> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div v-if="!selectedProvider"> |
|
|
|
<el-table :data="providers" v-loading="loading" style="width: 100%"> |
|
|
|
<el-table-column prop="name" label="供应商名称" width="180" /> |
|
|
|
<el-table-column prop="provider_type" label="类型" width="120"> |
|
|
|
<template #default="{ row }"> |
|
|
|
<el-tag size="small">{{ row.provider_type }}</el-tag> |
|
|
|
<!-- 按模型类型 Tab 切换 --> |
|
|
|
<el-tabs v-model="activeType" @tab-change="loadModels"> |
|
|
|
<el-tab-pane name="all"> |
|
|
|
<template #label> |
|
|
|
<span>全部模型 <el-badge :value="allModels.length" :max="99" v-if="allModels.length > 0" /></span> |
|
|
|
</template> |
|
|
|
</el-tab-pane> |
|
|
|
<el-tab-pane name="llm"> |
|
|
|
<template #label> |
|
|
|
<span>🤖 LLM 大语言模型 <el-badge :value="models.filter(m => m.model_type === 'llm').length" :max="99" v-if="models.filter(m => m.model_type === 'llm').length > 0" /></span> |
|
|
|
</template> |
|
|
|
</el-tab-pane> |
|
|
|
<el-tab-pane name="embedding"> |
|
|
|
<template #label> |
|
|
|
<span>📐 Embedding 向量嵌入 <el-badge :value="models.filter(m => m.model_type === 'embedding').length" :max="99" v-if="models.filter(m => m.model_type === 'embedding').length > 0" /></span> |
|
|
|
</template> |
|
|
|
</el-tab-pane> |
|
|
|
<el-tab-pane name="rerank"> |
|
|
|
<template #label> |
|
|
|
<span>🔄 Rerank 重排序 <el-badge :value="models.filter(m => m.model_type === 'rerank').length" :max="99" v-if="models.filter(m => m.model_type === 'rerank').length > 0" /></span> |
|
|
|
</template> |
|
|
|
</el-tab-pane> |
|
|
|
</el-tabs> |
|
|
|
|
|
|
|
<!-- 模型列表 --> |
|
|
|
<el-table :data="displayModels" v-loading="loading" style="width: 100%"> |
|
|
|
<el-table-column label="模型信息" min-width="280"> |
|
|
|
<template #default="{ row }"> |
|
|
|
<div style="display: flex; align-items: center; gap: 10px;"> |
|
|
|
<el-tag :type="modelTagType(row.model_type)" size="small">{{ modelTypeName(row.model_type) }}</el-tag> |
|
|
|
<div> |
|
|
|
<div style="font-weight: 500;">{{ row.display_name || row.model_name }}</div> |
|
|
|
<code style="font-size: 11px; color: #909399; background: #f5f7fa; padding: 1px 6px; border-radius: 3px;">{{ row.model_name }}</code> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
<el-table-column label="所属供应商" width="160"> |
|
|
|
<template #default="{ row }"> |
|
|
|
<span>{{ getProviderName(row.provider_id) }}</span> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
<el-table-column label="配置参数" min-width="260"> |
|
|
|
<template #default="{ row }"> |
|
|
|
<div v-if="row.model_type === 'llm'" style="display: flex; gap: 6px; flex-wrap: wrap;"> |
|
|
|
<el-tag v-if="(row.default_params || {}).temperature !== undefined && (row.default_params || {}).temperature !== ''" size="small" effect="plain">T={{ (row.default_params || {}).temperature }}</el-tag> |
|
|
|
<el-tag v-if="(row.default_params || {}).max_tokens" size="small" effect="plain">MaxTok={{ (row.default_params || {}).max_tokens }}</el-tag> |
|
|
|
<el-tag v-if="(row.capabilities || {}).vision" size="small" type="primary" effect="plain">Vision</el-tag> |
|
|
|
<el-tag v-if="(row.capabilities || {}).function_calling" size="small" type="primary" effect="plain">FC</el-tag> |
|
|
|
<el-tag v-if="row.is_default" size="small" type="success" effect="plain">默认</el-tag> |
|
|
|
</div> |
|
|
|
<div v-else-if="row.model_type === 'embedding'" style="display: flex; gap: 6px; flex-wrap: wrap;"> |
|
|
|
<el-tag v-if="(row.default_params || {}).dimension" size="small" effect="plain">Dim={{ (row.default_params || {}).dimension }}</el-tag> |
|
|
|
<el-tag v-if="(row.default_params || {}).max_tokens" size="small" effect="plain">Chunk={{ (row.default_params || {}).max_tokens }}</el-tag> |
|
|
|
<el-tag v-if="row.is_default" size="small" type="success" effect="plain">默认</el-tag> |
|
|
|
</div> |
|
|
|
<div v-else-if="row.model_type === 'rerank'" style="display: flex; gap: 6px; flex-wrap: wrap;"> |
|
|
|
<el-tag v-if="(row.default_params || {}).dimension" size="small" effect="plain">Dim={{ (row.default_params || {}).dimension }}</el-tag> |
|
|
|
<el-tag v-if="(row.default_params || {}).max_tokens" size="small" effect="plain">MaxTok={{ (row.default_params || {}).max_tokens }}</el-tag> |
|
|
|
<el-tag v-if="row.is_default" size="small" type="success" effect="plain">默认</el-tag> |
|
|
|
</div> |
|
|
|
<span v-else style="color: #c0c4cc; font-size: 12px;">-</span> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
<el-table-column prop="is_active" label="状态" width="80"> |
|
|
|
<template #default="{ row }"> |
|
|
|
<el-tag :type="row.is_active ? 'success' : 'danger'" size="small">{{ row.is_active ? '启用' : '禁用' }}</el-tag> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
<el-table-column label="操作" width="180"> |
|
|
|
<template #default="{ row }"> |
|
|
|
<el-button size="small" @click="openEditModelDialog(row)">编辑</el-button> |
|
|
|
<el-button size="small" type="danger" @click="deleteModel(row)">删除</el-button> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
</el-table> |
|
|
|
|
|
|
|
<!-- ========== 添加/编辑模型弹窗(Dify 风格:先选类型,再填配置)========== --> |
|
|
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="680px" :close-on-click-modal="false"> |
|
|
|
<!-- 步骤 1:选择模型类型 --> |
|
|
|
<div v-if="step === 1" style="padding: 20px 0;"> |
|
|
|
<h4 style="margin-bottom: 20px;">请选择要添加的模型类型:</h4> |
|
|
|
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px;"> |
|
|
|
<div |
|
|
|
v-for="t in modelTypes" |
|
|
|
:key="t.value" |
|
|
|
class="type-card" |
|
|
|
:class="{ selected: form.model_type === t.value }" |
|
|
|
@click="selectModelType(t.value)" |
|
|
|
> |
|
|
|
<div class="type-icon">{{ t.icon }}</div> |
|
|
|
<div class="type-name">{{ t.name }}</div> |
|
|
|
<div class="type-desc">{{ t.desc }}</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 步骤 2:填写模型配置(根据类型动态切换表单) --> |
|
|
|
<div v-if="step === 2"> |
|
|
|
<!-- 返回类型选择 --> |
|
|
|
<el-button text type="primary" @click="step = 1" style="margin-bottom: 16px;"> |
|
|
|
← 返回重新选择类型 |
|
|
|
</el-button> |
|
|
|
|
|
|
|
<el-form :model="form" label-width="130px"> |
|
|
|
|
|
|
|
<!-- 公共字段 --> |
|
|
|
<el-form-item label="所属供应商" required> |
|
|
|
<el-select v-model="form.provider_id" placeholder="请选择供应商" style="width: 100%"> |
|
|
|
<el-option v-for="p in providers" :key="p.id" :label="p.name" :value="p.id" /> |
|
|
|
</el-select> |
|
|
|
</el-form-item> |
|
|
|
|
|
|
|
<el-form-item label="模型标识名" required> |
|
|
|
<el-input v-model="form.model_name" :placeholder="currentTypePlaceholder.name"> |
|
|
|
<template #append> |
|
|
|
<el-tooltip content="与 API 调用时使用的模型名称一致,如 gpt-4o、text-embedding-3-small"> |
|
|
|
<el-icon><InfoFilled /></el-icon> |
|
|
|
</el-tooltip> |
|
|
|
</template> |
|
|
|
</el-input> |
|
|
|
</el-form-item> |
|
|
|
|
|
|
|
<el-form-item label="显示名称"> |
|
|
|
<el-input v-model="form.display_name" :placeholder="currentTypePlaceholder.displayName" /> |
|
|
|
</el-form-item> |
|
|
|
|
|
|
|
<el-divider /> |
|
|
|
|
|
|
|
<!-- ====== LLM 专用配置 ====== --> |
|
|
|
<template v-if="form.model_type === 'llm'"> |
|
|
|
<el-divider content-position="left"> |
|
|
|
<el-icon><Cpu /></el-icon> LLM 参数配置 |
|
|
|
</el-divider> |
|
|
|
|
|
|
|
<el-row :gutter="16"> |
|
|
|
<el-col :span="12"> |
|
|
|
<el-form-item label="Temperature"> |
|
|
|
<el-input-number v-model="llmForm.temperature" :min="0" :max="2" :step="0.1" :precision="2" controls-position="right" style="width: 100%" /> |
|
|
|
<div style="font-size: 11px; color: #909399; margin-top: 2px;">控制随机性,值越高输出越多样</div> |
|
|
|
</el-form-item> |
|
|
|
</el-col> |
|
|
|
<el-col :span="12"> |
|
|
|
<el-form-item label="Max Tokens"> |
|
|
|
<el-input-number v-model="llmForm.max_tokens" :min="1" :max="200000" :step="256" controls-position="right" style="width: 100%" /> |
|
|
|
<div style="font-size: 11px; color: #909399; margin-top: 2px;">最大输出 Token 数</div> |
|
|
|
</el-form-item> |
|
|
|
</el-col> |
|
|
|
</el-row> |
|
|
|
|
|
|
|
<el-row :gutter="16"> |
|
|
|
<el-col :span="12"> |
|
|
|
<el-form-item label="Top P"> |
|
|
|
<el-input-number v-model="llmForm.top_p" :min="0" :max="1" :step="0.05" :precision="2" controls-position="right" style="width: 100%" /> |
|
|
|
</el-form-item> |
|
|
|
</el-col> |
|
|
|
<el-col :span="12"> |
|
|
|
<el-form-item label="Frequency Penalty"> |
|
|
|
<el-input-number v-model="llmForm.frequency_penalty" :min="-2" :max="2" :step="0.1" :precision="2" controls-position="right" style="width: 100%" /> |
|
|
|
</el-form-item> |
|
|
|
</el-col> |
|
|
|
</el-row> |
|
|
|
|
|
|
|
<el-form-item label="能力支持"> |
|
|
|
<div style="padding: 12px; background: #f0f9ff; border-radius: 8px;"> |
|
|
|
<el-checkbox v-model="llmForm.vision">👁 支持视觉理解 (Vision / 多模态)</el-checkbox> |
|
|
|
<el-checkbox v-model="llmForm.function_calling">⚡ 支持函数调用 (Function Calling / Tool Use)</el-checkbox> |
|
|
|
<el-checkbox v-model="llmForm.streaming">🔄 支持流式输出 (Streaming)</el-checkbox> |
|
|
|
</div> |
|
|
|
</el-form-item> |
|
|
|
|
|
|
|
<el-form-item label="高级参数 (JSON)"> |
|
|
|
<el-input v-model="llmForm.extraParamsJson" type="textarea" :rows="3" placeholder='{"stop": ["\n\n"], "presence_penalty": 0}' /> |
|
|
|
</el-form-item> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
<el-table-column prop="base_url" label="API 端点" show-overflow-tooltip /> |
|
|
|
<el-table-column prop="is_active" label="状态" width="80"> |
|
|
|
<template #default="{ row }"> |
|
|
|
<el-tag :type="row.is_active ? 'success' : 'info'" size="small"> |
|
|
|
{{ row.is_active ? '启用' : '禁用' }} |
|
|
|
</el-tag> |
|
|
|
|
|
|
|
<!-- ====== Embedding 专用配置 ====== --> |
|
|
|
<template v-if="form.model_type === 'embedding'"> |
|
|
|
<el-divider content-position="left"> |
|
|
|
<el-icon><Grid /></el-icon> Embedding 向量参数 |
|
|
|
</el-divider> |
|
|
|
|
|
|
|
<el-row :gutter="16"> |
|
|
|
<el-col :span="12"> |
|
|
|
<el-form-item label="向量维度" required> |
|
|
|
<el-select v-model="embForm.dimension" style="width: 100%" filterable allow-create> |
|
|
|
<el-option label="1536 (OpenAI Ada/Text-Embedding)" value="1536" /> |
|
|
|
<el-option label="768 (OpenAI Small)" value="768" /> |
|
|
|
<el-option label="1024 (BGE Large)" value="1024" /> |
|
|
|
<el-option label="384 (BGE Small)" value="384" /> |
|
|
|
<el-option label="2048 (Cohere / 其他)" value="2048" /> |
|
|
|
<el-option label="4096 (自定义大维度)" value="4096" /> |
|
|
|
</el-select> |
|
|
|
<div style="font-size: 11px; color: #909399; margin-top: 2px;">输出向量的维度大小</div> |
|
|
|
</el-form-item> |
|
|
|
</el-col> |
|
|
|
<el-col :span="12"> |
|
|
|
<el-form-item label="最大分块 Token 数"> |
|
|
|
<el-input-number v-model="embForm.max_chunk_tokens" :min="128" :max="8192" :step="64" controls-position="right" style="width: 100%" /> |
|
|
|
<div style="font-size: 11px; color: #909399; margin-top: 2px;">单次文本分块的最大 Token 数</div> |
|
|
|
</el-form-item> |
|
|
|
</el-col> |
|
|
|
</el-row> |
|
|
|
|
|
|
|
<el-form-item label="高级参数 (JSON)"> |
|
|
|
<el-input v-model="embForm.extraParamsJson" type="textarea" :rows="3" placeholder='{"batch_size": 32, "normalize": true}' /> |
|
|
|
</el-form-item> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
<el-table-column label="操作" width="280"> |
|
|
|
<template #default="{ row }"> |
|
|
|
<el-button size="small" @click="selectProvider(row)">管理模型</el-button> |
|
|
|
<el-button size="small" @click="openProviderDialog(row)">编辑</el-button> |
|
|
|
<el-button size="small" type="danger" @click="deleteProvider(row.id)">删除</el-button> |
|
|
|
|
|
|
|
<!-- ====== Rerank 专用配置 ====== --> |
|
|
|
<template v-if="form.model_type === 'rerank'"> |
|
|
|
<el-divider content-position="left"> |
|
|
|
<el-icon><Sort /></el-icon> Rerank 重排序参数 |
|
|
|
</el-divider> |
|
|
|
|
|
|
|
<el-row :gutter="16"> |
|
|
|
<el-col :span="12"> |
|
|
|
<el-form-item label="向量维度" required> |
|
|
|
<el-select v-model="rerankForm.dimension" style="width: 100%" filterable allow-create> |
|
|
|
<el-option label="1024 (BGE Reranker V2 M3)" value="1024" /> |
|
|
|
<el-option label="768 (BGE Reranker Base)" value="768" /> |
|
|
|
<el-option label="384 (BGE Reranker Small)" value="384" /> |
|
|
|
<el-option label="256 (MiniLM Reranker)" value="256" /> |
|
|
|
<el-option label="4096 (Cohere Rerank)" value="4096" /> |
|
|
|
</el-select> |
|
|
|
<div style="font-size: 11px; color: #909399; margin-top: 2px;">查询向量与文档向量的维度</div> |
|
|
|
</el-form-item> |
|
|
|
</el-col> |
|
|
|
<el-col :span="12"> |
|
|
|
<el-form-item label="最大 Token 数"> |
|
|
|
<el-input-number v-model="rerankForm.max_tokens" :min="512" :max="8192" :step="256" controls-position="right" style="width: 100%" /> |
|
|
|
<div style="font-size: 11px; color: #909399; margin-top: 2px;">输入文本的最大长度</div> |
|
|
|
</el-form-item> |
|
|
|
</el-col> |
|
|
|
</el-row> |
|
|
|
|
|
|
|
<el-form-item label="评分方式"> |
|
|
|
<el-radio-group v-model="rerankForm.score_method"> |
|
|
|
<el-radio value="cosine">余弦相似度 (Cosine Similarity)</el-radio> |
|
|
|
<el-radio value="dotproduct">点积 (Dot Product)</el-radio> |
|
|
|
</el-radio-group> |
|
|
|
</el-form-item> |
|
|
|
|
|
|
|
<el-form-item label="高级参数 (JSON)"> |
|
|
|
<el-input v-model="rerankForm.extraParamsJson" type="textarea" :rows="3" placeholder='{"return_documents": true, "top_k": 5}' /> |
|
|
|
</el-form-item> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
</el-table> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div v-else> |
|
|
|
<div style="margin-bottom: 12px; display: flex; gap: 12px; align-items: center;"> |
|
|
|
<el-button @click="selectedProvider = null">← 返回供应商列表</el-button> |
|
|
|
<span style="font-size: 16px; font-weight: bold;">{{ selectedProvider.name }} - 模型管理</span> |
|
|
|
<el-button type="primary" size="small" @click="openModelDialog()">添加模型</el-button> |
|
|
|
<!-- 公共底部字段 --> |
|
|
|
<el-divider /> |
|
|
|
<el-form-item label="设为默认模型"> |
|
|
|
<el-switch v-model="form.is_default" /> |
|
|
|
<span style="margin-left: 8px; color: #999; font-size: 12px;">设为 {{ currentTypeName }} 类型的默认调用模型</span> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="启用状态"> |
|
|
|
<el-switch v-model="form.is_active" active-text="启用" inactive-text="禁用" /> |
|
|
|
</el-form-item> |
|
|
|
</el-form> |
|
|
|
</div> |
|
|
|
|
|
|
|
<el-tabs v-model="modelTab"> |
|
|
|
<el-tab-pane label="LLM" name="llm" /> |
|
|
|
<el-tab-pane label="Embedding" name="embedding" /> |
|
|
|
<el-tab-pane label="Rerank" name="rerank" /> |
|
|
|
</el-tabs> |
|
|
|
<template #footer> |
|
|
|
<el-button @click="dialogVisible = false">取消</el-button> |
|
|
|
<el-button v-if="step === 1" type="primary" @click="goToStep2" :disabled="!form.model_type">下一步</el-button> |
|
|
|
<el-button v-else type="primary" @click="saveModel" :loading="saving">{{ editingId ? '保存修改' : '添加模型' }}</el-button> |
|
|
|
</template> |
|
|
|
</el-dialog> |
|
|
|
|
|
|
|
<el-table :data="filteredModels" v-loading="modelLoading" style="width: 100%"> |
|
|
|
<el-table-column prop="model_name" label="模型名" width="200" /> |
|
|
|
<el-table-column prop="display_name" label="显示名称" width="200" /> |
|
|
|
<el-table-column prop="model_type" label="类型" width="100"> |
|
|
|
<!-- ========== 供应商管理抽屉(次要功能)========== --> |
|
|
|
<el-drawer v-model="providerDrawerVisible" title="供应商管理" direction="rtl" size="420px"> |
|
|
|
<template #header> |
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center;"> |
|
|
|
<span>供应商管理</span> |
|
|
|
<el-button type="primary" size="small" @click="openProviderDialog()">添加供应商</el-button> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
<el-table :data="providers" v-loading="providerLoading" size="small"> |
|
|
|
<el-table-column prop="name" label="名称" /> |
|
|
|
<el-table-column prop="provider_type" label="类型" width="120"> |
|
|
|
<template #default="{ row }"> |
|
|
|
<el-tag size="small">{{ row.model_type }}</el-tag> |
|
|
|
<el-tag :type="providerTagType(row.provider_type)" size="small">{{ providerTagName(row.provider_type) }}</el-tag> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
<el-table-column prop="is_default" label="默认" width="80"> |
|
|
|
<el-table-column prop="base_url" label="API 端点" show-overflow-tooltip /> |
|
|
|
<el-table-column prop="is_active" label="状态" width="70"> |
|
|
|
<template #default="{ row }"> |
|
|
|
<el-tag :type="row.is_default ? 'success' : 'info'" size="small"> |
|
|
|
{{ row.is_default ? '是' : '否' }} |
|
|
|
</el-tag> |
|
|
|
<el-tag :type="row.is_active ? 'success' : 'info'" size="small">{{ row.is_active ? '启' : '停' }}</el-tag> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
<el-table-column label="操作" width="180"> |
|
|
|
<el-table-column label="操作" width="120"> |
|
|
|
<template #default="{ row }"> |
|
|
|
<el-button size="small" @click="openModelDialog(row)">编辑</el-button> |
|
|
|
<el-button size="small" type="danger" @click="deleteModel(row.id)">删除</el-button> |
|
|
|
<el-button size="small" link @click="openProviderDialog(row)">编辑</el-button> |
|
|
|
<el-button size="small" link type="danger" @click="deleteProvider(row.id)">删</el-button> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
</el-table> |
|
|
|
</div> |
|
|
|
</el-drawer> |
|
|
|
|
|
|
|
<el-dialog v-model="providerDialogVisible" :title="editingProviderId ? '编辑供应商' : '添加供应商'" width="550px"> |
|
|
|
<el-form :model="providerForm" label-width="100px"> |
|
|
|
<el-form-item label="供应商名称" required> |
|
|
|
<el-input v-model="providerForm.name" placeholder="如 OpenAI / 智谱AI / 本地Ollama" /> |
|
|
|
<!-- 供应商表单弹窗 --> |
|
|
|
<el-dialog v-model="providerDialogVisible" :title="editingProviderId ? '编辑供应商' : '添加供应商'" width="520px"> |
|
|
|
<el-form :model="providerForm" label-width="90px"> |
|
|
|
<el-form-item label="名称" required> |
|
|
|
<el-input v-model="providerForm.name" placeholder="如 OpenAI、智谱AI、Ollama 本地" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="类型" required> |
|
|
|
<el-form-item label="接入类型" required> |
|
|
|
<el-select v-model="providerForm.provider_type" style="width: 100%"> |
|
|
|
<el-option label="OpenAI兼容" value="openai_compatible" /> |
|
|
|
<el-option label="OpenAI" value="openai" /> |
|
|
|
<el-option label="智谱AI" value="zhipu" /> |
|
|
|
<el-option label="Ollama" value="ollama" /> |
|
|
|
<el-option label="OpenAI 兼容接口" value="openai_compatible" /> |
|
|
|
<el-option label="OpenAI 官方" value="openai" /> |
|
|
|
<el-option label="智谱 AI (Zhipu)" value="zhipu" /> |
|
|
|
<el-option label="Ollama 本地部署" value="ollama" /> |
|
|
|
<el-option label="DeepSeek" value="deepseek" /> |
|
|
|
</el-select> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="API 端点"> |
|
|
|
<el-input v-model="providerForm.base_url" placeholder="如 https://api.openai.com/v1" /> |
|
|
|
<el-input v-model="providerForm.base_url" placeholder="https://api.openai.com/v1" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="API Key"> |
|
|
|
<el-input v-model="providerForm.api_key" type="password" placeholder="密钥(加密存储)" show-password /> |
|
|
|
<el-input v-model="providerForm.api_key" type="password" show-password placeholder="密钥(加密存储)" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="启用"> |
|
|
|
<el-switch v-model="providerForm.is_active" /> |
|
|
|
<el-switch v-model="providerForm.is_active" active-text="启用" inactive-text="禁用" /> |
|
|
|
</el-form-item> |
|
|
|
</el-form> |
|
|
|
<template #footer> |
|
|
|
<el-button @click="providerDialogVisible = false">取消</el-button> |
|
|
|
<el-button type="primary" @click="saveProvider" :loading="saving">保存</el-button> |
|
|
|
</template> |
|
|
|
</el-dialog> |
|
|
|
|
|
|
|
<el-dialog v-model="modelDialogVisible" :title="editingModelId ? '编辑模型' : '添加模型'" width="550px"> |
|
|
|
<el-form :model="modelForm" label-width="100px"> |
|
|
|
<el-form-item label="模型类型" required> |
|
|
|
<el-select v-model="modelForm.model_type" style="width: 100%" :disabled="!!editingModelId"> |
|
|
|
<el-option label="LLM(大语言模型)" value="llm" /> |
|
|
|
<el-option label="Embedding(嵌入模型)" value="embedding" /> |
|
|
|
<el-option label="Rerank(重排序模型)" value="rerank" /> |
|
|
|
</el-select> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="模型名" required> |
|
|
|
<el-input v-model="modelForm.model_name" placeholder="如 gpt-4o / text-embedding-3-small" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="显示名称"> |
|
|
|
<el-input v-model="modelForm.display_name" placeholder="如 GPT-4o" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="设为默认"> |
|
|
|
<el-switch v-model="modelForm.is_default" /> |
|
|
|
<span style="margin-left: 8px; color: #999; font-size: 12px;">该类型下的默认模型</span> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="能力配置" v-if="modelForm.model_type === 'llm'"> |
|
|
|
<div style="width: 100%;"> |
|
|
|
<el-checkbox v-model="llmVision" style="margin-right: 16px;">支持 Vision</el-checkbox> |
|
|
|
<el-checkbox v-model="llmFunctionCalling">支持 Function Calling</el-checkbox> |
|
|
|
</div> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="默认参数"> |
|
|
|
<el-input v-model="defaultParamsJson" type="textarea" :rows="3" placeholder='{"temperature": 0.7, "max_tokens": 4096}' /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="启用"> |
|
|
|
<el-switch v-model="modelForm.is_active" /> |
|
|
|
</el-form-item> |
|
|
|
</el-form> |
|
|
|
<template #footer> |
|
|
|
<el-button @click="modelDialogVisible = false">取消</el-button> |
|
|
|
<el-button type="primary" @click="saveModel" :loading="modelSaving">保存</el-button> |
|
|
|
<el-button type="primary" @click="saveProvider" :loading="providerSaving">保存</el-button> |
|
|
|
</template> |
|
|
|
</el-dialog> |
|
|
|
</div> |
|
|
|
@ -142,14 +342,70 @@ |
|
|
|
<script setup lang="ts"> |
|
|
|
import { ref, reactive, computed, onMounted } from 'vue' |
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus' |
|
|
|
import { Plus, InfoFilled, Cpu, Grid, Sort } from '@element-plus/icons-vue' |
|
|
|
import api from '@/api' |
|
|
|
|
|
|
|
const providers = ref<any[]>([]) |
|
|
|
// ==================== 数据 ==================== |
|
|
|
const loading = ref(false) |
|
|
|
const saving = ref(false) |
|
|
|
const dialogVisible = ref(false) |
|
|
|
const step = ref(1) // 步骤:1=选类型, 2=填配置 |
|
|
|
const editingId = ref('') |
|
|
|
const activeType = ref('all') |
|
|
|
|
|
|
|
const providers = ref<any[]>([]) |
|
|
|
const models = ref<any[]>([]) |
|
|
|
const providerLoading = ref(false) |
|
|
|
const providerSaving = ref(false) |
|
|
|
const providerDrawerVisible = ref(false) |
|
|
|
const providerDialogVisible = ref(false) |
|
|
|
const editingProviderId = ref('') |
|
|
|
|
|
|
|
// ==================== 类型定义 ==================== |
|
|
|
const modelTypes = [ |
|
|
|
{ value: 'llm', icon: '🤖', name: 'LLM 大语言模型', desc: '对话、生成、推理、代码编写等通用任务' }, |
|
|
|
{ value: 'embedding', icon: '📐', name: 'Embedding 向量嵌入', desc: '将文本转换为向量,用于语义搜索和RAG检索' }, |
|
|
|
{ value: 'rerank', icon: '🔄', name: 'Rerank 重排序', desc: '对搜索结果进行相关性重排序,提升精度' }, |
|
|
|
] |
|
|
|
|
|
|
|
// ==================== 表单数据 ==================== |
|
|
|
const form = reactive({ |
|
|
|
model_type: '', |
|
|
|
provider_id: '', |
|
|
|
model_name: '', |
|
|
|
display_name: '', |
|
|
|
is_default: false, |
|
|
|
is_active: true, |
|
|
|
}) |
|
|
|
|
|
|
|
// LLM 专用表单 |
|
|
|
const llmForm = reactive({ |
|
|
|
temperature: 0.7, |
|
|
|
max_tokens: 4096, |
|
|
|
top_p: 1, |
|
|
|
frequency_penalty: 0, |
|
|
|
vision: false, |
|
|
|
function_calling: false, |
|
|
|
streaming: true, |
|
|
|
extraParamsJson: '', |
|
|
|
}) |
|
|
|
|
|
|
|
// Embedding 专用表单 |
|
|
|
const embForm = reactive({ |
|
|
|
dimension: '1536', |
|
|
|
max_chunk_tokens: 8192, |
|
|
|
extraParamsJson: '', |
|
|
|
}) |
|
|
|
|
|
|
|
// Rerank 专用表单 |
|
|
|
const rerankForm = reactive({ |
|
|
|
dimension: '1024', |
|
|
|
max_tokens: 512, |
|
|
|
score_method: 'cosine', |
|
|
|
extraParamsJson: '', |
|
|
|
}) |
|
|
|
|
|
|
|
// 供应商表单 |
|
|
|
const providerForm = reactive({ |
|
|
|
name: '', |
|
|
|
provider_type: 'openai_compatible', |
|
|
|
@ -158,44 +414,231 @@ const providerForm = reactive({ |
|
|
|
is_active: true, |
|
|
|
}) |
|
|
|
|
|
|
|
const selectedProvider = ref<any>(null) |
|
|
|
const models = ref<any[]>([]) |
|
|
|
const modelLoading = ref(false) |
|
|
|
const modelSaving = ref(false) |
|
|
|
const modelDialogVisible = ref(false) |
|
|
|
const editingModelId = ref('') |
|
|
|
const modelTab = ref('llm') |
|
|
|
const llmVision = ref(false) |
|
|
|
const llmFunctionCalling = ref(false) |
|
|
|
const defaultParamsJson = ref('') |
|
|
|
|
|
|
|
const modelForm = reactive({ |
|
|
|
model_name: '', |
|
|
|
model_type: 'llm', |
|
|
|
display_name: '', |
|
|
|
is_default: false, |
|
|
|
is_active: true, |
|
|
|
// ==================== 计算属性 ==================== |
|
|
|
const allModels = computed(() => models.value) |
|
|
|
|
|
|
|
const displayModels = computed(() => { |
|
|
|
if (activeType.value === 'all') return models.value |
|
|
|
return models.value.filter((m: any) => m.model_type === activeType.value) |
|
|
|
}) |
|
|
|
|
|
|
|
const filteredModels = computed(() => models.value.filter((m: any) => m.model_type === modelTab.value)) |
|
|
|
const dialogTitle = computed(() => { |
|
|
|
if (editingId.value) return `编辑模型 - ${currentTypeName.value}` |
|
|
|
return `添加${currentTypeName.value}模型` |
|
|
|
}) |
|
|
|
|
|
|
|
const currentTypeName = computed(() => { |
|
|
|
const t = modelTypes.find(t => t.value === form.model_type) |
|
|
|
return t ? t.name.replace(/^(LLM|Embedding|Rerank)\s*/, '') : '' |
|
|
|
}) |
|
|
|
|
|
|
|
const currentTypePlaceholder = computed(() => { |
|
|
|
const map: Record<string, any> = { |
|
|
|
llm: { name: '如 gpt-4o、claude-3-opus、deepseek-chat、qwen-max', displayName: '如 GPT-4o Turbo' }, |
|
|
|
embedding: { name: '如 text-embedding-3-small、bge-large-zh-v1.5', displayName: '如 Text-Embedding-3-Small' }, |
|
|
|
rerank: { name: '如 bge-reranker-v2-m3、cohere-rerank-3-multilingual', displayName: '如 BGE Reranker V2 M3' }, |
|
|
|
} |
|
|
|
return map[form.model_type] || { name: '', displayName: '' } |
|
|
|
}) |
|
|
|
|
|
|
|
// ==================== 辅助函数 ==================== |
|
|
|
function modelTypeName(type: string): string { |
|
|
|
const map: Record<string, string> = { llm: 'LLM', embedding: 'Embedding', rerank: 'Rerank' } |
|
|
|
return map[type] || type |
|
|
|
} |
|
|
|
|
|
|
|
function modelTagType(type: string): string { |
|
|
|
const map: Record<string, string> = { llm: '', embedding: 'success', rerank: 'warning' } |
|
|
|
return map[type] || '' |
|
|
|
} |
|
|
|
|
|
|
|
function providerTagName(type: string): string { |
|
|
|
const map: Record<string, string> = { openai_compatible: 'OpenAI 兼容', openai: 'OpenAI', zhipu: '智谱 AI', ollama: 'Ollama', deepseek: 'DeepSeek' } |
|
|
|
return map[type] || type |
|
|
|
} |
|
|
|
|
|
|
|
function providerTagType(type: string): string { |
|
|
|
const map: Record<string, string> = { openai_compatible: '', openai: '', zhipu: 'success', ollama: 'warning', deepseek: 'danger' } |
|
|
|
return map[type] || '' |
|
|
|
} |
|
|
|
|
|
|
|
function getProviderName(providerId: string): string { |
|
|
|
const p = providers.value.find((x: any) => x.id === providerId) |
|
|
|
return p ? p.name : '-' |
|
|
|
} |
|
|
|
|
|
|
|
function buildPayloadFromForm() { |
|
|
|
let capabilities: any = {} |
|
|
|
let defaultParams: any = {} |
|
|
|
|
|
|
|
if (form.model_type === 'llm') { |
|
|
|
capabilities = {} |
|
|
|
if (llmForm.vision) capabilities.vision = true |
|
|
|
if (llmForm.function_calling) capabilities.function_calling = true |
|
|
|
if (llmForm.streaming) capabilities.streaming = true |
|
|
|
|
|
|
|
defaultParams = { |
|
|
|
temperature: llmForm.temperature, |
|
|
|
max_tokens: llmForm.max_tokens, |
|
|
|
top_p: llmForm.top_p, |
|
|
|
frequency_penalty: llmForm.frequency_penalty, |
|
|
|
} |
|
|
|
try { Object.assign(defaultParams, JSON.parse(llmForm.extraParamsJson || '{}')) } catch { /* ignore */ } |
|
|
|
} else if (form.model_type === 'embedding') { |
|
|
|
defaultParams = { |
|
|
|
dimension: embForm.dimension, |
|
|
|
max_tokens: embForm.max_chunk_tokens, |
|
|
|
} |
|
|
|
try { Object.assign(defaultParams, JSON.parse(embForm.extraParamsJson || '{}')) } catch { /* ignore */ } |
|
|
|
} else if (form.model_type === 'rerank') { |
|
|
|
defaultParams = { |
|
|
|
dimension: rerankForm.dimension, |
|
|
|
max_tokens: rerankForm.max_tokens, |
|
|
|
score_method: rerankForm.score_method, |
|
|
|
} |
|
|
|
try { Object.assign(defaultParams, JSON.parse(rerankForm.extraParamsJson || '{}')) } catch { /* ignore */ } |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
|
...form, |
|
|
|
capabilities, |
|
|
|
default_params: defaultParams, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function populateFormFromData(data: any) { |
|
|
|
form.model_type = data.model_type |
|
|
|
form.provider_id = data.provider_id |
|
|
|
form.model_name = data.model_name |
|
|
|
form.display_name = data.display_name || '' |
|
|
|
form.is_default = !!data.is_default |
|
|
|
form.is_active = !!data.is_active |
|
|
|
|
|
|
|
const caps = data.capabilities || {} |
|
|
|
const params = data.default_params || {} |
|
|
|
|
|
|
|
if (data.model_type === 'llm') { |
|
|
|
llmForm.temperature = params.temperature ?? 0.7 |
|
|
|
llmForm.max_tokens = params.max_tokens ?? 4096 |
|
|
|
llmForm.top_p = params.top_p ?? 1 |
|
|
|
llmForm.frequency_penalty = params.frequency_penalty ?? 0 |
|
|
|
llmForm.vision = !!caps.vision |
|
|
|
llmForm.function_calling = !!caps.function_calling |
|
|
|
llmForm.streaming = !!caps.streaming |
|
|
|
// 过滤掉已映射到独立字段的参数 |
|
|
|
const extra: Record<string, any> = { ...params } |
|
|
|
delete extra.temperature; delete extra.max_tokens; delete extra.top_p |
|
|
|
delete extra.frequency_penalty |
|
|
|
llmForm.extraParamsJson = Object.keys(extra).length > 0 ? JSON.stringify(extra, null, 2) : '' |
|
|
|
} else if (data.model_type === 'embedding') { |
|
|
|
embForm.dimension = String(params.dimension ?? '1536') |
|
|
|
embForm.max_chunk_tokens = params.max_tokens ?? 8192 |
|
|
|
const extra: Record<string, any> = { ...params } |
|
|
|
delete extra.dimension; delete extra.max_tokens |
|
|
|
embForm.extraParamsJson = Object.keys(extra).length > 0 ? JSON.stringify(extra, null, 2) : '' |
|
|
|
} else if (data.model_type === 'rerank') { |
|
|
|
rerankForm.dimension = String(params.dimension ?? '1024') |
|
|
|
rerankForm.max_tokens = params.max_tokens ?? 512 |
|
|
|
rerankForm.score_method = params.score_method ?? 'cosine' |
|
|
|
const extra: Record<string, any> = { ...params } |
|
|
|
delete extra.dimension; delete extra.max_tokens; delete extra.score_method |
|
|
|
rerankForm.extraParamsJson = Object.keys(extra).length > 0 ? JSON.stringify(extra, null, 2) : '' |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function resetForm() { |
|
|
|
form.model_type = '' |
|
|
|
form.provider_id = '' |
|
|
|
form.model_name = '' |
|
|
|
form.display_name = '' |
|
|
|
form.is_default = false |
|
|
|
form.is_active = true |
|
|
|
step.value = 1 |
|
|
|
llmForm.temperature = 0.7 |
|
|
|
llmForm.max_tokens = 4096 |
|
|
|
llmForm.top_p = 1 |
|
|
|
llmForm.frequency_penalty = 0 |
|
|
|
llmForm.vision = false |
|
|
|
llmForm.function_calling = false |
|
|
|
llmForm.streaming = true |
|
|
|
llmForm.extraParamsJson = '' |
|
|
|
embForm.dimension = '1536' |
|
|
|
embForm.max_chunk_tokens = 8192 |
|
|
|
embForm.extraParamsJson = '' |
|
|
|
rerankForm.dimension = '1024' |
|
|
|
rerankForm.max_tokens = 512 |
|
|
|
rerankForm.score_method = 'cosine' |
|
|
|
rerankForm.extraParamsJson = '' |
|
|
|
} |
|
|
|
|
|
|
|
// ==================== 对话框操作 ==================== |
|
|
|
function openAddModelDialog() { |
|
|
|
resetForm() |
|
|
|
editingId.value = '' |
|
|
|
dialogVisible.value = true |
|
|
|
step.value = 1 |
|
|
|
} |
|
|
|
|
|
|
|
function selectModelType(type: string) { |
|
|
|
form.model_type = type |
|
|
|
} |
|
|
|
|
|
|
|
function goToStep2() { |
|
|
|
if (!form.model_type) return |
|
|
|
if (!form.provider_id && providers.value.length > 0) { |
|
|
|
form.provider_id = providers.value[0].id |
|
|
|
} |
|
|
|
step.value = 2 |
|
|
|
} |
|
|
|
|
|
|
|
function openEditModelDialog(row: any) { |
|
|
|
resetForm() |
|
|
|
editingId.value = row.id |
|
|
|
populateFormFromData(row) |
|
|
|
dialogVisible.value = true |
|
|
|
step.value = 2 |
|
|
|
} |
|
|
|
|
|
|
|
async function saveModel() { |
|
|
|
if (!form.provider_id) { |
|
|
|
ElMessage.warning('请选择所属供应商') |
|
|
|
return |
|
|
|
} |
|
|
|
if (!form.model_name.trim()) { |
|
|
|
ElMessage.warning('请输入模型标识名') |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
saving.value = true |
|
|
|
try { |
|
|
|
const payload = buildPayloadFromForm() |
|
|
|
if (editingId.value) { |
|
|
|
await api.put(`/model-providers/${payload.provider_id}/models/${editingId.value}`, payload) |
|
|
|
ElMessage.success('模型已更新') |
|
|
|
} else { |
|
|
|
await api.post(`/model-providers/${payload.provider_id}/models`, payload) |
|
|
|
ElMessage.success('模型已添加') |
|
|
|
} |
|
|
|
dialogVisible.value = false |
|
|
|
await loadModels() |
|
|
|
} catch { /* interceptor handles error */ } finally { |
|
|
|
saving.value = false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function resetProviderForm() { |
|
|
|
providerForm.name = '' |
|
|
|
providerForm.provider_type = 'openai_compatible' |
|
|
|
providerForm.base_url = '' |
|
|
|
providerForm.api_key = '' |
|
|
|
providerForm.is_active = true |
|
|
|
async function deleteModel(row: any) { |
|
|
|
try { |
|
|
|
await ElMessageBox.confirm(`确定删除模型「${row.display_name || row.model_name}」?`, '确认删除', { type: 'warning' }) |
|
|
|
await api.delete(`/model-providers/${row.provider_id}/models/${row.id}`) |
|
|
|
ElMessage.success('已删除') |
|
|
|
await loadModels() |
|
|
|
} catch { /* cancelled */ } |
|
|
|
} |
|
|
|
|
|
|
|
function resetModelForm() { |
|
|
|
modelForm.model_name = '' |
|
|
|
modelForm.model_type = 'llm' |
|
|
|
modelForm.display_name = '' |
|
|
|
modelForm.is_default = false |
|
|
|
modelForm.is_active = true |
|
|
|
llmVision.value = false |
|
|
|
llmFunctionCalling.value = false |
|
|
|
defaultParamsJson.value = '' |
|
|
|
// ==================== 供应商操作 ==================== |
|
|
|
function openProviderDrawer() { |
|
|
|
loadProviders() |
|
|
|
providerDrawerVisible.value = true |
|
|
|
} |
|
|
|
|
|
|
|
function openProviderDialog(row?: any) { |
|
|
|
@ -203,28 +646,22 @@ function openProviderDialog(row?: any) { |
|
|
|
editingProviderId.value = row.id |
|
|
|
providerForm.name = row.name |
|
|
|
providerForm.provider_type = row.provider_type |
|
|
|
providerForm.base_url = row.base_url |
|
|
|
providerForm.base_url = row.base_url || '' |
|
|
|
providerForm.api_key = row.api_key || '' |
|
|
|
providerForm.is_active = row.is_active |
|
|
|
} else { |
|
|
|
editingProviderId.value = '' |
|
|
|
resetProviderForm() |
|
|
|
providerForm.name = '' |
|
|
|
providerForm.provider_type = 'openai_compatible' |
|
|
|
providerForm.base_url = '' |
|
|
|
providerForm.api_key = '' |
|
|
|
providerForm.is_active = true |
|
|
|
} |
|
|
|
providerDialogVisible.value = true |
|
|
|
} |
|
|
|
|
|
|
|
async function loadProviders() { |
|
|
|
loading.value = true |
|
|
|
try { |
|
|
|
const res: any = await api.get('/model-providers') |
|
|
|
providers.value = res?.data || res || [] |
|
|
|
} finally { |
|
|
|
loading.value = false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
async function saveProvider() { |
|
|
|
saving.value = true |
|
|
|
providerSaving.value = true |
|
|
|
try { |
|
|
|
const data = { ...providerForm } |
|
|
|
if (editingProviderId.value) { |
|
|
|
@ -236,103 +673,80 @@ async function saveProvider() { |
|
|
|
} |
|
|
|
providerDialogVisible.value = false |
|
|
|
await loadProviders() |
|
|
|
} catch { |
|
|
|
// interceptor handles error |
|
|
|
} finally { |
|
|
|
saving.value = false |
|
|
|
} catch { /* interceptor handles error */ } finally { |
|
|
|
providerSaving.value = false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
async function deleteProvider(id: string) { |
|
|
|
try { |
|
|
|
await ElMessageBox.confirm('删除供应商将同时删除其下所有模型配置,确定继续?', '确认删除', { type: 'warning' }) |
|
|
|
await ElMessageBox.confirm('删除供应商会同时删除其下所有模型,确定继续?', '警告', { type: 'warning' }) |
|
|
|
await api.delete(`/model-providers/${id}`) |
|
|
|
ElMessage.success('已删除') |
|
|
|
if (selectedProvider.value?.id === id) selectedProvider.value = null |
|
|
|
await loadProviders() |
|
|
|
await loadModels() |
|
|
|
} catch { /* cancelled */ } |
|
|
|
} |
|
|
|
|
|
|
|
async function selectProvider(provider: any) { |
|
|
|
selectedProvider.value = provider |
|
|
|
await loadModels(provider.id) |
|
|
|
} |
|
|
|
|
|
|
|
async function loadModels(providerId: string) { |
|
|
|
modelLoading.value = true |
|
|
|
// ==================== 数据加载 ==================== |
|
|
|
async function loadProviders() { |
|
|
|
providerLoading.value = true |
|
|
|
try { |
|
|
|
const res: any = await api.get(`/model-providers/${providerId}/models`) |
|
|
|
models.value = res?.data || res || [] |
|
|
|
const res: any = await api.get('/model-providers') |
|
|
|
providers.value = res?.data || res || [] |
|
|
|
} finally { |
|
|
|
modelLoading.value = false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function openModelDialog(row?: any) { |
|
|
|
if (row) { |
|
|
|
editingModelId.value = row.id |
|
|
|
modelForm.model_name = row.model_name |
|
|
|
modelForm.model_type = row.model_type |
|
|
|
modelForm.display_name = row.display_name || '' |
|
|
|
modelForm.is_default = row.is_default |
|
|
|
modelForm.is_active = row.is_active |
|
|
|
const caps = row.capabilities || {} |
|
|
|
llmVision.value = !!caps.vision |
|
|
|
llmFunctionCalling.value = !!caps.function_calling |
|
|
|
defaultParamsJson.value = row.default_params ? JSON.stringify(row.default_params, null, 2) : '' |
|
|
|
} else { |
|
|
|
editingModelId.value = '' |
|
|
|
resetModelForm() |
|
|
|
providerLoading.value = false |
|
|
|
} |
|
|
|
modelDialogVisible.value = true |
|
|
|
} |
|
|
|
|
|
|
|
async function saveModel() { |
|
|
|
if (!selectedProvider.value) return |
|
|
|
modelSaving.value = true |
|
|
|
async function loadModels() { |
|
|
|
loading.value = true |
|
|
|
try { |
|
|
|
let defaultParams = {} |
|
|
|
try { defaultParams = JSON.parse(defaultParamsJson.value || '{}') } catch { /* keep empty */ } |
|
|
|
|
|
|
|
const capabilities: any = {} |
|
|
|
if (modelForm.model_type === 'llm') { |
|
|
|
capabilities.vision = llmVision.value |
|
|
|
capabilities.function_calling = llmFunctionCalling.value |
|
|
|
} |
|
|
|
|
|
|
|
const data = { |
|
|
|
...modelForm, |
|
|
|
capabilities, |
|
|
|
default_params: defaultParams, |
|
|
|
} |
|
|
|
|
|
|
|
if (editingModelId.value) { |
|
|
|
await api.put(`/model-providers/${selectedProvider.value.id}/models/${editingModelId.value}`, data) |
|
|
|
ElMessage.success('模型已更新') |
|
|
|
} else { |
|
|
|
await api.post(`/model-providers/${selectedProvider.value.id}/models`, data) |
|
|
|
ElMessage.success('模型已添加') |
|
|
|
} |
|
|
|
modelDialogVisible.value = false |
|
|
|
await loadModels(selectedProvider.value.id) |
|
|
|
} catch { |
|
|
|
// interceptor handles error |
|
|
|
const res: any = await api.get('/model-providers/models/all') |
|
|
|
models.value = res?.data || res || [] |
|
|
|
} finally { |
|
|
|
modelSaving.value = false |
|
|
|
loading.value = false |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
async function deleteModel(id: string) { |
|
|
|
if (!selectedProvider.value) return |
|
|
|
try { |
|
|
|
await ElMessageBox.confirm('确定删除此模型配置?', '确认删除', { type: 'warning' }) |
|
|
|
await api.delete(`/model-providers/${selectedProvider.value.id}/models/${id}`) |
|
|
|
ElMessage.success('已删除') |
|
|
|
await loadModels(selectedProvider.value.id) |
|
|
|
} catch { /* cancelled */ } |
|
|
|
} |
|
|
|
|
|
|
|
onMounted(() => { |
|
|
|
loadProviders() |
|
|
|
loadModels() |
|
|
|
}) |
|
|
|
</script> |
|
|
|
|
|
|
|
<style scoped> |
|
|
|
.type-card { |
|
|
|
border: 2px solid #e4e7ed; |
|
|
|
border-radius: 12px; |
|
|
|
padding: 24px 16px; |
|
|
|
text-align: center; |
|
|
|
cursor: pointer; |
|
|
|
transition: all 0.25s ease; |
|
|
|
} |
|
|
|
.type-card:hover { |
|
|
|
border-color: #409eff; |
|
|
|
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15); |
|
|
|
transform: translateY(-2px); |
|
|
|
} |
|
|
|
.type-card.selected { |
|
|
|
border-color: #409eff; |
|
|
|
background: linear-gradient(135deg, #ecf5ff 0%, #f0f9ff 100%); |
|
|
|
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.2); |
|
|
|
} |
|
|
|
.type-icon { |
|
|
|
font-size: 36px; |
|
|
|
margin-bottom: 8px; |
|
|
|
} |
|
|
|
.type-name { |
|
|
|
font-size: 16px; |
|
|
|
font-weight: 600; |
|
|
|
color: #303133; |
|
|
|
margin-bottom: 4px; |
|
|
|
} |
|
|
|
.type-desc { |
|
|
|
font-size: 12px; |
|
|
|
color: #909399; |
|
|
|
line-height: 1.5; |
|
|
|
} |
|
|
|
</style> |
|
|
|
|