Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/cmd/gorm-gen/curd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ func main() {
model.PrequeueConfig{},
model.QueueQuotaLimit{},
model.UserBanRecord{},
model.KthenaChatSession{},
model.KthenaChatMessage{},
model.KthenaInferenceTemplate{},
)

// 执行并生成代码
Expand Down
35 changes: 35 additions & 0 deletions backend/cmd/gorm-gen/models/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,36 @@ func modelDownloadSubmissionMigration() *gormigrate.Migration {
}
}

func kthenaChatMigration() *gormigrate.Migration {
return &gormigrate.Migration{
ID: "202608021430",
Migrate: func(tx *gorm.DB) error {
if err := createTableIfMissing(tx, &model.KthenaChatSession{}); err != nil {
return err
}
return createTableIfMissing(tx, &model.KthenaChatMessage{})
},
Rollback: func(tx *gorm.DB) error {
if err := dropTableIfPresent(tx, &model.KthenaChatMessage{}); err != nil {
return err
}
return dropTableIfPresent(tx, &model.KthenaChatSession{})
},
}
}

func kthenaInferenceTemplateMigration() *gormigrate.Migration {
return &gormigrate.Migration{
ID: "202608021500",
Migrate: func(tx *gorm.DB) error {
return createTableIfMissing(tx, &model.KthenaInferenceTemplate{})
},
Rollback: func(tx *gorm.DB) error {
return dropTableIfPresent(tx, &model.KthenaInferenceTemplate{})
},
}
}

func createTableIfMissing(db *gorm.DB, value any) error {
if db.Migrator().HasTable(value) {
return nil
Expand Down Expand Up @@ -1572,6 +1602,8 @@ func main() {
return dropColumnIfPresent(tx, "users", &User{}, "BannedTimestamp")
},
},
kthenaChatMigration(),
kthenaInferenceTemplateMigration(),
modelDownloadSubmissionMigration(),
})

Expand Down Expand Up @@ -1609,6 +1641,9 @@ func main() {
&model.PrequeueConfig{},
&model.QueueQuotaLimit{},
&model.UserBanRecord{},
&model.KthenaChatSession{},
&model.KthenaChatMessage{},
&model.KthenaInferenceTemplate{},
)
if err != nil {
return err
Expand Down
56 changes: 56 additions & 0 deletions backend/cmd/gorm-gen/models/migrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,59 @@ func TestModelDownloadSubmissionMigrationAndRollback(t *testing.T) {
t.Fatal("model download submission table remains after rollback")
}
}

func TestKthenaChatMigrationAndRollback(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:kthena_chat_migration?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
migration := kthenaChatMigration()
if err := migration.Migrate(db); err != nil {
t.Fatalf("migrate: %v", err)
}
if err := migration.Migrate(db); err != nil {
t.Fatalf("idempotent migrate: %v", err)
}
for _, table := range []any{&model.KthenaChatSession{}, &model.KthenaChatMessage{}} {
if !db.Migrator().HasTable(table) {
t.Fatalf("missing migrated table %T", table)
}
}
if err := migration.Rollback(db); err != nil {
t.Fatalf("rollback: %v", err)
}
if err := migration.Rollback(db); err != nil {
t.Fatalf("idempotent rollback: %v", err)
}
for _, table := range []any{&model.KthenaChatSession{}, &model.KthenaChatMessage{}} {
if db.Migrator().HasTable(table) {
t.Fatalf("table remains after rollback: %T", table)
}
}
}

func TestKthenaInferenceTemplateMigrationAndRollback(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:kthena_inference_template_migration?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
migration := kthenaInferenceTemplateMigration()
if err := migration.Migrate(db); err != nil {
t.Fatalf("migrate: %v", err)
}
if err := migration.Migrate(db); err != nil {
t.Fatalf("idempotent migrate: %v", err)
}
if !db.Migrator().HasTable(&model.KthenaInferenceTemplate{}) {
t.Fatal("Kthena inference template table is missing")
}
if err := migration.Rollback(db); err != nil {
t.Fatalf("rollback: %v", err)
}
if err := migration.Rollback(db); err != nil {
t.Fatalf("idempotent rollback: %v", err)
}
if db.Migrator().HasTable(&model.KthenaInferenceTemplate{}) {
t.Fatal("Kthena inference template table remains after rollback")
}
}
63 changes: 63 additions & 0 deletions backend/dao/model/kthena_chat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package model

import (
"time"

"gorm.io/datatypes"
)

// KthenaChatSession stores one user's conversation with one routed Kthena
// deployment. ClientSessionID is deliberately an application-level UUID: the
// web client can keep using its existing local UUID while the database keeps a
// compact numeric primary key for message joins.
//
// A session is scoped by user, account, namespace, deployment and served
// model. The scope is derived from the authorized ModelBooster object rather
// than from a client supplied model name.
//
//nolint:lll // Composite GORM index declarations must stay in a single struct tag.
type KthenaChatSession struct {
ID uint `gorm:"primarykey"`

UserID uint `gorm:"not null;uniqueIndex:idx_kthena_chat_session_scope,priority:1;index:idx_kthena_chat_session_list,priority:1;comment:会话所属用户ID"`
AccountID uint `gorm:"not null;uniqueIndex:idx_kthena_chat_session_scope,priority:2;index:idx_kthena_chat_session_list,priority:2;comment:会话所属账户ID"`
Username string `gorm:"type:varchar(64);not null;comment:创建会话时的用户名快照"`

Namespace string `gorm:"type:varchar(63);not null;uniqueIndex:idx_kthena_chat_session_scope,priority:3;index:idx_kthena_chat_session_list,priority:3;comment:模型部署命名空间"`
ServiceName string `gorm:"type:varchar(63);not null;uniqueIndex:idx_kthena_chat_session_scope,priority:4;index:idx_kthena_chat_session_list,priority:4;comment:ModelBooster名称"`
ModelName string `gorm:"type:varchar(256);not null;uniqueIndex:idx_kthena_chat_session_scope,priority:5;index:idx_kthena_chat_session_list,priority:5;comment:路由模型名快照"`
BackendType string `gorm:"type:varchar(64);not null;comment:推理后端快照"`

ClientSessionID string `gorm:"type:varchar(128);not null;uniqueIndex:idx_kthena_chat_session_scope,priority:6;comment:客户端会话UUID"`
Title string `gorm:"type:varchar(256);not null;default:'';comment:会话标题"`
MessageCount int `gorm:"not null;default:0;comment:消息数"`
LastMessageAt *time.Time `gorm:"index:idx_kthena_chat_session_list,priority:6;comment:最后一条消息时间"`
CreatedAt time.Time `gorm:"not null;index:idx_kthena_chat_session_list,priority:7"`
UpdatedAt time.Time `gorm:"not null;index:idx_kthena_chat_session_list,priority:8"`
}

func (KthenaChatSession) TableName() string {
return "kthena_chat_sessions"
}

// KthenaChatMessage is an ordered message in a KthenaChatSession. ResponseJSON
// stores the original non-streaming OpenAI-compatible completion for safe
// idempotent retries of a client turn.
//
//nolint:lll // Composite GORM index declarations must stay in a single struct tag.
type KthenaChatMessage struct {
ID uint `gorm:"primarykey"`
SessionID uint `gorm:"not null;uniqueIndex:idx_kthena_chat_message_sequence,priority:1;uniqueIndex:idx_kthena_chat_message_turn,priority:1;index:idx_kthena_chat_message_session,priority:1;comment:会话ID"`
Sequence int `gorm:"not null;uniqueIndex:idx_kthena_chat_message_sequence,priority:2;comment:会话内消息顺序"`
Role string `gorm:"type:varchar(32);not null;comment:OpenAI消息角色"`
Content string `gorm:"type:text;not null;comment:消息正文"`
// ClientTurnID is populated only for the user message of an atomic turn.
// Nullable values keep normal CRUD message replacement unconstrained.
ClientTurnID *string `gorm:"type:varchar(128);uniqueIndex:idx_kthena_chat_message_turn,priority:2;comment:客户端幂等请求ID"`
ResponseJSON datatypes.JSON `gorm:"type:jsonb;comment:助手消息对应的原始模型响应"`
CreatedAt time.Time `gorm:"not null;index:idx_kthena_chat_message_session,priority:2"`
}

func (KthenaChatMessage) TableName() string {
return "kthena_chat_messages"
}
29 changes: 29 additions & 0 deletions backend/dao/model/kthena_inference_template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package model

import (
"gorm.io/datatypes"
"gorm.io/gorm"
)

// KthenaInferenceTemplate is a private reusable deployment preset. It is
// intentionally scoped by both user and account, so a template never becomes
// visible when a browser switches identity or account context.
//
// Config keeps the complete form payload as JSON. The deployment API remains
// authoritative when a template is eventually submitted, while templates can
// evolve without a schema migration for every new form field.
//
//nolint:lll // Composite GORM index declarations must stay in a single struct tag.
type KthenaInferenceTemplate struct {
gorm.Model

UserID uint `gorm:"not null;uniqueIndex:idx_kthena_inference_template_scope,priority:1;index:idx_kthena_inference_template_list,priority:1;comment:模板所属用户ID"`
AccountID uint `gorm:"not null;uniqueIndex:idx_kthena_inference_template_scope,priority:2;index:idx_kthena_inference_template_list,priority:2;comment:模板所属账户ID"`
Name string `gorm:"type:varchar(64);not null;uniqueIndex:idx_kthena_inference_template_scope,priority:3;comment:模板名称"`
Description string `gorm:"type:varchar(512);not null;default:'';comment:模板说明"`
Config datatypes.JSON `gorm:"type:jsonb;not null;comment:模型部署表单配置"`
}

func (KthenaInferenceTemplate) TableName() string {
return "kthena_inference_templates"
}
4 changes: 3 additions & 1 deletion backend/dao/model/system_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const (
ConfigKeyLLMModelName = "LLM_MODEL_NAME"

// 功能开关配置键
ConfigKeyEnableGpuAnalysis = "ENABLE_GPU_ANALYSIS" // 值: "true" or "false"
ConfigKeyEnableGpuAnalysis = "ENABLE_GPU_ANALYSIS" // 值: "true" or "false"
ConfigKeyEnableKthenaInference = "ENABLE_KTHENA_INFERENCE" // 值: "true" or "false"

// Billing 功能与调度配置键
ConfigKeyEnableBillingFeature = "ENABLE_BILLING_FEATURE"
Expand Down Expand Up @@ -48,6 +49,7 @@ var DefaultConfigKeys = []string{
ConfigKeyLLMAPIKey,
ConfigKeyLLMModelName,
ConfigKeyEnableGpuAnalysis,
ConfigKeyEnableKthenaInference,
ConfigKeyEnableBillingFeature,
ConfigKeyEnableBillingActive,
ConfigKeyEnableRunningSettlement,
Expand Down
Loading
Loading