Files
sundynix-agentix/sundynix-gateway/internal/store/model.go
T
Blizzard 4fd44380aa refactor(store): DB 规约统一 —— 雪花字符串 id + created/updated/deleted_at 软删
所有数据库映射结构体收敛到统一基类,清理混乱。

- store/base.go:BaseModel{ID string(雪花,bwmarrin/snowflake) PK, CreatedAt, UpdatedAt,
  DeletedAt gorm软删index} + BeforeCreate 生成 id + NewID()。
- 全模型嵌入 BaseModel:User/Task/LLMModel/KB/Doc/Agent(去掉各自 ID uint/CreatedAt);
  Task 业务 id(task_xxx)挪到 TaskID 唯一列,主键统一雪花。
- 模型 id uint→string:admin :id 路由、SetActiveModel/DeleteModel/SaveModel、modelBody.ID。
- 一次性迁移 migrateLegacyIntIDs:检测旧整型 id(AutoMigrate 不改主键类型)→ 备份
  sundynix_model 行(唯一不可再生的 API 密钥)→ 删旧表 → 按新规约重建 → 回灌模型(新雪花 id)。
  其它表(User/Task/KB/Doc/Agent)为可重建测试数据,重置。
- Doc 预埋 Size/Preview/ObjectKey 字段,DocLink 表(为后续 B/C)。

验证:重启 gateway → 日志"回灌 2 条模型配置";PG sundynix_model.id=varchar、有
created_at/updated_at/deleted_at;DeepSeek/百炼 密钥保留(keylen 35);admin 列表返回
雪花 string id + 脱敏 key;健康五灯全绿。gateway build 通过。

注:mcp-go 的 sundynix_user_profile(Profile) 模型尚未套同规约,待跟进对齐。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 16:45:52 +08:00

211 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package store
import (
"context"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// KB 是一个知识库(按 owner 隔离 + 按 kind 组织:文件夹/项目/案件/通用)。
// 表名 sundynix_kb。(owner,name) 唯一 —— 同一用户下知识库名不重复。
// 向量/全文/图谱实际以 "owner/name" 作分区键,保证只有 owner 能查到自己的库。
type KB struct {
BaseModel
Owner string `gorm:"size:64;uniqueIndex:idx_kb_owner_name"`
Name string `gorm:"size:64;uniqueIndex:idx_kb_owner_name"`
Kind string `gorm:"size:16"` // folder / project / case / general
}
func (KB) TableName() string { return "sundynix_kb" }
// ListKB 列出某 owner 的全部知识库(按创建时间)。
func (p *Postgres) ListKB(ctx context.Context, owner string) ([]KB, error) {
if p.db == nil {
return nil, nil
}
var rows []KB
err := p.db.WithContext(ctx).Where("owner = ?", owner).Order("id").Find(&rows).Error
return rows, err
}
// EnsureKB 幂等登记一个知识库(已存在则保持,不覆盖 kind)。
func (p *Postgres) EnsureKB(ctx context.Context, owner, name, kind string) error {
if p.db == nil {
return nil // 降级模式:不持久化注册表,不阻断入库
}
if kind == "" {
kind = "general"
}
return p.db.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "owner"}, {Name: "name"}},
DoNothing: true,
}).Create(&KB{Owner: owner, Name: name, Kind: kind}).Error
}
// Agent 是一份保存的 Agent 编排(React Flow 图 JSON,按 owner 隔离)。
// 表名 sundynix_agent。(owner,name) 唯一 —— 同一用户下编排名不重复。
type Agent struct {
BaseModel
Owner string `gorm:"size:64;uniqueIndex:idx_agent_on"`
Name string `gorm:"size:128;uniqueIndex:idx_agent_on"`
Graph string `gorm:"type:text"` // {nodes,edges} 的 JSON(含布局)
}
func (Agent) TableName() string { return "sundynix_agent" }
// ListAgents 返回某 owner 的全部编排(最近更新在前)。
func (p *Postgres) ListAgents(ctx context.Context, owner string) ([]Agent, error) {
if p.db == nil {
return nil, nil
}
var rows []Agent
err := p.db.WithContext(ctx).Where("owner = ?", owner).Order("updated_at desc").Find(&rows).Error
return rows, err
}
// SaveAgent 新建/更新一份编排(owner+name 唯一,重名覆盖图与更新时间)。
func (p *Postgres) SaveAgent(ctx context.Context, owner, name, graph string) error {
if p.db == nil {
return errStoreDisabled
}
return p.db.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "owner"}, {Name: "name"}},
DoUpdates: clause.Assignments(map[string]any{"graph": graph, "updated_at": time.Now()}),
}).Create(&Agent{Owner: owner, Name: name, Graph: graph}).Error
}
// DeleteAgent 删除某 owner 的一份编排。
func (p *Postgres) DeleteAgent(ctx context.Context, owner, name string) error {
if p.db == nil {
return errStoreDisabled
}
return p.db.WithContext(ctx).Where("owner = ? AND name = ?", owner, name).Delete(&Agent{}).Error
}
// Doc 是入库的一份原始文档/笔记(供 Obsidian 式"文库"浏览:列表 + Markdown 阅读 + 双链)。
// 表名 sundynix_doc。(owner,kb,name) 唯一;按 owner 隔离。
type Doc struct {
BaseModel
Owner string `gorm:"size:64;uniqueIndex:idx_doc_okn"`
KB string `gorm:"size:64;uniqueIndex:idx_doc_okn"`
Name string `gorm:"size:160;uniqueIndex:idx_doc_okn"`
Size int // 原文字数(rune
Preview string `gorm:"size:600"` // 前若干字预览(列表/反链用,不拉全文)
Content string `gorm:"type:text"` // 小文档内联正文;大文档转 MinIO 后置空
ObjectKey string `gorm:"size:160"` // 大文档在 MinIO 的对象键(空=内联 Content
}
func (Doc) TableName() string { return "sundynix_doc" }
// DocLink 是文档间 [[双链]] 的索引(owner+kb 内 from→to),供反链/笔记关系图按 SQL 查询,
// 避免在前端扫全部正文。入库/编辑时按 from 文档重建其出链。
type DocLink struct {
BaseModel
Owner string `gorm:"size:64;index:idx_link_okf"`
KB string `gorm:"size:64;index:idx_link_okf"`
FromName string `gorm:"size:160;index:idx_link_okf"`
ToName string `gorm:"size:160;index"`
}
func (DocLink) TableName() string { return "sundynix_doc_link" }
// SaveDoc 写入/更新一份文档(owner+kb+name 唯一,重名覆盖内容)。
func (p *Postgres) SaveDoc(ctx context.Context, owner, kb, name, content string) error {
if p.db == nil {
return nil
}
return p.db.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "owner"}, {Name: "kb"}, {Name: "name"}},
DoUpdates: clause.AssignmentColumns([]string{"content"}),
}).Create(&Doc{Owner: owner, KB: kb, Name: name, Content: content}).Error
}
// ListVault 返回某 owner 某 kb 的全部文档(名+内容),供文库浏览/双链/反链。
func (p *Postgres) ListVault(ctx context.Context, owner, kb string) ([]Doc, error) {
if p.db == nil {
return nil, nil
}
var rows []Doc
err := p.db.WithContext(ctx).Where("owner = ? AND kb = ?", owner, kb).Order("id").Find(&rows).Error
return rows, err
}
// LLMModel 是一个模型后端配置(控制面:管理员在此登记可用模型)。
// 表名 sundynix_model(遵守前缀约定)。每个 kind 同一时刻仅一条 Active=true。
type LLMModel struct {
BaseModel
Kind string `gorm:"size:16;index"` // chat / embedding
Provider string `gorm:"size:32"` // openai-compatible / vllm
BaseURL string `gorm:"size:255"` // 如 https://api.deepseek.com
APIKey string `gorm:"size:255"`
Model string `gorm:"size:64"` // 如 deepseek-chat / text-embedding-v3
Active bool
}
func (LLMModel) TableName() string { return "sundynix_model" }
// ListModels 列出某 kind 的模型配置(kind 空则全部)。
func (p *Postgres) ListModels(ctx context.Context, kind string) ([]LLMModel, error) {
if p.db == nil {
return nil, nil
}
var rows []LLMModel
q := p.db.WithContext(ctx).Order("id")
if kind != "" {
q = q.Where("kind = ?", kind)
}
err := q.Find(&rows).Error
return rows, err
}
// SaveModel 新增或更新一条模型配置(ID 空则新增,BeforeCreate 生成雪花 ID)。
func (p *Postgres) SaveModel(ctx context.Context, m *LLMModel) error {
if p.db == nil {
return errStoreDisabled
}
if m.ID == "" {
return p.db.WithContext(ctx).Create(m).Error
}
return p.db.WithContext(ctx).Save(m).Error
}
// SetActiveModel 把指定模型设为激活(同 kind 内其余取消),事务保证每 kind 唯一激活。
func (p *Postgres) SetActiveModel(ctx context.Context, id string) error {
if p.db == nil {
return errStoreDisabled
}
return p.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var m LLMModel
if err := tx.First(&m, "id = ?", id).Error; err != nil {
return err
}
if err := tx.Model(&LLMModel{}).Where("kind = ? AND active = ?", m.Kind, true).Update("active", false).Error; err != nil {
return err
}
return tx.Model(&LLMModel{}).Where("id = ?", id).Update("active", true).Error
})
}
// GetActiveModel 返回某 kind 当前激活模型(无则 nil)。
func (p *Postgres) GetActiveModel(ctx context.Context, kind string) (*LLMModel, error) {
if p.db == nil {
return nil, nil
}
var m LLMModel
err := p.db.WithContext(ctx).Where("kind = ? AND active = ?", kind, true).First(&m).Error
if err != nil {
return nil, nil // 未配置激活模型
}
return &m, nil
}
// DeleteModel 删除一条模型配置。
func (p *Postgres) DeleteModel(ctx context.Context, id string) error {
if p.db == nil {
return errStoreDisabled
}
return p.db.WithContext(ctx).Delete(&LLMModel{}, "id = ?", id).Error
}