Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
342 lines
13 KiB
Go
342 lines
13 KiB
Go
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)
|
||
}
|