Files
xinmi/internal/types/session.go
T

342 lines
13 KiB
Go
Raw Normal View History

package types
import (
"database/sql/driver"
"encoding/json"
"strings"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// FallbackStrategy represents the fallback strategy type
type FallbackStrategy string
const (
FallbackStrategyFixed FallbackStrategy = "fixed" // Fixed response
FallbackStrategyModel FallbackStrategy = "model" // Model fallback response
)
// SummaryConfig represents the summary configuration for a session
type SummaryConfig struct {
// Max tokens
MaxTokens int `json:"max_tokens"`
// Repeat penalty
RepeatPenalty float64 `json:"repeat_penalty"`
// TopK
TopK int `json:"top_k"`
// TopP
TopP float64 `json:"top_p"`
// Frequency penalty
FrequencyPenalty float64 `json:"frequency_penalty"`
// Presence penalty
PresencePenalty float64 `json:"presence_penalty"`
// Prompt
Prompt string `json:"prompt"`
// Context template
ContextTemplate string `json:"context_template"`
// No match prefix
NoMatchPrefix string `json:"no_match_prefix"`
// Temperature
Temperature float64 `json:"temperature"`
// Seed
Seed int `json:"seed"`
// Max completion tokens
MaxCompletionTokens int `json:"max_completion_tokens"`
// Thinking - whether to enable thinking mode
Thinking *bool `json:"thinking"`
}
// ContextCompressionStrategy represents the strategy for context compression
type ContextCompressionStrategy string
const (
// ContextCompressionSlidingWindow keeps the most recent N messages
ContextCompressionSlidingWindow ContextCompressionStrategy = "sliding_window"
// ContextCompressionSmart uses LLM to summarize old messages
ContextCompressionSmart ContextCompressionStrategy = "smart"
)
// ContextConfig configures LLM context management
// This is separate from message storage and manages token limits
type ContextConfig struct {
// Maximum tokens allowed in LLM context
MaxTokens int `json:"max_tokens"`
// Compression strategy: "sliding_window" or "smart"
CompressionStrategy ContextCompressionStrategy `json:"compression_strategy"`
// For sliding_window: number of messages to keep
// For smart: number of recent messages to keep uncompressed
RecentMessageCount int `json:"recent_message_count"`
// Summarize threshold: number of messages before summarization
SummarizeThreshold int `json:"summarize_threshold"`
}
// Session represents the session
type Session struct {
// ID
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
// Title
Title string `json:"title"`
// Description
Description string `json:"description"`
// Workspace ID
TenantID uint64 `json:"tenant_id" gorm:"index"`
// UserID is the owner scope for this session. WeKnora user UUIDs, API
// external-user principals, and embed visitor principals all use this column.
UserID string `json:"user_id,omitempty" gorm:"type:varchar(512);index"`
// IsPinned indicates whether the session is pinned in the list.
IsPinned bool `json:"is_pinned" gorm:"default:false"`
// PinnedAt records when the session was pinned; nil when not pinned.
PinnedAt *time.Time `json:"pinned_at,omitempty"`
// LastRequestState records the input-bar state used the last time this
// session sent a question (agent, model, KB scope, web search, MCPs).
// Persisted on every successful POST to /knowledge-chat or /agent-chat so
// that reopening the session can restore the original request context to
// the chat UI. Stored in the legacy sessions.agent_config JSONB column to
// avoid a new migration; the shape used today is `SessionLastRequestState`.
LastRequestState *SessionLastRequestState `json:"last_request_state,omitempty" gorm:"column:agent_config;type:jsonb"`
// SandboxConfigID pins which sandbox config this session's CURRENT live
// sandbox was created on. Empty means no live sandbox;
// SandboxConfigIDGlobalDefault means the deployment-wide default config.
//
// This is an ephemeral pin that dies with the sandbox, not a permanent
// owner: sessions outlive sandboxes by months, so treating it as
// permanent would make "no session references this config" never true.
SandboxConfigID string `json:"sandbox_config_id,omitempty" gorm:"type:varchar(36)"`
// // Strategy configuration
// KnowledgeBaseID string `json:"knowledge_base_id"` // 关联的知识库ID
// MaxRounds int `json:"max_rounds"` // 多轮保持轮数
// EnableRewrite bool `json:"enable_rewrite"` // 多轮改写开关
// FallbackStrategy FallbackStrategy `json:"fallback_strategy"` // 兜底策略
// FallbackResponse string `json:"fallback_response"` // 固定回复内容
// EmbeddingTopK int `json:"embedding_top_k"` // 向量召回TopK
// KeywordThreshold float64 `json:"keyword_threshold"` // 关键词召回阈值
// VectorThreshold float64 `json:"vector_threshold"` // 向量召回阈值
// RerankModelID string `json:"rerank_model_id"` // 排序模型ID
// RerankTopK int `json:"rerank_top_k"` // 排序TopK
// RerankThreshold float64 `json:"rerank_threshold"` // 排序阈值
// SummaryModelID string `json:"summary_model_id"` // 总结模型ID
// SummaryParameters *SummaryConfig `json:"summary_parameters" gorm:"type:json"` // 总结模型参数
// AgentConfig *SessionAgentConfig `json:"agent_config" gorm:"type:jsonb"` // Agent 配置(会话级别,仅存储enabled和knowledge_bases)
// ContextConfig *ContextConfig `json:"context_config" gorm:"type:jsonb"` // 上下文管理配置(可选)
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
// IMPlatform is the originating IM platform (e.g. "feishu", "wecom") when
// this session is bound to an IM channel. It is not stored on the sessions
// table (it lives in im_channel_sessions) and is populated on read so the
// Web console can classify a session's origin folder without a list query.
IMPlatform string `json:"im_platform,omitempty" gorm:"-"`
// Association relationship, not stored in the database
Messages []Message `json:"-" gorm:"foreignKey:SessionID"`
}
func (s *Session) BeforeCreate(tx *gorm.DB) (err error) {
s.ID = uuid.New().String()
return nil
}
// SessionSourceAPI is the source filter that lists every session created via a
// tenant API key across the whole tenant. It is an admin-only view: the service
// layer requires Admin+ and drops the per-user owner scope so a tenant
// Owner/admin can observe API-key traffic that is otherwise isolated per key.
const SessionSourceAPI = "api"
// SessionSourceWeb is the source filter for a user's own Web-console chats.
const SessionSourceWeb = "web"
// SessionListSourceRequiresAdmin reports whether a session-list source filter
// exposes tenant-wide channel traffic (API / IM / embed) in the Web console.
func SessionListSourceRequiresAdmin(source string) bool {
src := strings.TrimSpace(source)
if src == "" || strings.EqualFold(src, SessionSourceWeb) {
return false
}
return true
}
// SessionRequiresAdminConsoleRead reports whether a session row is channel-
// managed traffic that non-admin web users must not open from the console.
func SessionRequiresAdminConsoleRead(s *Session, imPlatform string) bool {
if s == nil {
return false
}
if IsAPISessionOwnerID(s.UserID) {
return true
}
if strings.HasPrefix(s.Description, EmbedSessionMarkerPrefix) ||
strings.HasPrefix(s.UserID, PrincipalEmbedSession+":") {
return true
}
// Defence in depth for skill maintenance transcripts: the listing hides
// them, and this keeps a leaked session id from being opened by a
// non-admin who happens to own the row.
if IsSkillMaintenanceDescription(s.Description) {
return true
}
return strings.TrimSpace(imPlatform) != ""
}
// IsSkillMaintenanceDescription reports whether description is the reserved
// prefix that hides skill-install sessions from the console list.
func IsSkillMaintenanceDescription(description string) bool {
return strings.HasPrefix(description, SkillMaintenanceSessionMarker)
}
// SanitizeClientSessionDescription keeps the skill-maintenance marker off
// client-writable descriptions. A row that is already a maintenance session
// keeps its stored description so a PUT cannot un-hide it; any other row
// drops a planted marker rather than accepting it.
func SanitizeClientSessionDescription(incoming, existing string) string {
if IsSkillMaintenanceDescription(existing) {
return existing
}
if IsSkillMaintenanceDescription(incoming) {
return ""
}
return incoming
}
// SessionListQuery bundles the parameters for listing sessions.
// UserID empty means "tenant-wide" (used by API-key callers / legacy rows).
// Keyword matches title ILIKE '%keyword%'.
// Source values: "web" (user chats, no IM/embed), "embed" / "embed:{channelID}",
// "api" (all API-key sessions, Admin+ only), or an IM platform name
// (e.g. "feishu", "wechat"). IM and embed sources are also Admin+ only.
// AgentID currently only filters sessions that have an IM channel mapping.
type SessionListQuery struct {
TenantID uint64
UserID string
Keyword string
Source string
AgentID string
Page int
PageSize int
}
// SessionListItem is a session row enriched with its IM origin (when any).
// IM-related fields are populated from the im_channel_sessions table via LEFT JOIN
// and are empty for Web-created sessions.
type SessionListItem struct {
Session
IMPlatform string `json:"im_platform,omitempty" gorm:"column:im_platform"`
IMChatID string `json:"im_chat_id,omitempty" gorm:"column:im_chat_id"`
IMThreadID string `json:"im_thread_id,omitempty" gorm:"column:im_thread_id"`
IMUserID string `json:"im_user_id,omitempty" gorm:"column:im_user_id"`
IMAgentID string `json:"im_agent_id,omitempty" gorm:"column:im_agent_id"`
IMChannelID string `json:"im_channel_id,omitempty" gorm:"column:im_channel_id"`
}
// StringArray represents a list of strings
type StringArray []string
// Value implements the driver.Valuer interface, used to convert StringArray to database value
func (c StringArray) Value() (driver.Value, error) {
return json.Marshal(c)
}
// Scan implements the sql.Scanner interface, used to convert database value to StringArray
func (c *StringArray) Scan(value interface{}) error {
if value == nil {
return nil
}
b, ok := value.([]byte)
if !ok {
return nil
}
return json.Unmarshal(b, c)
}
// Value implements the driver.Valuer interface, used to convert SummaryConfig to database value
func (c *SummaryConfig) Value() (driver.Value, error) {
return json.Marshal(c)
}
// Scan implements the sql.Scanner interface, used to convert database value to SummaryConfig
func (c *SummaryConfig) Scan(value interface{}) error {
if value == nil {
return nil
}
b, ok := value.([]byte)
if !ok {
return nil
}
return json.Unmarshal(b, c)
}
// SessionLastRequestState captures the user-facing input-bar state at the
// time of the most recent QA request on a session. It is purely a UI memory
// aid — none of the fields here drive backend behaviour. They are echoed back
// to the frontend by GetSession so the chat input can restore the same agent,
// model, KB scope, etc. the user had selected last time.
type SessionLastRequestState struct {
AgentID string `json:"agent_id,omitempty"`
AgentEnabled bool `json:"agent_enabled"`
ModelID string `json:"model_id,omitempty"`
KnowledgeBaseIDs []string `json:"knowledge_base_ids,omitempty"`
KnowledgeIDs []string `json:"knowledge_ids,omitempty"`
TagIDs []string `json:"tag_ids,omitempty"`
MCPServiceIDs []string `json:"mcp_service_ids,omitempty"`
SkillNames []string `json:"skill_names,omitempty"`
MentionedItems MentionedItems `json:"mentioned_items,omitempty"`
WebSearchEnabled bool `json:"web_search_enabled"`
}
// Value implements driver.Valuer for SessionLastRequestState (JSONB).
func (s *SessionLastRequestState) Value() (driver.Value, error) {
if s == nil {
return nil, nil
}
return json.Marshal(s)
}
// Scan implements sql.Scanner for SessionLastRequestState (JSONB).
// Tolerates legacy values that may not match the current schema by silently
// ignoring unmarshal errors — the stored row predates this struct.
func (s *SessionLastRequestState) Scan(value interface{}) error {
if value == nil {
return nil
}
var b []byte
switch v := value.(type) {
case []byte:
b = v
case string:
b = []byte(v)
default:
return nil
}
if len(b) == 0 {
return nil
}
if err := json.Unmarshal(b, s); err != nil {
// Tolerate legacy shapes from before this column was repurposed.
return nil
}
return nil
}
// Value implements the driver.Valuer interface, used to convert ContextConfig to database value
func (c *ContextConfig) Value() (driver.Value, error) {
return json.Marshal(c)
}
// Scan implements the sql.Scanner interface, used to convert database value to ContextConfig
func (c *ContextConfig) Scan(value interface{}) error {
if value == nil {
return nil
}
b, ok := value.([]byte)
if !ok {
return nil
}
return json.Unmarshal(b, c)
}