feat(builtin-models): add YAML-based declarative config with ${ENV} interpolation

Allow built-in models to be declared in config/builtin_models.yaml
instead of inserting rows via SQL. On every startup the file is read
and each entry is UPSERT-ed into the models table (is_builtin=true)
by stable id.

Any string field may reference an environment variable with ${NAME}.
Unset variables are left as the literal placeholder so
misconfiguration surfaces clearly in provider calls rather than
failing silently with an empty token.

The file is optional: missing file, parse errors, and per-entry
upsert failures all log a warning without aborting startup.
docker-compose.yml adds env_file (.env, required:false) so
deployment-specific variables are passed through automatically.
This commit is contained in:
jackson.jia
2026-05-22 23:15:12 +08:00
committed by lyingbug
parent e9be53e830
commit d439b3ae07
8 changed files with 475 additions and 113 deletions
+21
View File
@@ -18,6 +18,27 @@ GIN_MODE=release
# 可选值:true(自动放在 LOG_PATH 同目录下 llm_debug.log)、false/空(关闭)、或指定文件路径
# LLM_DEBUG_LOG=true
# ========== 内置模型(Built-in Models)声明式配置(可选) ==========
# 如果启用了 config/builtin_models.yaml(见 config/builtin_models.yaml.example),
# YAML 中的 ${NAME} 占位符会在应用启动期从这里读取真实值。变量名由 YAML 自行
# 指定,下面给出常见命名作为参考;按你自己的部署改名也可以。详见
# docs/BUILTIN_MODELS.md。
#
# LLM_MODEL_NAME=
# LLM_BASE_URL=
# LLM_API_KEY=
# LLM_PROVIDER=openai
#
# EMBEDDING_MODEL_NAME=
# EMBEDDING_BASE_URL=
# EMBEDDING_API_KEY=
# EMBEDDING_PROVIDER=openai
#
# RERANK_MODEL_NAME=
# RERANK_BASE_URL=
# RERANK_API_KEY=
# RERANK_PROVIDER=generic
# ========== Langfuse 可观测性(可选) ==========
# 用于追踪 chat / embedding / rerank / VLM / ASR 模型调用,统计 token 消耗。
# 详细说明:docs/Langfuse集成.md
+75
View File
@@ -0,0 +1,75 @@
# config/builtin_models.yaml — declarative built-in models.
#
# Copy to config/builtin_models.yaml (or point BUILTIN_MODELS_CONFIG at a custom
# path) to enable. Each entry below is inserted into the `models` table with
# is_builtin=true and becomes visible to every tenant.
#
# How values reach the file
# -------------------------
# Any string field can reference an env var with ${NAME}. Unset env vars are
# left as the literal ${NAME} so misconfiguration surfaces clearly in upstream
# API calls instead of producing a silent empty token. Non-string fields
# (type, source, is_default, dimension, truncate_prompt_tokens) must stay
# literal because YAML parses them as their target type.
#
# How env vars reach the container
# --------------------------------
# By default the app service in docker-compose.yml loads .env via
# env_file:
# - path: .env
# required: false
# so anything you put in .env is auto-passed to the container. There is no
# need to list each var separately under `environment:`.
#
# Re-applied on every application startup — editing the file and restarting is
# enough for changes to take effect. Entries removed from the file are NOT
# auto-deleted from the database; clean them up manually if you need to.
builtin_models: []
# ----- Example: env-driven entries — uncomment and adapt --------------------
#
# builtin_models:
# - id: builtin-llm-default
# type: KnowledgeQA # KnowledgeQA | Embedding | Rerank | VLLM | ASR
# source: remote # remote (default) | local | ...
# is_default: true
# name: ${LLM_MODEL_NAME}
# parameters:
# base_url: ${LLM_BASE_URL}
# api_key: ${LLM_API_KEY}
# provider: ${LLM_PROVIDER} # openai | generic | aliyun | moonshot | ...
#
# - id: builtin-embedding-default
# type: Embedding
# source: remote
# is_default: true
# name: ${EMBEDDING_MODEL_NAME}
# parameters:
# base_url: ${EMBEDDING_BASE_URL}
# api_key: ${EMBEDDING_API_KEY}
# provider: ${EMBEDDING_PROVIDER}
# embedding_parameters:
# dimension: 1536
# truncate_prompt_tokens: 0
#
# - id: builtin-rerank-default
# type: Rerank
# source: remote
# parameters:
# base_url: ${RERANK_BASE_URL}
# api_key: ${RERANK_API_KEY}
# provider: ${RERANK_PROVIDER}
#
# ----- Example: literal values (no env indirection) -------------------------
#
# builtin_models:
# - id: builtin-openai-chat
# name: gpt-4o-mini
# type: KnowledgeQA
# source: remote
# is_default: false
# parameters:
# base_url: https://api.openai.com/v1
# api_key: ${OPENAI_API_KEY}
# provider: openai
+11
View File
@@ -41,12 +41,23 @@ services:
- ./config/config.yaml:/app/config/config.yaml
# Optional: mount custom skills directory (allows adding skills without rebuilding image)
- ./skills/preloaded:/app/skills/preloaded
# Optional: declarative built-in models. Copy config/builtin_models.yaml.example
# to config/builtin_models.yaml, edit it for your deployment, then
# uncomment the line below. See docs/BUILTIN_MODELS.md.
# - ./config/builtin_models.yaml:/app/config/builtin_models.yaml:ro
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
# Optional: source .env so deployment-specific vars (e.g. those referenced
# by config/builtin_models.yaml via ${ENV}) reach the app container without
# having to be listed individually under `environment:`. Falls back
# silently when .env is absent (e.g. fresh upstream clone).
env_file:
- path: .env
required: false
environment:
- LOG_LEVEL=${LOG_LEVEL:-}
- COS_SECRET_ID=${COS_SECRET_ID:-}
+187 -105
View File
@@ -13,163 +13,245 @@
## 如何添加内置模型
内置模型需要通过数据库直接插入。以下是添加内置模型的步骤:
WeKnora 支持两种方式添加内置模型:**推荐**使用 YAML 声明式配置(自动幂等下发),保留 SQL 直插作为兼容路径。
### 1. 准备模型数据
### 方式一(推荐):YAML 配置文件
首先,确保你已经有了要设置为内置模型的模型配置信息,包括:
- 模型名称(name)
- 模型类型(type):`KnowledgeQA`、`Embedding`、`Rerank` 或 `VLLM`
- 模型来源(source):`local` 或 `remote`
- 模型参数(parameters):包括 base_url、api_key、provider 等
- 租户ID(tenant_id):建议使用小于10000的租户ID,避免冲突
#### 文件位置
**支持的服务商(provider)**:`generic`(自定义)、`openai`、`aliyun`、`zhipu`、`volcengine`、`hunyuan`、`deepseek`、`minimax`、`mimo`、`siliconflow`、`jina`、`openrouter`、`gemini`、`modelscope`、`moonshot`、`qianfan`、`qiniu`、`longcat`、`gpustack`
默认路径是 `config/builtin_models.yaml`(与 `config.yaml`、`builtin_agents.yaml` 同目录)。如需挂载到其他位置,设置环境变量 `BUILTIN_MODELS_CONFIG=/absolute/path/builtin_models.yaml` 覆盖。
### 2. 执行 SQL 插入语句
文件不存在时启动期会跳过、不报错;解析失败仅记录 Warning、不影响主流程。每次应用启动会重新读取并按 `id` 字段 UPSERT 到 `models` 表(保留 `created_at`,刷新其他字段)。
使用以下 SQL 语句插入内置模型:
#### Schema
```yaml
builtin_models:
- id: <required, stable, UPSERT key>
tenant_id: <int, default 10000> # 与 tenants_id_seq 起点对齐
name: <string>
type: KnowledgeQA | Embedding | Rerank | VLLM | ASR
source: <string, default "remote"> # local | remote | aliyun | ...
description: <string, optional>
is_default: <bool, default false>
status: <string, default "active">
parameters:
base_url: <string>
api_key: <string, supports ${ENV_VAR}>
provider: <string> # openai | generic | moonshot | ...
embedding_parameters: # 仅 Embedding 类型
dimension: <int>
truncate_prompt_tokens: <int>
```
#### 完整示例
```yaml
builtin_models:
- id: builtin-openai-chat
name: gpt-4o-mini
type: KnowledgeQA
source: remote
description: OpenAI 默认对话模型
is_default: false
parameters:
base_url: https://api.openai.com/v1
api_key: ${OPENAI_API_KEY}
provider: openai
- id: builtin-openai-embeddings
name: text-embedding-3-small
type: Embedding
source: remote
parameters:
base_url: https://api.openai.com/v1
api_key: ${OPENAI_API_KEY}
provider: openai
embedding_parameters:
dimension: 1536
truncate_prompt_tokens: 0
- id: builtin-rerank
name: bge-reranker-v2-m3
type: Rerank
source: remote
parameters:
base_url: ${RERANK_BASE_URL}
api_key: ${RERANK_API_KEY}
provider: generic
```
#### `${ENV}` 插值
`api_key` / `base_url` / `name` 等任何**字符串**字段都可以引用环境变量:`${OPENAI_API_KEY}` 会在启动时被对应的 `os.Getenv("OPENAI_API_KEY")` 替换。
- 环境变量存在 → 替换为实际值
- 环境变量不存在 → **保留字面 `${OPENAI_API_KEY}`** 字符串(让 401 错误能直接看出来 env 没设,便于排查)
- 不支持 `${VAR:-default}` 这种 shell 扩展,行为与现有 `config.yaml` 的插值实现一致
- **非字符串字段不能 env 化**(如 `type`、`dimension`、`is_default`),因为它们必须按 YAML 的目标类型解析
#### env 变量怎么进入容器
`docker-compose.yml` 的 `app` 服务已经预置了:
```yaml
env_file:
- path: .env
required: false
```
意味着把变量值写到项目根目录的 `.env` 文件里,启动时自动透传到容器。**无需**在 `environment:` 块里逐个透传。`required: false` 保证 `.env` 不存在时容器仍可启动(适配上游 fresh clone 场景)。
仓库的 `.env.example` 顶部预留了 **Built-in Models** 注释段,列出 LLM / Embedding / Rerank 的参考变量名作为起点;复制 `.env.example` 为 `.env` 后取消注释并填值即可。变量名由 YAML 自行决定,参考段只是常见样板,不是保留字。
完整端到端示例:
`.env`
```bash
LLM_MODEL_NAME=gpt-4o-mini
LLM_BASE_URL=https://api.openai.com/v1
LLM_API_KEY=sk-...
LLM_PROVIDER=openai
```
`config/builtin_models.yaml`
```yaml
builtin_models:
- id: builtin-llm-default
type: KnowledgeQA
is_default: true
name: ${LLM_MODEL_NAME}
parameters:
base_url: ${LLM_BASE_URL}
api_key: ${LLM_API_KEY}
provider: ${LLM_PROVIDER}
```
启动:
```bash
docker compose up -d
```
#### 启动后验证
```bash
docker compose logs app | grep -E 'Built-in models? config'
```
会看到类似:
```
Built-in model upserted: id=builtin-openai-chat name=gpt-4o-mini type=KnowledgeQA
Built-in model upserted: id=builtin-openai-embeddings name=text-embedding-3-small type=Embedding
Built-in models config applied: 2 entries from /app/config/builtin_models.yaml.
```
#### Docker 部署
在 `docker-compose.yml` 的 `app` 服务 `volumes` 块挂载文件:
```yaml
services:
app:
volumes:
- ./config/builtin_models.yaml:/app/config/builtin_models.yaml:ro
```
仓库提供了 `config/builtin_models.yaml.example` 作为起点,复制为 `config/builtin_models.yaml` 后按需修改。
### 方式二:直接 SQL 插入
支持的 provider:`generic`(自定义)、`openai`、`aliyun`、`zhipu`、`volcengine`、`hunyuan`、`deepseek`、`minimax`、`mimo`、`siliconflow`、`jina`、`openrouter`、`gemini`、`modelscope`、`moonshot`、`qianfan`、`qiniu`、`longcat`、`gpustack`
```sql
-- 示例:插入一个 LLM 内置模型
-- 示例:LLM 内置模型
INSERT INTO models (
id,
tenant_id,
name,
type,
source,
description,
parameters,
is_default,
status,
is_builtin
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", "provider": "openai"}'::jsonb, -- 参数(JSON格式)
false, -- 是否默认
'active', -- 状态
true -- 标记为内置模型
'builtin-llm-001',
10000,
'gpt-4o-mini',
'KnowledgeQA',
'remote',
'系统内置 LLM 模型',
'{"base_url": "https://api.openai.com/v1", "api_key": "sk-xxx", "provider": "openai"}'::jsonb,
false,
'active',
true
) ON CONFLICT (id) DO NOTHING;
-- 示例:插入一个 Embedding 内置模型
-- Embedding
INSERT INTO models (
id,
tenant_id,
name,
type,
source,
description,
parameters,
is_default,
status,
is_builtin
id, tenant_id, name, type, source, description,
parameters, is_default, status, is_builtin
) VALUES (
'builtin-embedding-001',
10000,
'text-embedding-ada-002',
'text-embedding-3-small',
'Embedding',
'remote',
'内置 Embedding 模型',
'系统内置 Embedding 模型',
'{"base_url": "https://api.openai.com/v1", "api_key": "sk-xxx", "provider": "openai", "embedding_parameters": {"dimension": 1536, "truncate_prompt_tokens": 0}}'::jsonb,
false,
'active',
true
) ON CONFLICT (id) DO NOTHING;
-- 示例:插入一个 ReRank 内置模型
-- Rerank
INSERT INTO models (
id,
tenant_id,
name,
type,
source,
description,
parameters,
is_default,
status,
is_builtin
id, tenant_id, name, type, source, description,
parameters, is_default, status, is_builtin
) VALUES (
'builtin-rerank-001',
10000,
'bge-reranker-base',
'bge-reranker-v2-m3',
'Rerank',
'remote',
'内置 ReRank 模型',
'系统内置 Rerank 模型',
'{"base_url": "https://api.jina.ai/v1", "api_key": "jina-xxx", "provider": "jina"}'::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://dashscope.aliyuncs.com/compatible-mode/v1", "api_key": "sk-xxx", "provider": "aliyun"}'::jsonb,
false,
'active',
true
) ON CONFLICT (id) DO NOTHING;
```
### 3. 验证插入结果
执行以下 SQL 查询验证内置模型是否成功插入:
### 验证插入结果
```sql
SELECT id, name, type, is_builtin, status
FROM models
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 = '模型名称';
UPDATE models
SET is_builtin = true
WHERE id = '模型ID';
```
## 移除内置模型
如果需要移除内置模型标记(恢复为普通模型),执行:
YAML 配置删除条目后**不会**自动清理 DB 里的对应行(避免误删 ops 手工维护的内置模型)。如需删除:
```sql
UPDATE models
SET is_builtin = false
WHERE id = '模型ID';
-- 取消 builtin 标记,恢复为普通模型
UPDATE models SET is_builtin = false WHERE id = '模型ID';
-- 或直接删除
DELETE FROM models WHERE id = '模型ID';
```
注意:移除内置模型标记后,该模型将恢复为普通模型,可以被编辑和删除。
## 注意事项
1. **ID 命名规范**:建议使用 `builtin-{type}-{slug}` 的格式,例如 `builtin-openai-chat`、`builtin-rerank`
2. **租户ID**:内置模型可以属于任意租户,默认 `10000`(与 `tenants_id_seq` 起点一致)
3. **YAML 与 SQL 并存**:两种方式可以同时使用,YAML 不会覆盖通过 SQL 插入但未在 YAML 出现的 builtin 行
4. **重启即生效**:修改 YAML 后 `docker compose restart app` 即可让新配置生效
5. **加密**:API Key 在 `parameters` JSONB 中以加密形式存储(若 `SYSTEM_AES_KEY` 已配置),未配置时降级为明文兼容路径
6. **安全性**:前端会自动隐藏内置模型的 API Key 和 Base URL,但数据库中的原始数据仍然存在,请妥善保管数据库访问权限
+19
View File
@@ -427,6 +427,24 @@ type FebriText struct {
WithNoTag string `yaml:"with_no_tag" json:"with_no_tag"`
}
// resolvedConfigDir holds the directory of the loaded config file. Populated by
// LoadConfig and read by ConfigDir(); empty until LoadConfig has run.
var resolvedConfigDir string
// ConfigDir returns the directory containing the loaded config.yaml. Other
// startup code (e.g. builtin model loader) uses this to locate sibling config
// files like builtin_models.yaml without re-implementing viper search rules.
// Falls back to "./config" when LoadConfig has not been called yet.
func ConfigDir() string {
if resolvedConfigDir != "" {
return resolvedConfigDir
}
if f := viper.ConfigFileUsed(); f != "" {
return filepath.Dir(f)
}
return "./config"
}
// LoadConfig 从配置文件加载配置
func LoadConfig() (*Config, error) {
// 设置配置文件名和路径
@@ -478,6 +496,7 @@ func LoadConfig() (*Config, error) {
// 加载提示词模板(从目录或配置文件)
configDir := filepath.Dir(viper.ConfigFileUsed())
resolvedConfigDir = configDir
promptTemplates, err := loadPromptTemplates(configDir)
if err != nil {
fmt.Printf("Warning: failed to load prompt templates from directory: %v\n", err)
+5
View File
@@ -536,6 +536,11 @@ func initDatabase(cfg *config.Config) (*gorm.DB, error) {
// The SQL migration marks KBs that have documents but no provider with "__pending_env__";
// we replace that with the actual STORAGE_TYPE from the environment.
resolveStorageProviderPending(db)
// Post-migration: declarative built-in models from config/builtin_models.yaml (optional).
if err := types.LoadBuiltinModelsConfig(context.Background(), db, config.ConfigDir()); err != nil {
logger.Warnf(context.Background(), "Load builtin models config failed: %v", err)
}
} else {
logger.Infof(context.Background(), "Auto-migration is disabled (AUTO_MIGRATE=false)")
}
+150
View File
@@ -0,0 +1,150 @@
package types
import (
"context"
"fmt"
"os"
"path/filepath"
"regexp"
"gopkg.in/yaml.v3"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// BuiltinModelEntry mirrors one entry in builtin_models.yaml.
// Each entry becomes a row in the models table with is_builtin=true.
type BuiltinModelEntry struct {
ID string `yaml:"id"`
TenantID uint64 `yaml:"tenant_id"`
Name string `yaml:"name"`
Type ModelType `yaml:"type"`
Source ModelSource `yaml:"source"`
Description string `yaml:"description"`
IsDefault bool `yaml:"is_default"`
Status ModelStatus `yaml:"status"`
Parameters ModelParameters `yaml:"parameters"`
}
type builtinModelsFile struct {
BuiltinModels []BuiltinModelEntry `yaml:"builtin_models"`
}
// builtinModelEnvPattern matches ${NAME} placeholders. Mirrors the pattern in
// internal/config/config.go so YAML interpolation behaves identically to the
// main config.yaml flow.
var builtinModelEnvPattern = regexp.MustCompile(`\${([^}]+)}`)
// interpolateBuiltinModelEnv substitutes ${NAME} occurrences with the
// corresponding os.Getenv value. Unset vars are left as the literal ${NAME}
// so misconfiguration surfaces visibly in downstream provider calls instead
// of failing silently with an empty token.
func interpolateBuiltinModelEnv(s string) string {
return builtinModelEnvPattern.ReplaceAllStringFunc(s, func(m string) string {
name := m[2 : len(m)-1]
if v := os.Getenv(name); v != "" {
return v
}
return m
})
}
// LoadBuiltinModelsConfig reads builtin_models.yaml (or the path pointed to by
// BUILTIN_MODELS_CONFIG) and UPSERTs each entry into the models table.
//
// Behaviour:
// - file not found / mount point is a directory / path unset: no-op
// - YAML parse error: prints a warning, returns nil (never aborts startup)
// - per-entry UPSERT error: prints a warning, continues with the next entry
// - never deletes entries that disappeared from YAML: operators may have
// seeded extras manually via SQL; removing them here would be surprising
func LoadBuiltinModelsConfig(ctx context.Context, db *gorm.DB, configDir string) error {
path := os.Getenv("BUILTIN_MODELS_CONFIG")
if path == "" {
path = filepath.Join(configDir, "builtin_models.yaml")
}
// Treat "missing", "is a directory", and other non-regular-file cases the
// same way. Docker bind-mounting a non-existent source file silently
// substitutes a directory; we don't want that to spam WARN logs.
info, statErr := os.Stat(path)
if statErr != nil || !info.Mode().IsRegular() {
fmt.Printf("Built-in models config not present at %s; skipping.\n", path)
return nil
}
raw, err := os.ReadFile(path)
if err != nil {
fmt.Printf("Warning: read built-in models config %s failed: %v; skipping.\n", path, err)
return nil
}
expanded := interpolateBuiltinModelEnv(string(raw))
var file builtinModelsFile
if err := yaml.Unmarshal([]byte(expanded), &file); err != nil {
fmt.Printf("Warning: parse built-in models config %s failed: %v; skipping.\n", path, err)
return nil
}
if len(file.BuiltinModels) == 0 {
fmt.Printf("Built-in models config %s contains no entries.\n", path)
return nil
}
applied := 0
for i := range file.BuiltinModels {
e := &file.BuiltinModels[i]
if e.ID == "" {
fmt.Printf("Warning: built-in model entry %d has empty id; skipping.\n", i)
continue
}
m := e.toModel()
res := db.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{
"tenant_id", "name", "type", "source", "description",
"parameters", "is_default", "status", "is_builtin", "updated_at",
}),
}).Create(&m)
if res.Error != nil {
fmt.Printf("Warning: upsert built-in model %s failed: %v; continuing.\n", e.ID, res.Error)
continue
}
applied++
fmt.Printf("Built-in model upserted: id=%s name=%s type=%s\n", e.ID, e.Name, e.Type)
}
fmt.Printf("Built-in models config applied: %d entries from %s.\n", applied, path)
return nil
}
// toModel converts a YAML entry to a runtime Model with sensible defaults.
// tenant_id defaults to 10000 (matches the seed value of tenants_id_seq);
// source defaults to "remote"; status defaults to "active". IsBuiltin is
// always forced to true regardless of YAML input.
func (e *BuiltinModelEntry) toModel() Model {
tenantID := e.TenantID
if tenantID == 0 {
tenantID = 10000
}
source := e.Source
if source == "" {
source = ModelSourceRemote
}
status := e.Status
if status == "" {
status = ModelStatusActive
}
return Model{
ID: e.ID,
TenantID: tenantID,
Name: e.Name,
Type: e.Type,
Source: source,
Description: e.Description,
Parameters: e.Parameters,
IsDefault: e.IsDefault,
IsBuiltin: true,
Status: status,
}
}
+7 -8
View File
@@ -164,14 +164,13 @@ func (c *ModelParameters) Scan(value interface{}) error {
return nil
}
// BeforeCreate is a GORM hook that runs before creating a new model record
// Automatically generates a UUID for new models
// Parameters:
// - tx: GORM database transaction
//
// Returns:
// - error: Any error encountered during the hook execution
// BeforeCreate is a GORM hook that runs before creating a new model record.
// Generates a UUID only when the caller has not supplied an ID — preserves
// stable IDs declared in built-in model YAML config while keeping the
// existing UUID behaviour for API-driven model creation.
func (m *Model) BeforeCreate(tx *gorm.DB) (err error) {
m.ID = uuid.New().String()
if m.ID == "" {
m.ID = uuid.New().String()
}
return nil
}