diff --git a/docs/BUILTIN_MODELS.md b/docs/BUILTIN_MODELS.md
new file mode 100644
index 00000000..99e5b739
--- /dev/null
+++ b/docs/BUILTIN_MODELS.md
@@ -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';
+```
+
+注意:移除内置模型标记后,该模型将恢复为普通模型,可以被编辑和删除。
+
diff --git a/frontend/src/api/model/index.ts b/frontend/src/api/model/index.ts
index 16f42cd5..95de6ad9 100644
--- a/frontend/src/api/model/index.ts
+++ b/frontend/src/api/model/index.ts
@@ -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;
diff --git a/frontend/src/components/ModelEditorDialog.vue b/frontend/src/components/ModelEditorDialog.vue
index 39a4a042..e99b1da9 100644
--- a/frontend/src/components/ModelEditorDialog.vue
+++ b/frontend/src/components/ModelEditorDialog.vue
@@ -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;
+ }
+ }
+ }
}
// 过渡动画
diff --git a/frontend/src/components/ModelSelector.vue b/frontend/src/components/ModelSelector.vue
index 49ce1201..98f112b2 100644
--- a/frontend/src/components/ModelSelector.vue
+++ b/frontend/src/components/ModelSelector.vue
@@ -19,6 +19,7 @@
{{ model.name }}
+ 内置
{{ $t('model.defaultTag') }}
diff --git a/frontend/src/views/knowledge/KnowledgeBaseEditorModal.vue b/frontend/src/views/knowledge/KnowledgeBaseEditorModal.vue
index 9a7a1bc4..e6222786 100644
--- a/frontend/src/views/knowledge/KnowledgeBaseEditorModal.vue
+++ b/frontend/src/views/knowledge/KnowledgeBaseEditorModal.vue
@@ -535,6 +535,16 @@ watch(() => props.visible, async (newVal) => {
}, 300)
}
})
+
+// 监听全局设置弹窗关闭后刷新模型列表
+watch(
+ () => uiStore.showSettingsModal,
+ async (visible, previous) => {
+ if (!visible && previous && props.visible) {
+ await loadAllModels()
+ }
+ }
+)
diff --git a/internal/application/repository/model.go b/internal/application/repository/model.go
index 7dab452d..e5f4627f 100644
--- a/internal/application/repository/model.go
+++ b/internal/application/repository/model.go
@@ -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 != "" {
diff --git a/internal/application/service/model.go b/internal/application/service/model.go
index 8fc09b21..180f7cb2 100644
--- a/internal/application/service/model.go
+++ b/internal/application/service/model.go
@@ -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,
diff --git a/internal/handler/initialization.go b/internal/handler/initialization.go
index 082d52f6..761d84a4 100644
--- a/internal/handler/initialization.go
+++ b/internal/handler/initialization.go
@@ -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,
}
}
}
diff --git a/internal/handler/model.go b/internal/handler/model.go
index 83086a41..db051fd1 100644
--- a/internal/handler/model.go
+++ b/internal/handler/model.go
@@ -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,
})
}
diff --git a/internal/types/model.go b/internal/types/model.go
index f6542604..eb7d36e0 100644
--- a/internal/types/model.go
+++ b/internal/types/model.go
@@ -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
diff --git a/migrations/versioned/000007_add_builtin_models.down.sql b/migrations/versioned/000007_add_builtin_models.down.sql
new file mode 100644
index 00000000..dc37d724
--- /dev/null
+++ b/migrations/versioned/000007_add_builtin_models.down.sql
@@ -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;
+
diff --git a/migrations/versioned/000007_add_builtin_models.up.sql b/migrations/versioned/000007_add_builtin_models.up.sql
new file mode 100644
index 00000000..1d566a5d
--- /dev/null
+++ b/migrations/versioned/000007_add_builtin_models.up.sql
@@ -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;
+