feat: Add support for builtin models in the application
- Introduced `is_builtin` property in the model configuration to identify builtin models. - Updated frontend components to display a tag for builtin models in the ModelSelector and ModelSettings. - Enhanced the ModelEditorDialog to prevent editing and deletion of builtin models. - Implemented logic in the backend to hide sensitive information for builtin models and restrict their modification. - Added informative UI elements to clarify the nature of builtin models in the settings and editor views.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
# 内置模型管理指南
|
||||
|
||||
## 概述
|
||||
|
||||
内置模型是系统级别的模型配置,对所有租户可见,但敏感信息会被隐藏,且不可编辑或删除。内置模型通常用于提供系统默认的模型配置,确保所有租户都能使用统一的模型服务。
|
||||
|
||||
## 内置模型特性
|
||||
|
||||
- **所有租户可见**:内置模型对所有租户都可见,无需单独配置
|
||||
- **安全保护**:内置模型的敏感信息(API Key、Base URL)会被隐藏,无法查看详情
|
||||
- **只读保护**:内置模型不能被编辑或删除,只能设置为默认模型
|
||||
- **统一管理**:由系统管理员统一维护,确保配置一致性和安全性
|
||||
|
||||
## 如何添加内置模型
|
||||
|
||||
内置模型需要通过数据库直接插入。以下是添加内置模型的步骤:
|
||||
|
||||
### 1. 准备模型数据
|
||||
|
||||
首先,确保你已经有了要设置为内置模型的模型配置信息,包括:
|
||||
- 模型名称(name)
|
||||
- 模型类型(type):`KnowledgeQA`、`Embedding`、`Rerank` 或 `VLLM`
|
||||
- 模型来源(source):`local` 或 `remote`
|
||||
- 模型参数(parameters):包括 base_url、api_key 等
|
||||
- 租户ID(tenant_id):建议使用小于10000的租户ID,避免冲突
|
||||
|
||||
### 2. 执行 SQL 插入语句
|
||||
|
||||
使用以下 SQL 语句插入内置模型:
|
||||
|
||||
```sql
|
||||
-- 示例:插入一个 LLM 内置模型
|
||||
INSERT INTO models (
|
||||
id,
|
||||
tenant_id,
|
||||
name,
|
||||
type,
|
||||
source,
|
||||
description,
|
||||
parameters,
|
||||
is_default,
|
||||
status,
|
||||
is_builtin
|
||||
) VALUES (
|
||||
'builtin-llm-001', -- 使用固定ID,建议使用 builtin- 前缀
|
||||
10000, -- 租户ID(使用第一个租户)
|
||||
'GPT-4', -- 模型名称
|
||||
'KnowledgeQA', -- 模型类型
|
||||
'remote', -- 模型来源
|
||||
'内置 LLM 模型', -- 描述
|
||||
'{"base_url": "https://api.openai.com/v1", "api_key": "sk-xxx"}'::jsonb, -- 参数(JSON格式)
|
||||
false, -- 是否默认
|
||||
'active', -- 状态
|
||||
true -- 标记为内置模型
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 示例:插入一个 Embedding 内置模型
|
||||
INSERT INTO models (
|
||||
id,
|
||||
tenant_id,
|
||||
name,
|
||||
type,
|
||||
source,
|
||||
description,
|
||||
parameters,
|
||||
is_default,
|
||||
status,
|
||||
is_builtin
|
||||
) VALUES (
|
||||
'builtin-embedding-001',
|
||||
10000,
|
||||
'text-embedding-ada-002',
|
||||
'Embedding',
|
||||
'remote',
|
||||
'内置 Embedding 模型',
|
||||
'{"base_url": "https://api.openai.com/v1", "api_key": "sk-xxx", "embedding_parameters": {"dimension": 1536, "truncate_prompt_tokens": 0}}'::jsonb,
|
||||
false,
|
||||
'active',
|
||||
true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 示例:插入一个 ReRank 内置模型
|
||||
INSERT INTO models (
|
||||
id,
|
||||
tenant_id,
|
||||
name,
|
||||
type,
|
||||
source,
|
||||
description,
|
||||
parameters,
|
||||
is_default,
|
||||
status,
|
||||
is_builtin
|
||||
) VALUES (
|
||||
'builtin-rerank-001',
|
||||
10000,
|
||||
'bge-reranker-base',
|
||||
'Rerank',
|
||||
'remote',
|
||||
'内置 ReRank 模型',
|
||||
'{"base_url": "https://api.example.com/v1", "api_key": "xxx"}'::jsonb,
|
||||
false,
|
||||
'active',
|
||||
true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 示例:插入一个 VLLM 内置模型
|
||||
INSERT INTO models (
|
||||
id,
|
||||
tenant_id,
|
||||
name,
|
||||
type,
|
||||
source,
|
||||
description,
|
||||
parameters,
|
||||
is_default,
|
||||
status,
|
||||
is_builtin
|
||||
) VALUES (
|
||||
'builtin-vllm-001',
|
||||
10000,
|
||||
'gpt-4-vision',
|
||||
'VLLM',
|
||||
'remote',
|
||||
'内置 VLLM 模型',
|
||||
'{"base_url": "https://api.openai.com/v1", "api_key": "sk-xxx"}'::jsonb,
|
||||
false,
|
||||
'active',
|
||||
true
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
```
|
||||
|
||||
### 3. 验证插入结果
|
||||
|
||||
执行以下 SQL 查询验证内置模型是否成功插入:
|
||||
|
||||
```sql
|
||||
SELECT id, name, type, is_builtin, status
|
||||
FROM models
|
||||
WHERE is_builtin = true
|
||||
ORDER BY type, created_at;
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **ID 命名规范**:建议使用 `builtin-{type}-{序号}` 的格式,例如 `builtin-llm-001`、`builtin-embedding-001`
|
||||
2. **租户ID**:内置模型可以属于任意租户,但建议使用第一个租户ID(通常是 10000)
|
||||
3. **参数格式**:`parameters` 字段必须是有效的 JSON 格式
|
||||
4. **幂等性**:使用 `ON CONFLICT (id) DO NOTHING` 确保重复执行不会报错
|
||||
5. **安全性**:内置模型的 API Key 和 Base URL 在前端会被自动隐藏,但数据库中的原始数据仍然存在,请妥善保管数据库访问权限
|
||||
|
||||
## 将现有模型设置为内置模型
|
||||
|
||||
如果你已经有一个模型,想将其设置为内置模型,可以使用 UPDATE 语句:
|
||||
|
||||
```sql
|
||||
UPDATE models
|
||||
SET is_builtin = true
|
||||
WHERE id = '模型ID' AND name = '模型名称';
|
||||
```
|
||||
|
||||
## 移除内置模型
|
||||
|
||||
如果需要移除内置模型标记(恢复为普通模型),执行:
|
||||
|
||||
```sql
|
||||
UPDATE models
|
||||
SET is_builtin = false
|
||||
WHERE id = '模型ID';
|
||||
```
|
||||
|
||||
注意:移除内置模型标记后,该模型将恢复为普通模型,可以被编辑和删除。
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface ModelConfig {
|
||||
parameter_size?: string; // Ollama模型参数大小 (e.g., "7B", "13B", "70B")
|
||||
};
|
||||
is_default?: boolean;
|
||||
is_builtin?: boolean;
|
||||
status?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
|
||||
@@ -781,12 +781,12 @@ const handleCancel = () => {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(6px);
|
||||
overflow: hidden; // 防止背景滚动
|
||||
}
|
||||
|
||||
@@ -798,7 +798,7 @@ const handleCancel = () => {
|
||||
max-height: 85vh;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -813,41 +813,43 @@ const handleCancel = () => {
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
transition: all 0.2s ease;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
z-index: 10;
|
||||
|
||||
&:hover {
|
||||
background: #e5e5e5;
|
||||
color: #000;
|
||||
background: rgba(7, 192, 95, 0.1);
|
||||
color: #07C05F;
|
||||
}
|
||||
}
|
||||
|
||||
// 标题区域
|
||||
.modal-header {
|
||||
padding: 24px 32px 20px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
padding: 28px 32px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(to bottom, #ffffff 0%, #fafafa 100%);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #000000e6;
|
||||
color: #1a1a1a;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.modal-desc {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
color: #00000066;
|
||||
color: #666666;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
@@ -855,7 +857,8 @@ const handleCancel = () => {
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px 32px;
|
||||
padding: 28px 32px;
|
||||
background: #ffffff;
|
||||
|
||||
// 自定义滚动条
|
||||
&::-webkit-scrollbar {
|
||||
@@ -870,9 +873,10 @@ const handleCancel = () => {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #d0d0d0;
|
||||
border-radius: 3px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #b0b0b0;
|
||||
background: #07C05F;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -894,16 +898,18 @@ const handleCancel = () => {
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 10px;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #000000e6;
|
||||
color: #333333;
|
||||
letter-spacing: -0.2px;
|
||||
|
||||
&.required::after {
|
||||
content: '*';
|
||||
color: #FA5151;
|
||||
margin-left: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -914,26 +920,53 @@ const handleCancel = () => {
|
||||
:deep(.t-input-number) {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.t-input__inner,
|
||||
input {
|
||||
font-size: 14px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover .t-input__inner,
|
||||
&:hover input {
|
||||
border-color: #07C05F;
|
||||
}
|
||||
|
||||
&.t-is-focused .t-input__inner,
|
||||
&.t-is-focused input {
|
||||
border-color: #07C05F;
|
||||
box-shadow: 0 0 0 2px rgba(7, 192, 95, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 单选按钮组
|
||||
:deep(.t-radio-group) {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
gap: 20px;
|
||||
|
||||
.t-radio {
|
||||
margin-right: 0;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
.t-radio__label {
|
||||
color: #07C05F;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.t-radio__label {
|
||||
font-size: 14px;
|
||||
color: #000000e6;
|
||||
color: #333333;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.t-radio__input:checked + .t-radio__label {
|
||||
color: #07C05F;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -949,12 +982,37 @@ const handleCancel = () => {
|
||||
|
||||
// 底部按钮区域
|
||||
.modal-footer {
|
||||
padding: 16px 32px;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
padding: 20px 32px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
background: #fafafa;
|
||||
|
||||
:deep(.t-button) {
|
||||
min-width: 88px;
|
||||
height: 36px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
|
||||
&.t-button--theme-primary {
|
||||
background: #07C05F;
|
||||
border-color: #07C05F;
|
||||
|
||||
&:hover {
|
||||
background: #06b04d;
|
||||
border-color: #06b04d;
|
||||
}
|
||||
}
|
||||
|
||||
&.t-button--variant-outline {
|
||||
&:hover {
|
||||
border-color: #07C05F;
|
||||
color: #07C05F;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 过渡动画
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<div class="model-option">
|
||||
<t-icon name="check-circle-filled" class="model-icon" />
|
||||
<span class="model-name">{{ model.name }}</span>
|
||||
<t-tag v-if="model.is_builtin" size="small" theme="primary">内置</t-tag>
|
||||
<t-tag v-if="model.is_default" size="small" theme="success">{{ $t('model.defaultTag') }}</t-tag>
|
||||
</div>
|
||||
</t-option>
|
||||
|
||||
@@ -535,6 +535,16 @@ watch(() => props.visible, async (newVal) => {
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听全局设置弹窗关闭后刷新模型列表
|
||||
watch(
|
||||
() => uiStore.showSettingsModal,
|
||||
async (visible, previous) => {
|
||||
if (!visible && previous && props.visible) {
|
||||
await loadAllModels()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
@@ -3,6 +3,25 @@
|
||||
<div class="section-header">
|
||||
<h2>{{ $t('modelSettings.title') }}</h2>
|
||||
<p class="section-description">{{ $t('modelSettings.description') }}</p>
|
||||
|
||||
<!-- 内置模型说明 -->
|
||||
<div class="builtin-models-info">
|
||||
<div class="info-box">
|
||||
<div class="info-header">
|
||||
<t-icon name="info-circle" class="info-icon" />
|
||||
<span class="info-title">内置模型</span>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<p>内置模型对所有租户可见,敏感信息会被隐藏,且不可编辑或删除。</p>
|
||||
<p class="doc-link">
|
||||
<t-icon name="link" class="link-icon" />
|
||||
<a href="https://github.com/Tencent/WeKnora/blob/main/docs/BUILTIN_MODELS.md" target="_blank" rel="noopener noreferrer">
|
||||
查看内置模型管理指南
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 对话模型 -->
|
||||
@@ -12,19 +31,20 @@
|
||||
<h3>{{ $t('modelSettings.chat.title') }}</h3>
|
||||
<p>{{ $t('modelSettings.chat.desc') }}</p>
|
||||
</div>
|
||||
<t-button size="small" theme="primary" @click="openAddDialog('chat')">
|
||||
<t-button size="small" theme="primary" @click="openAddDialog('chat')" class="add-model-btn">
|
||||
<template #icon>
|
||||
<t-icon name="add" />
|
||||
<t-icon name="add" class="add-icon" />
|
||||
</template>
|
||||
{{ $t('modelSettings.actions.addModel') }}
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
<div v-if="chatModels.length > 0" class="model-list-container">
|
||||
<div v-for="model in chatModels" :key="model.id" class="model-card">
|
||||
<div v-for="model in chatModels" :key="model.id" class="model-card" :class="{ 'builtin-model': model.isBuiltin }">
|
||||
<div class="model-info">
|
||||
<div class="model-name">
|
||||
{{ model.name }}
|
||||
<t-tag v-if="model.isBuiltin" theme="primary" size="small">内置</t-tag>
|
||||
<t-tag v-if="model.isDefault" theme="success" size="small">{{ $t('common.default') }}</t-tag>
|
||||
</div>
|
||||
<div class="model-meta">
|
||||
@@ -61,19 +81,20 @@
|
||||
<h3>{{ $t('modelSettings.embedding.title') }}</h3>
|
||||
<p>{{ $t('modelSettings.embedding.desc') }}</p>
|
||||
</div>
|
||||
<t-button size="small" theme="primary" @click="openAddDialog('embedding')">
|
||||
<t-button size="small" theme="primary" @click="openAddDialog('embedding')" class="add-model-btn">
|
||||
<template #icon>
|
||||
<t-icon name="add" />
|
||||
<t-icon name="add" class="add-icon" />
|
||||
</template>
|
||||
{{ $t('modelSettings.actions.addModel') }}
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
<div v-if="embeddingModels.length > 0" class="model-list-container">
|
||||
<div v-for="model in embeddingModels" :key="model.id" class="model-card">
|
||||
<div v-for="model in embeddingModels" :key="model.id" class="model-card" :class="{ 'builtin-model': model.isBuiltin }">
|
||||
<div class="model-info">
|
||||
<div class="model-name">
|
||||
{{ model.name }}
|
||||
<t-tag v-if="model.isBuiltin" theme="primary" size="small">内置</t-tag>
|
||||
<t-tag v-if="model.isDefault" theme="success" size="small">{{ $t('common.default') }}</t-tag>
|
||||
</div>
|
||||
<div class="model-meta">
|
||||
@@ -111,19 +132,20 @@
|
||||
<h3>{{ $t('modelSettings.rerank.title') }}</h3>
|
||||
<p>{{ $t('modelSettings.rerank.desc') }}</p>
|
||||
</div>
|
||||
<t-button size="small" theme="primary" @click="openAddDialog('rerank')">
|
||||
<t-button size="small" theme="primary" @click="openAddDialog('rerank')" class="add-model-btn">
|
||||
<template #icon>
|
||||
<t-icon name="add" />
|
||||
<t-icon name="add" class="add-icon" />
|
||||
</template>
|
||||
{{ $t('modelSettings.actions.addModel') }}
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
<div v-if="rerankModels.length > 0" class="model-list-container">
|
||||
<div v-for="model in rerankModels" :key="model.id" class="model-card">
|
||||
<div v-for="model in rerankModels" :key="model.id" class="model-card" :class="{ 'builtin-model': model.isBuiltin }">
|
||||
<div class="model-info">
|
||||
<div class="model-name">
|
||||
{{ model.name }}
|
||||
<t-tag v-if="model.isBuiltin" theme="primary" size="small">内置</t-tag>
|
||||
<t-tag v-if="model.isDefault" theme="success" size="small">{{ $t('common.default') }}</t-tag>
|
||||
</div>
|
||||
<div class="model-meta">
|
||||
@@ -160,19 +182,20 @@
|
||||
<h3>{{ $t('modelSettings.vllm.title') }}</h3>
|
||||
<p>{{ $t('modelSettings.vllm.desc') }}</p>
|
||||
</div>
|
||||
<t-button size="small" theme="primary" @click="openAddDialog('vllm')">
|
||||
<t-button size="small" theme="primary" @click="openAddDialog('vllm')" class="add-model-btn">
|
||||
<template #icon>
|
||||
<t-icon name="add" />
|
||||
<t-icon name="add" class="add-icon" />
|
||||
</template>
|
||||
{{ $t('modelSettings.actions.addModel') }}
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
<div v-if="vllmModels.length > 0" class="model-list-container">
|
||||
<div v-for="model in vllmModels" :key="model.id" class="model-card">
|
||||
<div v-for="model in vllmModels" :key="model.id" class="model-card" :class="{ 'builtin-model': model.isBuiltin }">
|
||||
<div class="model-info">
|
||||
<div class="model-name">
|
||||
{{ model.name }}
|
||||
<t-tag v-if="model.isBuiltin" theme="primary" size="small">内置</t-tag>
|
||||
<t-tag v-if="model.isDefault" theme="success" size="small">{{ $t('common.default') }}</t-tag>
|
||||
</div>
|
||||
<div class="model-meta">
|
||||
@@ -272,7 +295,8 @@ function convertToLegacyFormat(model: ModelConfig) {
|
||||
baseUrl: model.parameters.base_url || '',
|
||||
apiKey: model.parameters.api_key || '',
|
||||
dimension: model.parameters.embedding_parameters?.dimension,
|
||||
isDefault: model.is_default || false
|
||||
isDefault: model.is_default || false,
|
||||
isBuiltin: model.is_builtin || false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,6 +354,11 @@ const openAddDialog = (type: 'chat' | 'embedding' | 'rerank' | 'vllm') => {
|
||||
|
||||
// 编辑模型
|
||||
const editModel = (type: 'chat' | 'embedding' | 'rerank' | 'vllm', model: any) => {
|
||||
// 内置模型不能编辑
|
||||
if (model.isBuiltin) {
|
||||
MessagePlugin.warning('内置模型不能编辑')
|
||||
return
|
||||
}
|
||||
currentModelType.value = type
|
||||
editingModel.value = { ...model }
|
||||
showDialog.value = true
|
||||
@@ -412,6 +441,13 @@ const handleModelSave = async (modelData: any) => {
|
||||
|
||||
// 删除模型
|
||||
const deleteModel = async (type: 'chat' | 'embedding' | 'rerank' | 'vllm', modelId: string) => {
|
||||
// 检查是否是内置模型
|
||||
const model = allModels.value.find(m => m.id === modelId)
|
||||
if (model?.is_builtin) {
|
||||
MessagePlugin.warning('内置模型不能删除')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteModelAPI(modelId)
|
||||
MessagePlugin.success(t('modelSettings.toasts.deleted'))
|
||||
@@ -441,6 +477,18 @@ const setDefault = async (type: 'chat' | 'embedding' | 'rerank' | 'vllm', modelI
|
||||
const getModelOptions = (type: 'chat' | 'embedding' | 'rerank' | 'vllm', model: any) => {
|
||||
const options: any[] = []
|
||||
|
||||
// 内置模型不能编辑和删除,只能设为默认
|
||||
if (model.isBuiltin) {
|
||||
// 如果不是默认模型,显示"设为默认"选项
|
||||
if (!model.isDefault) {
|
||||
options.push({
|
||||
content: t('modelSettings.actions.setDefault'),
|
||||
value: `set-default-${type}-${model.id}`
|
||||
})
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// 如果不是默认模型,显示"设为默认"选项
|
||||
if (!model.isDefault) {
|
||||
options.push({
|
||||
@@ -556,7 +604,20 @@ onMounted(() => {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加模型按钮样式优化
|
||||
:deep(.add-model-btn) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-weight: 500;
|
||||
|
||||
.add-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.model-list-container {
|
||||
@@ -585,6 +646,39 @@ onMounted(() => {
|
||||
border-color: #07C05F;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
// 内置模型置灰样式
|
||||
&.builtin-model {
|
||||
opacity: 0.7;
|
||||
background: #f5f5f5;
|
||||
border-color: #d9d9d9;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
border-color: #d9d9d9;
|
||||
background: #f5f5f5;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.model-info {
|
||||
.model-name {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.model-meta {
|
||||
color: #999999;
|
||||
|
||||
.source-tag {
|
||||
background: #e5e5e5;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.model-actions {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.model-info {
|
||||
@@ -661,4 +755,68 @@ onMounted(() => {
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.builtin-models-info {
|
||||
margin-top: 20px;
|
||||
|
||||
.info-box {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-left: 3px solid #07C05F;
|
||||
border-radius: 4px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.info-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.info-icon {
|
||||
font-size: 16px;
|
||||
color: #07C05F;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
.info-content {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #666666;
|
||||
|
||||
p {
|
||||
margin: 0 0 8px 0;
|
||||
|
||||
&.doc-link {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.link-icon {
|
||||
font-size: 14px;
|
||||
color: #07C05F;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #07C05F;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #06b04d;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,7 +28,7 @@ func (r *modelRepository) Create(ctx context.Context, m *types.Model) error {
|
||||
func (r *modelRepository) GetByID(ctx context.Context, tenantID uint, id string) (*types.Model, error) {
|
||||
var m types.Model
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).Where(
|
||||
"tenant_id = ? or tenant_id = 0", tenantID,
|
||||
"tenant_id = ? OR is_builtin = true", tenantID,
|
||||
).First(&m).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
@@ -44,7 +44,7 @@ func (r *modelRepository) List(
|
||||
) ([]*types.Model, error) {
|
||||
var models []*types.Model
|
||||
query := r.db.WithContext(ctx).Where(
|
||||
"tenant_id = ? or tenant_id = 0", tenantID,
|
||||
"tenant_id = ? OR is_builtin = true", tenantID,
|
||||
)
|
||||
|
||||
if modelType != "" {
|
||||
|
||||
@@ -178,6 +178,20 @@ func (s *modelService) UpdateModel(ctx context.Context, model *types.Model) erro
|
||||
logger.Info(ctx, "Start updating model")
|
||||
logger.Infof(ctx, "Updating model ID: %s, name: %s", model.ID, model.Name)
|
||||
|
||||
// Check if the model is builtin - builtin models cannot be updated
|
||||
tenantID := ctx.Value(types.TenantIDContextKey).(uint)
|
||||
existingModel, err := s.repo.GetByID(ctx, tenantID, model.ID)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"model_id": model.ID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
if existingModel != nil && existingModel.IsBuiltin {
|
||||
logger.Warnf(ctx, "Attempted to update builtin model: %s", model.ID)
|
||||
return errors.New("builtin models cannot be updated")
|
||||
}
|
||||
|
||||
// If this model is set as default, unset other default models of the same type
|
||||
if model.IsDefault {
|
||||
logger.Infof(ctx, "Model is set as default, clearing other default models of type: %s", model.Type)
|
||||
@@ -192,7 +206,7 @@ func (s *modelService) UpdateModel(ctx context.Context, model *types.Model) erro
|
||||
}
|
||||
|
||||
// Update model in repository
|
||||
err := s.repo.Update(ctx, model)
|
||||
err = s.repo.Update(ctx, model)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"model_id": model.ID,
|
||||
@@ -213,8 +227,21 @@ func (s *modelService) DeleteModel(ctx context.Context, id string) error {
|
||||
tenantID := ctx.Value(types.TenantIDContextKey).(uint)
|
||||
logger.Infof(ctx, "Tenant ID: %d", tenantID)
|
||||
|
||||
// Check if the model is builtin - builtin models cannot be deleted
|
||||
existingModel, err := s.repo.GetByID(ctx, tenantID, id)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"model_id": id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
if existingModel != nil && existingModel.IsBuiltin {
|
||||
logger.Warnf(ctx, "Attempted to delete builtin model: %s", id)
|
||||
return errors.New("builtin models cannot be deleted")
|
||||
}
|
||||
|
||||
// Delete model from repository
|
||||
err := s.repo.Delete(ctx, tenantID, id)
|
||||
err = s.repo.Delete(ctx, tenantID, id)
|
||||
if err != nil {
|
||||
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
||||
"model_id": id,
|
||||
|
||||
@@ -1155,28 +1155,36 @@ func (h *InitializationHandler) buildConfigResponse(ctx context.Context, models
|
||||
|
||||
// 按类型分组模型
|
||||
for _, model := range models {
|
||||
// Hide sensitive information for builtin models
|
||||
baseURL := model.Parameters.BaseURL
|
||||
apiKey := model.Parameters.APIKey
|
||||
if model.IsBuiltin {
|
||||
baseURL = ""
|
||||
apiKey = ""
|
||||
}
|
||||
|
||||
switch model.Type {
|
||||
case types.ModelTypeKnowledgeQA:
|
||||
config["llm"] = map[string]interface{}{
|
||||
"source": string(model.Source),
|
||||
"modelName": model.Name,
|
||||
"baseUrl": model.Parameters.BaseURL,
|
||||
"apiKey": model.Parameters.APIKey,
|
||||
"baseUrl": baseURL,
|
||||
"apiKey": apiKey,
|
||||
}
|
||||
case types.ModelTypeEmbedding:
|
||||
config["embedding"] = map[string]interface{}{
|
||||
"source": string(model.Source),
|
||||
"modelName": model.Name,
|
||||
"baseUrl": model.Parameters.BaseURL,
|
||||
"apiKey": model.Parameters.APIKey,
|
||||
"baseUrl": baseURL,
|
||||
"apiKey": apiKey,
|
||||
"dimension": model.Parameters.EmbeddingParameters.Dimension,
|
||||
}
|
||||
case types.ModelTypeRerank:
|
||||
config["rerank"] = map[string]interface{}{
|
||||
"enabled": true,
|
||||
"modelName": model.Name,
|
||||
"baseUrl": model.Parameters.BaseURL,
|
||||
"apiKey": model.Parameters.APIKey,
|
||||
"baseUrl": baseURL,
|
||||
"apiKey": apiKey,
|
||||
}
|
||||
case types.ModelTypeVLLM:
|
||||
if config["multimodal"] == nil {
|
||||
@@ -1187,8 +1195,8 @@ func (h *InitializationHandler) buildConfigResponse(ctx context.Context, models
|
||||
multimodal := config["multimodal"].(map[string]interface{})
|
||||
multimodal["vlm"] = map[string]interface{}{
|
||||
"modelName": model.Name,
|
||||
"baseUrl": model.Parameters.BaseURL,
|
||||
"apiKey": model.Parameters.APIKey,
|
||||
"baseUrl": baseURL,
|
||||
"apiKey": apiKey,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,37 @@ func NewModelHandler(service interfaces.ModelService) *ModelHandler {
|
||||
return &ModelHandler{service: service}
|
||||
}
|
||||
|
||||
// hideSensitiveInfo hides sensitive information (APIKey, BaseURL) for builtin models
|
||||
// Returns a copy of the model with sensitive fields cleared if it's a builtin model
|
||||
func hideSensitiveInfo(model *types.Model) *types.Model {
|
||||
if !model.IsBuiltin {
|
||||
return model
|
||||
}
|
||||
|
||||
// Create a copy with sensitive information hidden
|
||||
return &types.Model{
|
||||
ID: model.ID,
|
||||
TenantID: model.TenantID,
|
||||
Name: model.Name,
|
||||
Type: model.Type,
|
||||
Source: model.Source,
|
||||
Description: model.Description,
|
||||
Parameters: types.ModelParameters{
|
||||
// Hide APIKey and BaseURL for builtin models
|
||||
BaseURL: "",
|
||||
APIKey: "",
|
||||
// Keep other parameters like embedding dimensions
|
||||
EmbeddingParameters: model.Parameters.EmbeddingParameters,
|
||||
ParameterSize: model.Parameters.ParameterSize,
|
||||
},
|
||||
IsDefault: model.IsDefault,
|
||||
IsBuiltin: model.IsBuiltin,
|
||||
Status: model.Status,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateModelRequest defines the structure for model creation requests
|
||||
// Contains all fields required to create a new model in the system
|
||||
type CreateModelRequest struct {
|
||||
@@ -82,9 +113,13 @@ func (h *ModelHandler) CreateModel(c *gin.Context) {
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Model created successfully, ID: %s, Name: %s", model.ID, model.Name)
|
||||
|
||||
// Hide sensitive information for builtin models (though newly created models are unlikely to be builtin)
|
||||
responseModel := hideSensitiveInfo(model)
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"success": true,
|
||||
"data": model,
|
||||
"data": responseModel,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -119,9 +154,16 @@ func (h *ModelHandler) GetModel(c *gin.Context) {
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Retrieved model successfully, ID: %s, Name: %s", model.ID, model.Name)
|
||||
|
||||
// Hide sensitive information for builtin models
|
||||
responseModel := hideSensitiveInfo(model)
|
||||
if model.IsBuiltin {
|
||||
logger.Infof(ctx, "Builtin model detected, hiding sensitive information for model: %s", model.ID)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": model,
|
||||
"data": responseModel,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -150,9 +192,19 @@ func (h *ModelHandler) ListModels(c *gin.Context) {
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Retrieved model list successfully, Tenant ID: %d, Total: %d models", tenantID, len(models))
|
||||
|
||||
// Hide sensitive information for builtin models in the list
|
||||
responseModels := make([]*types.Model, len(models))
|
||||
for i, model := range models {
|
||||
responseModels[i] = hideSensitiveInfo(model)
|
||||
if model.IsBuiltin {
|
||||
logger.Infof(ctx, "Builtin model detected in list, hiding sensitive information for model: %s", model.ID)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": models,
|
||||
"data": responseModels,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -224,9 +276,13 @@ func (h *ModelHandler) UpdateModel(c *gin.Context) {
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Model updated successfully, ID: %s", id)
|
||||
|
||||
// Hide sensitive information for builtin models (though builtin models cannot be updated)
|
||||
responseModel := hideSensitiveInfo(model)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": model,
|
||||
"data": responseModel,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@ type Model struct {
|
||||
Parameters ModelParameters `yaml:"parameters" json:"parameters" gorm:"type:json"`
|
||||
// Whether the model is the default model
|
||||
IsDefault bool `yaml:"is_default" json:"is_default"`
|
||||
// Whether the model is a builtin model (visible to all tenants)
|
||||
IsBuiltin bool `yaml:"is_builtin" json:"is_builtin" gorm:"default:false"`
|
||||
// Model status, default: active, possible: downloading, download_failed
|
||||
Status ModelStatus `yaml:"status" json:"status"`
|
||||
// Creation time of the model
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- 07-add-builtin-models.sql (rollback)
|
||||
-- Remove is_builtin field from models table
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Drop index
|
||||
DROP INDEX IF EXISTS idx_models_is_builtin;
|
||||
|
||||
-- Remove is_builtin column
|
||||
ALTER TABLE models
|
||||
DROP COLUMN IF EXISTS is_builtin;
|
||||
|
||||
COMMIT;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- 07-add-builtin-models.sql
|
||||
-- Add is_builtin field to models table to support builtin models visible to all tenants
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Add is_builtin column to models table
|
||||
ALTER TABLE models
|
||||
ADD COLUMN IF NOT EXISTS is_builtin BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- Add index for is_builtin field to improve query performance
|
||||
CREATE INDEX IF NOT EXISTS idx_models_is_builtin ON models(is_builtin);
|
||||
|
||||
COMMIT;
|
||||
|
||||
Reference in New Issue
Block a user