- Introduced new status field in the attachment table to track processing states: uploading, uploaded, indexing, indexed, upload_failed, and index_failed. - Added progress and error fields to provide detailed information during the attachment processing workflow. - Implemented AttachmentFilter struct to support filtering attachments by status. - Updated SaveAttachment and GetAttachments methods to handle new status and progress information. - Enhanced tests to validate attachment status management and filtering functionality.
2253 lines
59 KiB
Go
2253 lines
59 KiB
Go
package store
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/yaoapp/gou/connector"
|
|
"github.com/yaoapp/gou/session"
|
|
"github.com/yaoapp/kun/log"
|
|
"github.com/yaoapp/xun/capsule"
|
|
"github.com/yaoapp/xun/dbal/query"
|
|
"github.com/yaoapp/xun/dbal/schema"
|
|
"github.com/yaoapp/yao/neo/i18n"
|
|
)
|
|
|
|
// Package conversation provides functionality for managing chat conversations and assistants.
|
|
|
|
// Xun implements the Conversation interface using a database backend.
|
|
// It provides functionality for:
|
|
// - Managing chat conversations and their message histories
|
|
// - Organizing chats with pagination and date-based grouping
|
|
// - Handling chat metadata like titles and creation dates
|
|
// - Managing AI assistants with their configurations and metadata
|
|
// - Managing file attachments with metadata and access control
|
|
// - Managing knowledge collections for AI assistants
|
|
// - Supporting data expiration through TTL settings
|
|
type Xun struct {
|
|
query query.Query
|
|
schema schema.Schema
|
|
setting Setting
|
|
cleanTicker *time.Ticker
|
|
cleanStop chan bool
|
|
}
|
|
|
|
// Public interface methods:
|
|
//
|
|
// NewXun creates a new conversation instance with the given settings
|
|
// UpdateChatTitle updates the title of a specific chat
|
|
// GetChats retrieves a paginated list of chats grouped by date
|
|
// GetChat retrieves a specific chat and its message history
|
|
// GetHistory retrieves the message history for a specific chat
|
|
// SaveHistory saves new messages to a chat's history
|
|
// DeleteChat deletes a specific chat and its history
|
|
// DeleteAllChats deletes all chats and their histories for a user
|
|
// SaveAssistant creates or updates an assistant
|
|
// DeleteAssistant deletes an assistant by assistant_id
|
|
// GetAssistants retrieves a paginated list of assistants with filtering
|
|
// GetAssistant retrieves a single assistant by assistant_id
|
|
// SaveAttachment creates or updates an attachment
|
|
// DeleteAttachment deletes an attachment by file_id
|
|
// GetAttachments retrieves a paginated list of attachments with filtering
|
|
// GetAttachment retrieves a single attachment by file_id
|
|
// SaveKnowledge creates or updates a knowledge collection
|
|
// DeleteKnowledge deletes a knowledge collection by collection_id
|
|
// GetKnowledges retrieves a paginated list of knowledge collections with filtering
|
|
// GetKnowledge retrieves a single knowledge collection by collection_id
|
|
|
|
// NewXun create a new xun store
|
|
func NewXun(setting Setting) (Store, error) {
|
|
conv := &Xun{setting: setting}
|
|
if setting.Connector == "default" {
|
|
conv.query = capsule.Global.Query()
|
|
conv.schema = capsule.Global.Schema()
|
|
} else {
|
|
conn, err := connector.Select(setting.Connector)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conv.query, err = conn.Query()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conv.schema, err = conn.Schema()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err := conv.initialize()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return conv, nil
|
|
}
|
|
|
|
// Rename the following functions to start with lowercase letters to make them private:
|
|
|
|
func (conv *Xun) newQuery() query.Query {
|
|
qb := conv.query.New()
|
|
qb.Table(conv.getHistoryTable())
|
|
return qb
|
|
}
|
|
|
|
func (conv *Xun) newQueryChat() query.Query {
|
|
qb := conv.query.New()
|
|
qb.Table(conv.getChatTable())
|
|
return qb
|
|
}
|
|
|
|
func (conv *Xun) clean() {
|
|
nums, err := conv.newQuery().Where("expired_at", "<=", time.Now()).Delete()
|
|
if err != nil {
|
|
log.Error("Clean the conversation table error: %s", err.Error())
|
|
return
|
|
}
|
|
|
|
if nums > 0 {
|
|
log.Trace("Clean the conversation table: %s %d", conv.setting.Prefix, nums)
|
|
}
|
|
}
|
|
|
|
// startAutoClean starts the automatic cleanup routine
|
|
func (conv *Xun) startAutoClean() {
|
|
if conv.cleanTicker != nil {
|
|
conv.stopAutoClean() // Stop existing ticker if any
|
|
}
|
|
|
|
conv.cleanTicker = time.NewTicker(1 * time.Hour) // Clean every hour
|
|
conv.cleanStop = make(chan bool)
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-conv.cleanTicker.C:
|
|
conv.clean()
|
|
case <-conv.cleanStop:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
log.Trace("Started automatic cleanup for: %s", conv.setting.Prefix)
|
|
}
|
|
|
|
// stopAutoClean stops the automatic cleanup routine
|
|
func (conv *Xun) stopAutoClean() {
|
|
if conv.cleanTicker != nil {
|
|
conv.cleanTicker.Stop()
|
|
conv.cleanTicker = nil
|
|
}
|
|
|
|
if conv.cleanStop != nil {
|
|
close(conv.cleanStop)
|
|
conv.cleanStop = nil
|
|
}
|
|
|
|
log.Trace("Stopped automatic cleanup for: %s", conv.setting.Prefix)
|
|
}
|
|
|
|
// Close stops the automatic cleanup and closes resources
|
|
func (conv *Xun) Close() error {
|
|
conv.stopAutoClean()
|
|
return nil
|
|
}
|
|
|
|
// Rename Init to initialize to avoid conflicts
|
|
func (conv *Xun) initialize() error {
|
|
// Initialize history table
|
|
if err := conv.initHistoryTable(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize chat table
|
|
if err := conv.initChatTable(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize assistant table
|
|
if err := conv.initAssistantTable(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize attachment table
|
|
if err := conv.initAttachmentTable(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize knowledge table
|
|
if err := conv.initKnowledgeTable(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Start automatic cleanup if TTL is enabled
|
|
if conv.setting.TTL > 0 {
|
|
conv.startAutoClean()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (conv *Xun) initHistoryTable() error {
|
|
historyTable := conv.getHistoryTable()
|
|
has, err := conv.schema.HasTable(historyTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the history table
|
|
if !has {
|
|
err = conv.schema.CreateTable(historyTable, func(table schema.Blueprint) {
|
|
table.ID("id")
|
|
table.String("sid", 255).Index()
|
|
table.String("cid", 200).Null().Index()
|
|
table.String("uid", 255).Null().Index()
|
|
table.String("role", 200).Null().Index()
|
|
table.String("name", 200).Null().Index()
|
|
table.Text("content").Null()
|
|
table.JSON("context").Null()
|
|
table.String("assistant_id", 200).Null().Index()
|
|
table.String("assistant_name", 200).Null()
|
|
table.String("assistant_avatar", 200).Null()
|
|
table.JSON("mentions").Null()
|
|
table.Boolean("silent").SetDefault(false).Index()
|
|
table.TimestampTz("created_at").SetDefaultRaw("CURRENT_TIMESTAMP").Index()
|
|
table.TimestampTz("updated_at").Null().Index()
|
|
table.TimestampTz("expired_at").Null().Index()
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Trace("Create the conversation history table: %s", historyTable)
|
|
}
|
|
|
|
// Validate the table
|
|
tab, err := conv.schema.GetTable(historyTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fields := []string{"id", "sid", "cid", "uid", "role", "name", "content", "context", "assistant_id", "assistant_name", "assistant_avatar", "mentions", "silent", "created_at", "updated_at", "expired_at"}
|
|
for _, field := range fields {
|
|
if !tab.HasColumn(field) {
|
|
return fmt.Errorf("%s is required", field)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (conv *Xun) initChatTable() error {
|
|
chatTable := conv.getChatTable()
|
|
has, err := conv.schema.HasTable(chatTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the chat table
|
|
if !has {
|
|
err = conv.schema.CreateTable(chatTable, func(table schema.Blueprint) {
|
|
table.ID("id")
|
|
table.String("chat_id", 200).Unique().Index()
|
|
table.String("title", 200).Null()
|
|
table.String("assistant_id", 200).Null().Index()
|
|
table.String("sid", 255).Index()
|
|
table.Boolean("silent").SetDefault(false).Index()
|
|
table.TimestampTz("created_at").SetDefaultRaw("CURRENT_TIMESTAMP").Index()
|
|
table.TimestampTz("updated_at").Null().Index()
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Trace("Create the chat table: %s", chatTable)
|
|
}
|
|
|
|
// Validate the table
|
|
tab, err := conv.schema.GetTable(chatTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fields := []string{"id", "chat_id", "title", "assistant_id", "sid", "silent", "created_at", "updated_at"}
|
|
for _, field := range fields {
|
|
if !tab.HasColumn(field) {
|
|
return fmt.Errorf("%s is required", field)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (conv *Xun) initAssistantTable() error {
|
|
assistantTable := conv.getAssistantTable()
|
|
has, err := conv.schema.HasTable(assistantTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the assistant table
|
|
if !has {
|
|
err = conv.schema.CreateTable(assistantTable, func(table schema.Blueprint) {
|
|
table.ID("id")
|
|
table.String("assistant_id", 200).Unique().Index()
|
|
table.String("type", 200).SetDefault("assistant").Index() // default is assistant
|
|
table.String("name", 200).Null() // assistant name
|
|
table.String("avatar", 200).Null() // assistant avatar
|
|
table.String("connector", 200).NotNull() // assistant connector
|
|
table.String("description", 600).Null().Index() // assistant description
|
|
table.String("path", 200).Null() // assistant storage path
|
|
table.Integer("sort").SetDefault(9999).Index() // assistant sort order
|
|
table.Boolean("built_in").SetDefault(false).Index() // whether this is a built-in assistant
|
|
table.JSON("placeholder").Null() // assistant placeholder
|
|
table.JSON("options").Null() // assistant options
|
|
table.JSON("prompts").Null() // assistant prompts
|
|
table.JSON("workflow").Null() // assistant workflow
|
|
table.JSON("knowledge").Null() // assistant knowledge
|
|
table.JSON("tools").Null() // assistant tools
|
|
table.JSON("tags").Null() // assistant tags
|
|
table.Boolean("readonly").SetDefault(false).Index() // assistant readonly
|
|
table.JSON("permissions").Null() // assistant permissions
|
|
table.JSON("locales").Null() // assistant i18n
|
|
table.Boolean("automated").SetDefault(true).Index() // assistant autoable
|
|
table.Boolean("mentionable").SetDefault(true).Index() // Whether this assistant can appear in @ mention list
|
|
table.TimestampTz("created_at").SetDefaultRaw("CURRENT_TIMESTAMP").Index()
|
|
table.TimestampTz("updated_at").Null().Index()
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Trace("Create the assistant table: %s", assistantTable)
|
|
}
|
|
|
|
// Validate the table
|
|
tab, err := conv.schema.GetTable(assistantTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fields := []string{"id", "assistant_id", "type", "name", "avatar", "connector", "description", "path", "sort", "built_in", "placeholder", "options", "prompts", "workflow", "knowledge", "tools", "tags", "readonly", "permissions", "locales", "automated", "mentionable", "created_at", "updated_at"}
|
|
for _, field := range fields {
|
|
if !tab.HasColumn(field) {
|
|
return fmt.Errorf("%s is required", field)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (conv *Xun) initAttachmentTable() error {
|
|
attachmentTable := conv.getAttachmentTable()
|
|
has, err := conv.schema.HasTable(attachmentTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the attachment table
|
|
if !has {
|
|
err = conv.schema.CreateTable(attachmentTable, func(table schema.Blueprint) {
|
|
table.ID("id")
|
|
table.String("file_id", 255).Unique().Index()
|
|
table.String("uid", 255).Index()
|
|
table.Boolean("guest").SetDefault(false).Index()
|
|
table.String("manager", 200).Index()
|
|
table.String("content_type", 200).Index()
|
|
table.String("name", 500).Index()
|
|
table.Boolean("public").SetDefault(false).Index()
|
|
table.JSON("scope").Null()
|
|
table.Boolean("gzip").SetDefault(false).Index()
|
|
table.BigInteger("bytes").Index()
|
|
table.String("collection_id", 200).Null().Index()
|
|
table.Enum("status", []string{"uploading", "uploaded", "indexing", "indexed", "upload_failed", "index_failed"}).SetDefault("uploading").Index() // Status field enum
|
|
table.String("progress", 200).Null() // Progress information
|
|
table.String("error", 600).Null() // Error information
|
|
table.TimestampTz("created_at").SetDefaultRaw("CURRENT_TIMESTAMP").Index()
|
|
table.TimestampTz("updated_at").Null().Index()
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Trace("Create the attachment table: %s", attachmentTable)
|
|
}
|
|
|
|
// Validate the table
|
|
tab, err := conv.schema.GetTable(attachmentTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fields := []string{"id", "file_id", "uid", "guest", "manager", "content_type", "name", "public", "scope", "gzip", "bytes", "collection_id", "status", "progress", "error", "created_at", "updated_at"}
|
|
for _, field := range fields {
|
|
if !tab.HasColumn(field) {
|
|
return fmt.Errorf("%s is required", field)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (conv *Xun) initKnowledgeTable() error {
|
|
knowledgeTable := conv.getKnowledgeTable()
|
|
has, err := conv.schema.HasTable(knowledgeTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the knowledge table
|
|
if !has {
|
|
err = conv.schema.CreateTable(knowledgeTable, func(table schema.Blueprint) {
|
|
table.ID("id")
|
|
table.String("collection_id", 200).Unique().Index()
|
|
table.String("name", 200).Index()
|
|
table.String("description", 600).Null().Index() // knowledge description
|
|
table.String("uid", 255).Index()
|
|
table.Boolean("public").SetDefault(false).Index()
|
|
table.JSON("scope").Null()
|
|
table.Boolean("readonly").SetDefault(false).Index()
|
|
table.JSON("option").Null()
|
|
table.Boolean("system").SetDefault(false).Index()
|
|
table.Integer("sort").SetDefault(9999).Index() // knowledge sort order
|
|
table.String("cover", 500).Null()
|
|
table.TimestampTz("created_at").SetDefaultRaw("CURRENT_TIMESTAMP").Index()
|
|
table.TimestampTz("updated_at").Null().Index()
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Trace("Create the knowledge table: %s", knowledgeTable)
|
|
}
|
|
|
|
// Validate the table
|
|
tab, err := conv.schema.GetTable(knowledgeTable)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fields := []string{"id", "collection_id", "name", "description", "uid", "public", "scope", "readonly", "option", "system", "sort", "cover", "created_at", "updated_at"}
|
|
for _, field := range fields {
|
|
if !tab.HasColumn(field) {
|
|
return fmt.Errorf("%s is required", field)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (conv *Xun) getUserID(sid string) (string, error) {
|
|
field := "user_id"
|
|
if conv.setting.UserField != "" {
|
|
field = conv.setting.UserField
|
|
}
|
|
|
|
id, err := session.Global().ID(sid).Get(field)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if id == nil || id == "" {
|
|
return sid, nil
|
|
}
|
|
|
|
return fmt.Sprintf("%v", id), nil
|
|
}
|
|
|
|
func (conv *Xun) getHistoryTable() string {
|
|
return conv.setting.Prefix + "history"
|
|
}
|
|
|
|
func (conv *Xun) getChatTable() string {
|
|
return conv.setting.Prefix + "chat"
|
|
}
|
|
|
|
func (conv *Xun) getAssistantTable() string {
|
|
return conv.setting.Prefix + "assistant"
|
|
}
|
|
|
|
func (conv *Xun) getAttachmentTable() string {
|
|
return conv.setting.Prefix + "attachment"
|
|
}
|
|
|
|
func (conv *Xun) getKnowledgeTable() string {
|
|
return conv.setting.Prefix + "knowledge"
|
|
}
|
|
|
|
func (conv *Xun) newQueryAttachment() query.Query {
|
|
qb := conv.query.New()
|
|
qb.Table(conv.getAttachmentTable())
|
|
return qb
|
|
}
|
|
|
|
func (conv *Xun) newQueryKnowledge() query.Query {
|
|
qb := conv.query.New()
|
|
qb.Table(conv.getKnowledgeTable())
|
|
return qb
|
|
}
|
|
|
|
// UpdateChatTitle update the chat title
|
|
func (conv *Xun) UpdateChatTitle(sid string, cid string, title string) error {
|
|
userID, err := conv.getUserID(sid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = conv.newQueryChat().
|
|
Where("sid", userID).
|
|
Where("chat_id", cid).
|
|
Update(map[string]interface{}{
|
|
"title": title,
|
|
"updated_at": time.Now(),
|
|
})
|
|
return err
|
|
}
|
|
|
|
// GetChats get the chat list with grouping by date
|
|
func (conv *Xun) GetChats(sid string, filter ChatFilter, locale ...string) (*ChatGroupResponse, error) {
|
|
// Default behavior: exclude silent chats
|
|
if filter.Silent == nil {
|
|
silentFalse := false
|
|
filter.Silent = &silentFalse
|
|
}
|
|
|
|
return conv.getChatsWithFilter(sid, filter, locale...)
|
|
}
|
|
|
|
// getChatsWithFilter get the chats with filter options
|
|
func (conv *Xun) getChatsWithFilter(sid string, filter ChatFilter, locale ...string) (*ChatGroupResponse, error) {
|
|
userID, err := conv.getUserID(sid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set default values
|
|
if filter.Page <= 0 {
|
|
filter.Page = 1
|
|
}
|
|
if filter.PageSize <= 0 {
|
|
filter.PageSize = 20
|
|
}
|
|
if filter.Order == "" {
|
|
filter.Order = "desc"
|
|
}
|
|
|
|
// Get total count
|
|
qbCount := conv.newQueryChat().
|
|
Where("sid", userID)
|
|
|
|
// Apply silent filter if provided
|
|
if filter.Silent != nil {
|
|
if *filter.Silent {
|
|
// Include all chats (both silent and non-silent)
|
|
} else {
|
|
// Only include non-silent chats
|
|
qbCount.Where("silent", false)
|
|
}
|
|
}
|
|
|
|
// Apply keyword filter if provided
|
|
if filter.Keywords != "" {
|
|
qbCount.Where("title", "like", fmt.Sprintf("%%%s%%", filter.Keywords))
|
|
}
|
|
|
|
total, err := qbCount.Count()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Calculate last page
|
|
lastPage := int(math.Ceil(float64(total) / float64(filter.PageSize)))
|
|
if lastPage < 1 {
|
|
lastPage = 1
|
|
}
|
|
|
|
// Get chats with pagination
|
|
qb := conv.newQueryChat().
|
|
Select("chat_id", "title", "assistant_id", "silent", "created_at", "updated_at").
|
|
Where("sid", userID)
|
|
|
|
// Apply silent filter if provided
|
|
if filter.Silent != nil {
|
|
if *filter.Silent {
|
|
// Include all chats (both silent and non-silent)
|
|
} else {
|
|
// Only include non-silent chats
|
|
qb.Where("silent", false)
|
|
}
|
|
}
|
|
|
|
// Apply keyword filter if provided
|
|
if filter.Keywords != "" {
|
|
qb.Where("title", "like", fmt.Sprintf("%%%s%%", filter.Keywords))
|
|
}
|
|
|
|
// Apply pagination
|
|
offset := (filter.Page - 1) * filter.PageSize
|
|
qb.OrderBy("updated_at", filter.Order).
|
|
Offset(offset).
|
|
Limit(filter.PageSize)
|
|
|
|
rows, err := qb.Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Group chats by date
|
|
today := time.Now().Truncate(24 * time.Hour)
|
|
yesterday := today.AddDate(0, 0, -1)
|
|
thisWeekStart := today.AddDate(0, 0, -int(today.Weekday()))
|
|
lastWeekStart := thisWeekStart.AddDate(0, 0, -7)
|
|
lastWeekEnd := thisWeekStart.AddDate(0, 0, -1)
|
|
|
|
groups := map[string][]map[string]interface{}{
|
|
"Today": {},
|
|
"Yesterday": {},
|
|
"This Week": {},
|
|
"Last Week": {},
|
|
"Even Earlier": {},
|
|
}
|
|
|
|
// Collect assistant IDs to fetch their details
|
|
assistantIDs := []interface{}{}
|
|
for _, row := range rows {
|
|
if assistantID := row.Get("assistant_id"); assistantID != nil && assistantID != "" {
|
|
assistantIDs = append(assistantIDs, assistantID)
|
|
}
|
|
}
|
|
|
|
// Fetch assistant details
|
|
assistantMap := map[string]map[string]interface{}{}
|
|
if len(assistantIDs) > 0 {
|
|
assistants, err := conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Select("assistant_id", "name", "avatar").
|
|
WhereIn("assistant_id", assistantIDs).
|
|
Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, assistant := range assistants {
|
|
if id := assistant.Get("assistant_id"); id != nil {
|
|
name := assistant.Get("name")
|
|
if len(locale) > 0 {
|
|
lang := strings.ToLower(locale[0])
|
|
name = i18n.Translate(id.(string), lang, name).(string)
|
|
}
|
|
assistantMap[fmt.Sprintf("%v", id)] = map[string]interface{}{
|
|
"name": name,
|
|
"avatar": assistant.Get("avatar"),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, row := range rows {
|
|
chatID := row.Get("chat_id")
|
|
if chatID == nil || chatID == "" {
|
|
continue
|
|
}
|
|
|
|
chat := map[string]interface{}{
|
|
"chat_id": chatID,
|
|
"title": row.Get("title"),
|
|
"assistant_id": row.Get("assistant_id"),
|
|
"silent": row.Get("silent"),
|
|
}
|
|
|
|
// Add assistant details if available
|
|
if assistantID := row.Get("assistant_id"); assistantID != nil && assistantID != "" {
|
|
if assistant, ok := assistantMap[fmt.Sprintf("%v", assistantID)]; ok {
|
|
name := assistant["name"]
|
|
if len(locale) > 0 {
|
|
lang := strings.ToLower(locale[0])
|
|
name = i18n.Translate(assistantID.(string), lang, name).(string)
|
|
}
|
|
chat["assistant_name"] = name
|
|
chat["assistant_avatar"] = assistant["avatar"]
|
|
}
|
|
}
|
|
|
|
var dbDatetime = row.Get("updated_at")
|
|
if dbDatetime == nil {
|
|
dbDatetime = row.Get("created_at")
|
|
}
|
|
|
|
var createdAt time.Time
|
|
switch v := dbDatetime.(type) {
|
|
case time.Time:
|
|
createdAt = v
|
|
case string:
|
|
parsed, err := time.Parse("2006-01-02 15:04:05.999999-07:00", v)
|
|
if err != nil {
|
|
// Try alternative format
|
|
parsed, err = time.Parse(time.RFC3339, v)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
}
|
|
createdAt = parsed
|
|
default:
|
|
continue
|
|
}
|
|
|
|
createdDate := createdAt.Truncate(24 * time.Hour)
|
|
|
|
switch {
|
|
case createdDate.Equal(today):
|
|
groups["Today"] = append(groups["Today"], chat)
|
|
case createdDate.Equal(yesterday):
|
|
groups["Yesterday"] = append(groups["Yesterday"], chat)
|
|
case createdDate.After(thisWeekStart) && createdDate.Before(today):
|
|
groups["This Week"] = append(groups["This Week"], chat)
|
|
case createdDate.After(lastWeekStart) && createdDate.Before(lastWeekEnd.AddDate(0, 0, 1)):
|
|
groups["Last Week"] = append(groups["Last Week"], chat)
|
|
default:
|
|
groups["Even Earlier"] = append(groups["Even Earlier"], chat)
|
|
}
|
|
}
|
|
|
|
// Convert to ordered slice and apply i18n
|
|
result := []ChatGroup{}
|
|
for _, label := range []string{"Today", "Yesterday", "This Week", "Last Week", "Even Earlier"} {
|
|
if len(groups[label]) > 0 {
|
|
translatedLabel := label
|
|
if len(locale) > 0 {
|
|
lang := strings.ToLower(locale[0])
|
|
translatedLabel = i18n.TranslateGlobal(lang, label).(string)
|
|
}
|
|
result = append(result, ChatGroup{
|
|
Label: translatedLabel,
|
|
Chats: groups[label],
|
|
})
|
|
}
|
|
}
|
|
|
|
return &ChatGroupResponse{
|
|
Groups: result,
|
|
Page: filter.Page,
|
|
PageSize: filter.PageSize,
|
|
Total: total,
|
|
LastPage: lastPage,
|
|
}, nil
|
|
}
|
|
|
|
// GetHistory get the history
|
|
func (conv *Xun) GetHistory(sid string, cid string, locale ...string) ([]map[string]interface{}, error) {
|
|
userID, err := conv.getUserID(sid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
qb := conv.newQuery().
|
|
Select("role", "name", "content", "context", "assistant_id", "assistant_name", "assistant_avatar", "mentions", "uid", "silent", "created_at", "updated_at").
|
|
Where("sid", userID).
|
|
Where("cid", cid).
|
|
OrderBy("id", "desc")
|
|
|
|
// By default, exclude silent messages
|
|
qb.Where("silent", false)
|
|
|
|
if conv.setting.TTL > 0 {
|
|
qb.Where("expired_at", ">", time.Now())
|
|
}
|
|
|
|
limit := 20
|
|
if conv.setting.MaxSize > 0 {
|
|
limit = conv.setting.MaxSize
|
|
}
|
|
|
|
rows, err := qb.Limit(limit).Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := []map[string]interface{}{}
|
|
for _, row := range rows {
|
|
assistantName := row.Get("assistant_name")
|
|
assistantID := row.Get("assistant_id")
|
|
if len(locale) > 0 && assistantID != nil {
|
|
lang := strings.ToLower(locale[0])
|
|
assistantName = i18n.Translate(assistantID.(string), lang, assistantName).(string)
|
|
}
|
|
|
|
message := map[string]interface{}{
|
|
"role": row.Get("role"),
|
|
"name": row.Get("name"),
|
|
"content": row.Get("content"),
|
|
"context": row.Get("context"),
|
|
"assistant_id": row.Get("assistant_id"),
|
|
"assistant_name": assistantName,
|
|
"assistant_avatar": row.Get("assistant_avatar"),
|
|
"mentions": row.Get("mentions"),
|
|
"uid": row.Get("uid"),
|
|
"silent": row.Get("silent"),
|
|
"created_at": row.Get("created_at"),
|
|
"updated_at": row.Get("updated_at"),
|
|
}
|
|
res = append([]map[string]interface{}{message}, res...)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// SaveHistory save the history
|
|
func (conv *Xun) SaveHistory(sid string, messages []map[string]interface{}, cid string, context map[string]interface{}) error {
|
|
|
|
if cid == "" {
|
|
cid = uuid.New().String() // Generate a new UUID if cid is empty
|
|
}
|
|
|
|
userID, err := conv.getUserID(sid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get assistant_id from context
|
|
var assistantID interface{} = nil
|
|
if context != nil {
|
|
if id, ok := context["assistant_id"].(string); ok && id != "" {
|
|
assistantID = id
|
|
}
|
|
}
|
|
|
|
// Get silent flag from context
|
|
var silent bool = false
|
|
var historyVisible bool = true
|
|
if context != nil {
|
|
if silentVal, ok := context["silent"]; ok {
|
|
switch v := silentVal.(type) {
|
|
case bool:
|
|
silent = v
|
|
case string:
|
|
silent = v == "true" || v == "1" || v == "yes"
|
|
case int:
|
|
silent = v != 0
|
|
case float64:
|
|
silent = v != 0
|
|
}
|
|
}
|
|
|
|
// Get history visible from context
|
|
if historyVisibleVal, ok := context["history_visible"]; ok {
|
|
switch v := historyVisibleVal.(type) {
|
|
case bool:
|
|
historyVisible = v
|
|
case string:
|
|
historyVisible = v == "true" || v == "1" || v == "yes"
|
|
case int:
|
|
historyVisible = v != 0
|
|
case float64:
|
|
historyVisible = v != 0
|
|
}
|
|
}
|
|
}
|
|
|
|
// First ensure chat record exists
|
|
exists, err := conv.newQueryChat().
|
|
Where("chat_id", cid).
|
|
Where("sid", userID).
|
|
Exists()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !exists {
|
|
// Create new chat record
|
|
err = conv.newQueryChat().
|
|
Insert(map[string]interface{}{
|
|
"chat_id": cid,
|
|
"sid": userID,
|
|
"assistant_id": assistantID,
|
|
"silent": silent || historyVisible == false,
|
|
"created_at": time.Now(),
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// Update assistant_id and silent if needed
|
|
_, err = conv.newQueryChat().
|
|
Where("chat_id", cid).
|
|
Where("sid", userID).
|
|
Update(map[string]interface{}{
|
|
"assistant_id": assistantID,
|
|
"silent": silent || historyVisible == false,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Save message history
|
|
var expiredAt interface{} = nil
|
|
values := []map[string]interface{}{}
|
|
if conv.setting.TTL > 0 {
|
|
expiredAt = time.Now().Add(time.Duration(conv.setting.TTL) * time.Second)
|
|
}
|
|
|
|
now := time.Now()
|
|
for _, message := range messages {
|
|
// Type assertion safety checks
|
|
role, ok := message["role"].(string)
|
|
if !ok {
|
|
return fmt.Errorf("invalid role type in message: %v", message["role"])
|
|
}
|
|
|
|
content, ok := message["content"].(string)
|
|
if !ok {
|
|
return fmt.Errorf("invalid content type in message: %v", message["content"])
|
|
}
|
|
|
|
var contextRaw interface{} = nil
|
|
if context != nil {
|
|
contextRaw, err = jsoniter.MarshalToString(context)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Process mentions if present
|
|
var mentionsRaw interface{} = nil
|
|
if mentions, ok := message["mentions"].([]interface{}); ok && len(mentions) > 0 {
|
|
mentionsRaw, err = jsoniter.MarshalToString(mentions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
value := map[string]interface{}{
|
|
"role": role,
|
|
"name": "",
|
|
"content": content,
|
|
"sid": userID,
|
|
"cid": cid,
|
|
"uid": userID,
|
|
"context": contextRaw,
|
|
"mentions": mentionsRaw,
|
|
"assistant_id": nil,
|
|
"assistant_name": nil,
|
|
"assistant_avatar": nil,
|
|
"silent": silent,
|
|
"created_at": now,
|
|
"updated_at": nil,
|
|
"expired_at": expiredAt,
|
|
}
|
|
|
|
if name, ok := message["name"].(string); ok {
|
|
value["name"] = name
|
|
}
|
|
|
|
// Add assistant fields if present
|
|
if assistantID, ok := message["assistant_id"].(string); ok {
|
|
value["assistant_id"] = assistantID
|
|
}
|
|
if assistantName, ok := message["assistant_name"].(string); ok {
|
|
value["assistant_name"] = assistantName
|
|
}
|
|
if assistantAvatar, ok := message["assistant_avatar"].(string); ok {
|
|
value["assistant_avatar"] = assistantAvatar
|
|
}
|
|
|
|
values = append(values, value)
|
|
}
|
|
|
|
err = conv.newQuery().Insert(values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update Chat updated_at
|
|
_, err = conv.newQueryChat().
|
|
Where("chat_id", cid).
|
|
Where("sid", userID).
|
|
Update(map[string]interface{}{"updated_at": now})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetChat get the chat info and its history
|
|
func (conv *Xun) GetChat(sid string, cid string, locale ...string) (*ChatInfo, error) {
|
|
userID, err := conv.getUserID(sid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get chat info
|
|
qb := conv.newQueryChat().
|
|
Select("chat_id", "title", "assistant_id").
|
|
Where("sid", userID).
|
|
Where("chat_id", cid)
|
|
|
|
row, err := qb.First()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return nil if chat_id is nil (means no chat found)
|
|
if row.Get("chat_id") == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
chat := map[string]interface{}{
|
|
"chat_id": row.Get("chat_id"),
|
|
"title": row.Get("title"),
|
|
"assistant_id": row.Get("assistant_id"),
|
|
}
|
|
|
|
// Get assistant details if assistant_id exists
|
|
if assistantID := row.Get("assistant_id"); assistantID != nil && assistantID != "" {
|
|
assistant, err := conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Select("name", "avatar").
|
|
Where("assistant_id", assistantID).
|
|
First()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
name := assistant.Get("name")
|
|
if len(locale) > 0 {
|
|
lang := strings.ToLower(locale[0])
|
|
name = i18n.Translate(assistantID.(string), lang, name).(string)
|
|
}
|
|
|
|
if assistant != nil {
|
|
chat["assistant_name"] = name
|
|
chat["assistant_avatar"] = assistant.Get("avatar")
|
|
}
|
|
}
|
|
|
|
// Get chat history with default filter (silent=false)
|
|
history, err := conv.GetHistory(sid, cid, locale...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ChatInfo{
|
|
Chat: chat,
|
|
History: history,
|
|
}, nil
|
|
}
|
|
|
|
// GetChatWithFilter get the chat info and its history with filter options
|
|
func (conv *Xun) GetChatWithFilter(sid string, cid string, filter ChatFilter, locale ...string) (*ChatInfo, error) {
|
|
userID, err := conv.getUserID(sid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get chat info
|
|
qb := conv.newQueryChat().
|
|
Select("chat_id", "title", "assistant_id").
|
|
Where("sid", userID).
|
|
Where("chat_id", cid)
|
|
|
|
row, err := qb.First()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return nil if chat_id is nil (means no chat found)
|
|
if row.Get("chat_id") == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
chat := map[string]interface{}{
|
|
"chat_id": row.Get("chat_id"),
|
|
"title": row.Get("title"),
|
|
"assistant_id": row.Get("assistant_id"),
|
|
}
|
|
|
|
// Get assistant details if assistant_id exists
|
|
if assistantID := row.Get("assistant_id"); assistantID != nil && assistantID != "" {
|
|
assistant, err := conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Select("name", "avatar").
|
|
Where("assistant_id", assistantID).
|
|
First()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if assistant != nil {
|
|
chat["assistant_name"] = assistant.Get("name")
|
|
chat["assistant_avatar"] = assistant.Get("avatar")
|
|
}
|
|
}
|
|
|
|
// Get chat history with filter
|
|
history, err := conv.GetHistoryWithFilter(sid, cid, filter, locale...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ChatInfo{
|
|
Chat: chat,
|
|
History: history,
|
|
}, nil
|
|
}
|
|
|
|
// DeleteChat deletes a specific chat and its history
|
|
func (conv *Xun) DeleteChat(sid string, cid string) error {
|
|
userID, err := conv.getUserID(sid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete history records first
|
|
_, err = conv.newQuery().
|
|
Where("sid", userID).
|
|
Where("cid", cid).
|
|
Delete()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Then delete the chat
|
|
_, err = conv.newQueryChat().
|
|
Where("sid", userID).
|
|
Where("chat_id", cid).
|
|
Limit(1).
|
|
Delete()
|
|
return err
|
|
}
|
|
|
|
// DeleteAllChats deletes all chats and their histories for a user
|
|
func (conv *Xun) DeleteAllChats(sid string) error {
|
|
userID, err := conv.getUserID(sid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete history records first
|
|
_, err = conv.newQuery().
|
|
Where("sid", userID).
|
|
Delete()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Then delete all chats
|
|
_, err = conv.newQueryChat().
|
|
Where("sid", userID).
|
|
Delete()
|
|
return err
|
|
}
|
|
|
|
// parseJSONFields parses JSON string fields into their corresponding Go types
|
|
func (conv *Xun) parseJSONFields(data map[string]interface{}, fields []string) {
|
|
for _, field := range fields {
|
|
if val := data[field]; val != nil {
|
|
if strVal, ok := val.(string); ok && strVal != "" {
|
|
var parsed interface{}
|
|
if err := jsoniter.UnmarshalFromString(strVal, &parsed); err == nil {
|
|
data[field] = parsed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// SaveAssistant saves assistant information
|
|
func (conv *Xun) SaveAssistant(assistant map[string]interface{}) (interface{}, error) {
|
|
// Validate required fields
|
|
requiredFields := []string{"name", "type", "connector"}
|
|
for _, field := range requiredFields {
|
|
if _, ok := assistant[field]; !ok {
|
|
return nil, fmt.Errorf("field %s is required", field)
|
|
}
|
|
if assistant[field] == nil || assistant[field] == "" {
|
|
return nil, fmt.Errorf("field %s cannot be empty", field)
|
|
}
|
|
}
|
|
|
|
// Create a copy of the assistant map to avoid modifying the original
|
|
assistantCopy := make(map[string]interface{})
|
|
for k, v := range assistant {
|
|
assistantCopy[k] = v
|
|
}
|
|
|
|
// Process JSON fields
|
|
jsonFields := []string{"tags", "options", "prompts", "workflow", "knowledge", "tools", "permissions", "placeholder", "locales"}
|
|
for _, field := range jsonFields {
|
|
if val, ok := assistantCopy[field]; ok && val != nil {
|
|
// If it's a string, try to parse it first
|
|
if strVal, ok := val.(string); ok && strVal != "" {
|
|
var parsed interface{}
|
|
if err := jsoniter.UnmarshalFromString(strVal, &parsed); err == nil {
|
|
assistantCopy[field] = parsed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate assistant_id if not provided
|
|
if _, ok := assistantCopy["assistant_id"]; !ok {
|
|
var err error
|
|
assistantCopy["assistant_id"], err = conv.GenerateAssistantID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Check if assistant exists
|
|
exists, err := conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Where("assistant_id", assistantCopy["assistant_id"]).
|
|
Exists()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert JSON fields to strings for storage
|
|
for _, field := range jsonFields {
|
|
if val, ok := assistantCopy[field]; ok && val != nil {
|
|
jsonStr, err := jsoniter.MarshalToString(val)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal %s to JSON: %v", field, err)
|
|
}
|
|
assistantCopy[field] = jsonStr
|
|
}
|
|
}
|
|
|
|
// Update or insert
|
|
if exists {
|
|
_, err := conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Where("assistant_id", assistantCopy["assistant_id"]).
|
|
Update(assistantCopy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return assistantCopy["assistant_id"], nil
|
|
}
|
|
|
|
err = conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Insert(assistantCopy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return assistantCopy["assistant_id"], nil
|
|
}
|
|
|
|
// DeleteAssistant deletes an assistant by assistant_id
|
|
func (conv *Xun) DeleteAssistant(assistantID string) error {
|
|
// Check if assistant exists
|
|
exists, err := conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Where("assistant_id", assistantID).
|
|
Exists()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !exists {
|
|
return fmt.Errorf("assistant %s not found", assistantID)
|
|
}
|
|
|
|
_, err = conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Where("assistant_id", assistantID).
|
|
Delete()
|
|
return err
|
|
}
|
|
|
|
// GetAssistants retrieves assistants with pagination and filtering
|
|
func (conv *Xun) GetAssistants(filter AssistantFilter, locale ...string) (*AssistantResponse, error) {
|
|
qb := conv.query.New().
|
|
Table(conv.getAssistantTable())
|
|
|
|
// Apply tag filter if provided
|
|
if filter.Tags != nil && len(filter.Tags) > 0 {
|
|
qb.Where(func(qb query.Query) {
|
|
for i, tag := range filter.Tags {
|
|
// For each tag, we need to match it as part of a JSON array
|
|
// This will match both single tag arrays ["tag1"] and multi-tag arrays ["tag1","tag2"]
|
|
pattern := fmt.Sprintf("%%\"%s\"%%", tag)
|
|
if i == 0 {
|
|
qb.Where("tags", "like", pattern)
|
|
} else {
|
|
qb.OrWhere("tags", "like", pattern)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Apply keyword filter if provided
|
|
if filter.Keywords != "" {
|
|
qb.Where(func(qb query.Query) {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Keywords)).
|
|
OrWhere("description", "like", fmt.Sprintf("%%%s%%", filter.Keywords))
|
|
})
|
|
}
|
|
|
|
// Apply type filter if provided
|
|
if filter.Type != "" {
|
|
qb.Where("type", filter.Type)
|
|
}
|
|
|
|
// Apply connector filter if provided
|
|
if filter.Connector != "" {
|
|
qb.Where("connector", filter.Connector)
|
|
}
|
|
|
|
// Apply assistant_id filter if provided
|
|
if filter.AssistantID != "" {
|
|
qb.Where("assistant_id", filter.AssistantID)
|
|
}
|
|
|
|
// Apply assistantIDs filter if provided
|
|
if filter.AssistantIDs != nil && len(filter.AssistantIDs) > 0 {
|
|
qb.WhereIn("assistant_id", filter.AssistantIDs)
|
|
}
|
|
|
|
// Apply mentionable filter if provided
|
|
if filter.Mentionable != nil {
|
|
qb.Where("mentionable", *filter.Mentionable)
|
|
}
|
|
|
|
// Apply automated filter if provided
|
|
if filter.Automated != nil {
|
|
qb.Where("automated", *filter.Automated)
|
|
}
|
|
|
|
// Apply built_in filter if provided
|
|
if filter.BuiltIn != nil {
|
|
qb.Where("built_in", *filter.BuiltIn)
|
|
}
|
|
|
|
// Set defaults for pagination
|
|
if filter.PageSize <= 0 {
|
|
filter.PageSize = 20
|
|
}
|
|
if filter.Page <= 0 {
|
|
filter.Page = 1
|
|
}
|
|
|
|
// Get total count
|
|
total, err := qb.Clone().Count()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Calculate pagination
|
|
offset := (filter.Page - 1) * filter.PageSize
|
|
totalPages := int(math.Ceil(float64(total) / float64(filter.PageSize)))
|
|
nextPage := filter.Page + 1
|
|
if nextPage > totalPages {
|
|
nextPage = 0
|
|
}
|
|
prevPage := filter.Page - 1
|
|
if prevPage < 1 {
|
|
prevPage = 0
|
|
}
|
|
|
|
// Apply select fields if provided
|
|
if filter.Select != nil && len(filter.Select) > 0 {
|
|
selectFields := make([]interface{}, len(filter.Select))
|
|
for i, field := range filter.Select {
|
|
selectFields[i] = field
|
|
}
|
|
qb.Select(selectFields...)
|
|
}
|
|
|
|
// Get paginated results
|
|
rows, err := qb.OrderBy("sort", "asc").
|
|
OrderBy("updated_at", "desc").
|
|
Offset(offset).
|
|
Limit(filter.PageSize).
|
|
Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert rows to map slice and parse JSON fields
|
|
data := make([]map[string]interface{}, len(rows))
|
|
jsonFields := []string{"tags", "options", "prompts", "workflow", "knowledge", "tools", "permissions", "placeholder"}
|
|
for i, row := range rows {
|
|
data[i] = row
|
|
// Only parse JSON fields if they are selected or no select filter is provided
|
|
if filter.Select == nil || len(filter.Select) == 0 {
|
|
conv.parseJSONFields(data[i], jsonFields)
|
|
} else {
|
|
// Parse only selected JSON fields
|
|
selectedJSONFields := []string{}
|
|
for _, field := range jsonFields {
|
|
for _, selected := range filter.Select {
|
|
if selected == field {
|
|
selectedJSONFields = append(selectedJSONFields, field)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if len(selectedJSONFields) > 0 {
|
|
conv.parseJSONFields(data[i], selectedJSONFields)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Translate Data
|
|
if len(locale) > 0 {
|
|
lang := strings.ToLower(locale[0])
|
|
for i, row := range data {
|
|
assistantID := row["assistant_id"].(string)
|
|
data[i] = i18n.Translate(assistantID, lang, row).(map[string]interface{})
|
|
}
|
|
}
|
|
|
|
return &AssistantResponse{
|
|
Data: data,
|
|
Page: filter.Page,
|
|
PageSize: filter.PageSize,
|
|
PageCnt: totalPages,
|
|
Next: nextPage,
|
|
Prev: prevPage,
|
|
Total: total,
|
|
}, nil
|
|
}
|
|
|
|
// GetAssistant retrieves a single assistant by ID
|
|
func (conv *Xun) GetAssistant(assistantID string, locale ...string) (map[string]interface{}, error) {
|
|
row, err := conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Where("assistant_id", assistantID).
|
|
First()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if row == nil {
|
|
return nil, fmt.Errorf("assistant %s not found", assistantID)
|
|
}
|
|
|
|
data := row.ToMap()
|
|
if data == nil || len(data) == 0 {
|
|
return nil, fmt.Errorf("the assistant %s is empty", assistantID)
|
|
}
|
|
|
|
// Parse JSON fields
|
|
jsonFields := []string{"tags", "options", "prompts", "workflow", "knowledge", "tools", "permissions", "placeholder"}
|
|
conv.parseJSONFields(data, jsonFields)
|
|
if len(locale) > 0 {
|
|
lang := strings.ToLower(locale[0])
|
|
return i18n.Translate(assistantID, lang, data).(map[string]interface{}), nil
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// DeleteAssistants deletes assistants based on filter conditions
|
|
func (conv *Xun) DeleteAssistants(filter AssistantFilter) (int64, error) {
|
|
qb := conv.query.New().
|
|
Table(conv.getAssistantTable())
|
|
|
|
// Apply tag filter if provided
|
|
if filter.Tags != nil && len(filter.Tags) > 0 {
|
|
qb.Where(func(qb query.Query) {
|
|
for i, tag := range filter.Tags {
|
|
pattern := fmt.Sprintf("%%\"%s\"%%", tag)
|
|
if i == 0 {
|
|
qb.Where("tags", "like", pattern)
|
|
} else {
|
|
qb.OrWhere("tags", "like", pattern)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Apply keyword filter if provided
|
|
if filter.Keywords != "" {
|
|
qb.Where(func(qb query.Query) {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Keywords)).
|
|
OrWhere("description", "like", fmt.Sprintf("%%%s%%", filter.Keywords))
|
|
})
|
|
}
|
|
|
|
// Apply connector filter if provided
|
|
if filter.Connector != "" {
|
|
qb.Where("connector", filter.Connector)
|
|
}
|
|
|
|
// Apply assistant_id filter if provided
|
|
if filter.AssistantID != "" {
|
|
qb.Where("assistant_id", filter.AssistantID)
|
|
}
|
|
|
|
// Apply assistantIDs filter if provided
|
|
if filter.AssistantIDs != nil && len(filter.AssistantIDs) > 0 {
|
|
qb.WhereIn("assistant_id", filter.AssistantIDs)
|
|
}
|
|
|
|
// Apply mentionable filter if provided
|
|
if filter.Mentionable != nil {
|
|
qb.Where("mentionable", *filter.Mentionable)
|
|
}
|
|
|
|
// Apply automated filter if provided
|
|
if filter.Automated != nil {
|
|
qb.Where("automated", *filter.Automated)
|
|
}
|
|
|
|
// Apply built_in filter if provided
|
|
if filter.BuiltIn != nil {
|
|
qb.Where("built_in", *filter.BuiltIn)
|
|
}
|
|
|
|
// Execute delete and return number of deleted records
|
|
return qb.Delete()
|
|
}
|
|
|
|
// GetAssistantTags retrieves all unique tags from assistants
|
|
func (conv *Xun) GetAssistantTags(locale ...string) ([]Tag, error) {
|
|
q := conv.newQuery().Table(conv.getAssistantTable())
|
|
rows, err := q.Select("tags").Where("type", "assistant").GroupBy("tags").Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tagSet := map[string]bool{}
|
|
for _, row := range rows {
|
|
if tags, ok := row["tags"].(string); ok && tags != "" {
|
|
var tagList []string
|
|
if err := jsoniter.UnmarshalFromString(tags, &tagList); err == nil {
|
|
for _, tag := range tagList {
|
|
tagSet[tag] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
lang := "en"
|
|
if len(locale) > 0 {
|
|
lang = locale[0]
|
|
}
|
|
|
|
// Convert map keys to slice
|
|
tags := make([]Tag, 0, len(tagSet))
|
|
for tag := range tagSet {
|
|
tags = append(tags, Tag{
|
|
Value: tag,
|
|
Label: i18n.TranslateGlobal(lang, tag).(string),
|
|
})
|
|
}
|
|
return tags, nil
|
|
}
|
|
|
|
// GetHistoryWithFilter get the history with filter options
|
|
func (conv *Xun) GetHistoryWithFilter(sid string, cid string, filter ChatFilter, locale ...string) ([]map[string]interface{}, error) {
|
|
userID, err := conv.getUserID(sid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
qb := conv.newQuery().
|
|
Select("role", "name", "content", "context", "assistant_id", "assistant_name", "assistant_avatar", "mentions", "uid", "silent", "created_at", "updated_at").
|
|
Where("sid", userID).
|
|
Where("cid", cid).
|
|
OrderBy("id", "desc")
|
|
|
|
// Apply silent filter if provided, otherwise exclude silent messages by default
|
|
if filter.Silent != nil {
|
|
if *filter.Silent {
|
|
// Include all messages (both silent and non-silent)
|
|
} else {
|
|
// Only include non-silent messages
|
|
qb.Where("silent", false)
|
|
}
|
|
} else {
|
|
// Default behavior: exclude silent messages
|
|
qb.Where("silent", false)
|
|
}
|
|
|
|
if conv.setting.TTL > 0 {
|
|
qb.Where("expired_at", ">", time.Now())
|
|
}
|
|
|
|
limit := 20
|
|
if conv.setting.MaxSize > 0 {
|
|
limit = conv.setting.MaxSize
|
|
}
|
|
if filter.PageSize > 0 {
|
|
limit = filter.PageSize
|
|
}
|
|
|
|
// Apply pagination if provided
|
|
if filter.Page > 0 {
|
|
offset := (filter.Page - 1) * limit
|
|
qb.Offset(offset)
|
|
}
|
|
|
|
rows, err := qb.Limit(limit).Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := []map[string]interface{}{}
|
|
for _, row := range rows {
|
|
message := map[string]interface{}{
|
|
"role": row.Get("role"),
|
|
"name": row.Get("name"),
|
|
"content": row.Get("content"),
|
|
"context": row.Get("context"),
|
|
"assistant_id": row.Get("assistant_id"),
|
|
"assistant_name": row.Get("assistant_name"),
|
|
"assistant_avatar": row.Get("assistant_avatar"),
|
|
"mentions": row.Get("mentions"),
|
|
"uid": row.Get("uid"),
|
|
"silent": row.Get("silent"),
|
|
"created_at": row.Get("created_at"),
|
|
"updated_at": row.Get("updated_at"),
|
|
}
|
|
res = append([]map[string]interface{}{message}, res...)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// GenerateAssistantID generates a random-looking 6-digit ID
|
|
func (conv *Xun) GenerateAssistantID() (string, error) {
|
|
maxAttempts := 10 // Maximum number of attempts to generate a unique ID
|
|
for i := 0; i < maxAttempts; i++ {
|
|
// Generate a random number using timestamp and some bit operations
|
|
timestamp := time.Now().UnixNano()
|
|
random := (timestamp ^ (timestamp >> 12)) % 1000000
|
|
hash := fmt.Sprintf("%06d", random)
|
|
|
|
// Check if this ID already exists
|
|
exists, err := conv.query.New().
|
|
Table(conv.getAssistantTable()).
|
|
Where("assistant_id", hash).
|
|
Exists()
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !exists {
|
|
return hash, nil
|
|
}
|
|
|
|
// If ID exists, wait a bit and try again
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
|
|
return "", fmt.Errorf("failed to generate unique ID after %d attempts", maxAttempts)
|
|
}
|
|
|
|
// SaveAttachment saves attachment information
|
|
func (conv *Xun) SaveAttachment(attachment map[string]interface{}) (interface{}, error) {
|
|
// Validate required fields
|
|
requiredFields := []string{"file_id", "uid", "manager", "content_type", "name"}
|
|
for _, field := range requiredFields {
|
|
if _, ok := attachment[field]; !ok {
|
|
return nil, fmt.Errorf("field %s is required", field)
|
|
}
|
|
if attachment[field] == nil || attachment[field] == "" {
|
|
return nil, fmt.Errorf("field %s cannot be empty", field)
|
|
}
|
|
}
|
|
|
|
// Create a copy of the attachment map to avoid modifying the original
|
|
attachmentCopy := make(map[string]interface{})
|
|
for k, v := range attachment {
|
|
attachmentCopy[k] = v
|
|
}
|
|
|
|
// Process JSON fields
|
|
jsonFields := []string{"scope"}
|
|
for _, field := range jsonFields {
|
|
if val, ok := attachmentCopy[field]; ok && val != nil {
|
|
// If it's a string, try to parse it first
|
|
if strVal, ok := val.(string); ok && strVal != "" {
|
|
var parsed interface{}
|
|
if err := jsoniter.UnmarshalFromString(strVal, &parsed); err == nil {
|
|
attachmentCopy[field] = parsed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if attachment exists
|
|
exists, err := conv.query.New().
|
|
Table(conv.getAttachmentTable()).
|
|
Where("file_id", attachmentCopy["file_id"]).
|
|
Exists()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert JSON fields to strings for storage
|
|
for _, field := range jsonFields {
|
|
if val, ok := attachmentCopy[field]; ok && val != nil {
|
|
jsonStr, err := jsoniter.MarshalToString(val)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal %s to JSON: %v", field, err)
|
|
}
|
|
attachmentCopy[field] = jsonStr
|
|
}
|
|
}
|
|
|
|
// Update or insert
|
|
if exists {
|
|
attachmentCopy["updated_at"] = time.Now()
|
|
_, err := conv.query.New().
|
|
Table(conv.getAttachmentTable()).
|
|
Where("file_id", attachmentCopy["file_id"]).
|
|
Update(attachmentCopy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return attachmentCopy["file_id"], nil
|
|
}
|
|
|
|
attachmentCopy["created_at"] = time.Now()
|
|
err = conv.query.New().
|
|
Table(conv.getAttachmentTable()).
|
|
Insert(attachmentCopy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return attachmentCopy["file_id"], nil
|
|
}
|
|
|
|
// DeleteAttachment deletes an attachment by file_id
|
|
func (conv *Xun) DeleteAttachment(fileID string) error {
|
|
// Check if attachment exists
|
|
exists, err := conv.query.New().
|
|
Table(conv.getAttachmentTable()).
|
|
Where("file_id", fileID).
|
|
Exists()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !exists {
|
|
return fmt.Errorf("attachment %s not found", fileID)
|
|
}
|
|
|
|
_, err = conv.query.New().
|
|
Table(conv.getAttachmentTable()).
|
|
Where("file_id", fileID).
|
|
Delete()
|
|
return err
|
|
}
|
|
|
|
// GetAttachments retrieves attachments with pagination and filtering
|
|
func (conv *Xun) GetAttachments(filter AttachmentFilter, locale ...string) (*AttachmentResponse, error) {
|
|
qb := conv.query.New().
|
|
Table(conv.getAttachmentTable())
|
|
|
|
// Apply UID filter if provided
|
|
if filter.UID != "" {
|
|
qb.Where("uid", filter.UID)
|
|
}
|
|
|
|
// Apply guest filter if provided
|
|
if filter.Guest != nil {
|
|
qb.Where("guest", *filter.Guest)
|
|
}
|
|
|
|
// Apply manager filter if provided
|
|
if filter.Manager != "" {
|
|
qb.Where("manager", filter.Manager)
|
|
}
|
|
|
|
// Apply content_type filter if provided
|
|
if filter.ContentType != "" {
|
|
qb.Where("content_type", filter.ContentType)
|
|
}
|
|
|
|
// Apply name filter if provided
|
|
if filter.Name != "" {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Name))
|
|
}
|
|
|
|
// Apply public filter if provided
|
|
if filter.Public != nil {
|
|
qb.Where("public", *filter.Public)
|
|
}
|
|
|
|
// Apply gzip filter if provided
|
|
if filter.Gzip != nil {
|
|
qb.Where("gzip", *filter.Gzip)
|
|
}
|
|
|
|
// Apply collection_id filter if provided
|
|
if filter.CollectionID != "" {
|
|
qb.Where("collection_id", filter.CollectionID)
|
|
}
|
|
|
|
// Apply status filter if provided
|
|
if filter.Status != "" {
|
|
qb.Where("status", filter.Status)
|
|
}
|
|
|
|
// Apply keyword filter if provided
|
|
if filter.Keywords != "" {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Keywords))
|
|
}
|
|
|
|
// Set defaults for pagination
|
|
if filter.PageSize <= 0 {
|
|
filter.PageSize = 20
|
|
}
|
|
if filter.Page <= 0 {
|
|
filter.Page = 1
|
|
}
|
|
|
|
// Get total count
|
|
total, err := qb.Clone().Count()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Calculate pagination
|
|
offset := (filter.Page - 1) * filter.PageSize
|
|
totalPages := int(math.Ceil(float64(total) / float64(filter.PageSize)))
|
|
nextPage := filter.Page + 1
|
|
if nextPage > totalPages {
|
|
nextPage = 0
|
|
}
|
|
prevPage := filter.Page - 1
|
|
if prevPage < 1 {
|
|
prevPage = 0
|
|
}
|
|
|
|
// Apply select fields if provided
|
|
if filter.Select != nil && len(filter.Select) > 0 {
|
|
selectFields := make([]interface{}, len(filter.Select))
|
|
for i, field := range filter.Select {
|
|
selectFields[i] = field
|
|
}
|
|
qb.Select(selectFields...)
|
|
}
|
|
|
|
// Get paginated results
|
|
rows, err := qb.OrderBy("created_at", "desc").
|
|
Offset(offset).
|
|
Limit(filter.PageSize).
|
|
Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert rows to map slice and parse JSON fields
|
|
data := make([]map[string]interface{}, len(rows))
|
|
jsonFields := []string{"scope"}
|
|
for i, row := range rows {
|
|
data[i] = row
|
|
// Only parse JSON fields if they are selected or no select filter is provided
|
|
if filter.Select == nil || len(filter.Select) == 0 {
|
|
conv.parseJSONFields(data[i], jsonFields)
|
|
} else {
|
|
// Parse only selected JSON fields
|
|
selectedJSONFields := []string{}
|
|
for _, field := range jsonFields {
|
|
for _, selected := range filter.Select {
|
|
if selected == field {
|
|
selectedJSONFields = append(selectedJSONFields, field)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if len(selectedJSONFields) > 0 {
|
|
conv.parseJSONFields(data[i], selectedJSONFields)
|
|
}
|
|
}
|
|
}
|
|
|
|
return &AttachmentResponse{
|
|
Data: data,
|
|
Page: filter.Page,
|
|
PageSize: filter.PageSize,
|
|
PageCnt: totalPages,
|
|
Next: nextPage,
|
|
Prev: prevPage,
|
|
Total: total,
|
|
}, nil
|
|
}
|
|
|
|
// GetAttachment retrieves a single attachment by file_id
|
|
func (conv *Xun) GetAttachment(fileID string, locale ...string) (map[string]interface{}, error) {
|
|
row, err := conv.query.New().
|
|
Table(conv.getAttachmentTable()).
|
|
Where("file_id", fileID).
|
|
First()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if row == nil {
|
|
return nil, fmt.Errorf("attachment %s not found", fileID)
|
|
}
|
|
|
|
data := row.ToMap()
|
|
if data == nil || len(data) == 0 {
|
|
return nil, fmt.Errorf("the attachment %s is empty", fileID)
|
|
}
|
|
|
|
// Parse JSON fields
|
|
jsonFields := []string{"scope"}
|
|
conv.parseJSONFields(data, jsonFields)
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// DeleteAttachments deletes attachments based on filter conditions
|
|
func (conv *Xun) DeleteAttachments(filter AttachmentFilter) (int64, error) {
|
|
qb := conv.query.New().
|
|
Table(conv.getAttachmentTable())
|
|
|
|
// Apply UID filter if provided
|
|
if filter.UID != "" {
|
|
qb.Where("uid", filter.UID)
|
|
}
|
|
|
|
// Apply guest filter if provided
|
|
if filter.Guest != nil {
|
|
qb.Where("guest", *filter.Guest)
|
|
}
|
|
|
|
// Apply manager filter if provided
|
|
if filter.Manager != "" {
|
|
qb.Where("manager", filter.Manager)
|
|
}
|
|
|
|
// Apply content_type filter if provided
|
|
if filter.ContentType != "" {
|
|
qb.Where("content_type", filter.ContentType)
|
|
}
|
|
|
|
// Apply name filter if provided
|
|
if filter.Name != "" {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Name))
|
|
}
|
|
|
|
// Apply public filter if provided
|
|
if filter.Public != nil {
|
|
qb.Where("public", *filter.Public)
|
|
}
|
|
|
|
// Apply gzip filter if provided
|
|
if filter.Gzip != nil {
|
|
qb.Where("gzip", *filter.Gzip)
|
|
}
|
|
|
|
// Apply collection_id filter if provided
|
|
if filter.CollectionID != "" {
|
|
qb.Where("collection_id", filter.CollectionID)
|
|
}
|
|
|
|
// Apply status filter if provided
|
|
if filter.Status != "" {
|
|
qb.Where("status", filter.Status)
|
|
}
|
|
|
|
// Apply keyword filter if provided
|
|
if filter.Keywords != "" {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Keywords))
|
|
}
|
|
|
|
// Execute delete and return number of deleted records
|
|
return qb.Delete()
|
|
}
|
|
|
|
// SaveKnowledge saves knowledge collection information
|
|
func (conv *Xun) SaveKnowledge(knowledge map[string]interface{}) (interface{}, error) {
|
|
// Validate required fields
|
|
requiredFields := []string{"collection_id", "name", "uid"}
|
|
for _, field := range requiredFields {
|
|
if _, ok := knowledge[field]; !ok {
|
|
return nil, fmt.Errorf("field %s is required", field)
|
|
}
|
|
if knowledge[field] == nil || knowledge[field] == "" {
|
|
return nil, fmt.Errorf("field %s cannot be empty", field)
|
|
}
|
|
}
|
|
|
|
// Create a copy of the knowledge map to avoid modifying the original
|
|
knowledgeCopy := make(map[string]interface{})
|
|
for k, v := range knowledge {
|
|
knowledgeCopy[k] = v
|
|
}
|
|
|
|
// Process JSON fields
|
|
jsonFields := []string{"scope", "option"}
|
|
for _, field := range jsonFields {
|
|
if val, ok := knowledgeCopy[field]; ok && val != nil {
|
|
// If it's a string, try to parse it first
|
|
if strVal, ok := val.(string); ok && strVal != "" {
|
|
var parsed interface{}
|
|
if err := jsoniter.UnmarshalFromString(strVal, &parsed); err == nil {
|
|
knowledgeCopy[field] = parsed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if knowledge exists
|
|
exists, err := conv.query.New().
|
|
Table(conv.getKnowledgeTable()).
|
|
Where("collection_id", knowledgeCopy["collection_id"]).
|
|
Exists()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert JSON fields to strings for storage
|
|
for _, field := range jsonFields {
|
|
if val, ok := knowledgeCopy[field]; ok && val != nil {
|
|
jsonStr, err := jsoniter.MarshalToString(val)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal %s to JSON: %v", field, err)
|
|
}
|
|
knowledgeCopy[field] = jsonStr
|
|
}
|
|
}
|
|
|
|
// Update or insert
|
|
if exists {
|
|
knowledgeCopy["updated_at"] = time.Now()
|
|
_, err := conv.query.New().
|
|
Table(conv.getKnowledgeTable()).
|
|
Where("collection_id", knowledgeCopy["collection_id"]).
|
|
Update(knowledgeCopy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return knowledgeCopy["collection_id"], nil
|
|
}
|
|
|
|
knowledgeCopy["created_at"] = time.Now()
|
|
err = conv.query.New().
|
|
Table(conv.getKnowledgeTable()).
|
|
Insert(knowledgeCopy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return knowledgeCopy["collection_id"], nil
|
|
}
|
|
|
|
// DeleteKnowledge deletes a knowledge collection by collection_id
|
|
func (conv *Xun) DeleteKnowledge(collectionID string) error {
|
|
// Check if knowledge exists
|
|
exists, err := conv.query.New().
|
|
Table(conv.getKnowledgeTable()).
|
|
Where("collection_id", collectionID).
|
|
Exists()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !exists {
|
|
return fmt.Errorf("knowledge collection %s not found", collectionID)
|
|
}
|
|
|
|
_, err = conv.query.New().
|
|
Table(conv.getKnowledgeTable()).
|
|
Where("collection_id", collectionID).
|
|
Delete()
|
|
return err
|
|
}
|
|
|
|
// GetKnowledges retrieves knowledge collections with pagination and filtering
|
|
func (conv *Xun) GetKnowledges(filter KnowledgeFilter, locale ...string) (*KnowledgeResponse, error) {
|
|
qb := conv.query.New().
|
|
Table(conv.getKnowledgeTable())
|
|
|
|
// Apply UID filter if provided
|
|
if filter.UID != "" {
|
|
qb.Where("uid", filter.UID)
|
|
}
|
|
|
|
// Apply name filter if provided
|
|
if filter.Name != "" {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Name))
|
|
}
|
|
|
|
// Apply keyword filter if provided
|
|
if filter.Keywords != "" {
|
|
qb.Where(func(qb query.Query) {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Keywords)).
|
|
OrWhere("description", "like", fmt.Sprintf("%%%s%%", filter.Keywords))
|
|
})
|
|
}
|
|
|
|
// Apply public filter if provided
|
|
if filter.Public != nil {
|
|
qb.Where("public", *filter.Public)
|
|
}
|
|
|
|
// Apply readonly filter if provided
|
|
if filter.Readonly != nil {
|
|
qb.Where("readonly", *filter.Readonly)
|
|
}
|
|
|
|
// Apply system filter if provided
|
|
if filter.System != nil {
|
|
qb.Where("system", *filter.System)
|
|
}
|
|
|
|
// Set defaults for pagination
|
|
if filter.PageSize <= 0 {
|
|
filter.PageSize = 20
|
|
}
|
|
if filter.Page <= 0 {
|
|
filter.Page = 1
|
|
}
|
|
|
|
// Get total count
|
|
total, err := qb.Clone().Count()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Calculate pagination
|
|
offset := (filter.Page - 1) * filter.PageSize
|
|
totalPages := int(math.Ceil(float64(total) / float64(filter.PageSize)))
|
|
nextPage := filter.Page + 1
|
|
if nextPage > totalPages {
|
|
nextPage = 0
|
|
}
|
|
prevPage := filter.Page - 1
|
|
if prevPage < 1 {
|
|
prevPage = 0
|
|
}
|
|
|
|
// Apply select fields if provided
|
|
if filter.Select != nil && len(filter.Select) > 0 {
|
|
selectFields := make([]interface{}, len(filter.Select))
|
|
for i, field := range filter.Select {
|
|
selectFields[i] = field
|
|
}
|
|
qb.Select(selectFields...)
|
|
}
|
|
|
|
// Get paginated results
|
|
rows, err := qb.OrderBy("sort", "asc").
|
|
OrderBy("created_at", "desc").
|
|
Offset(offset).
|
|
Limit(filter.PageSize).
|
|
Get()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert rows to map slice and parse JSON fields
|
|
data := make([]map[string]interface{}, len(rows))
|
|
jsonFields := []string{"scope", "option"}
|
|
for i, row := range rows {
|
|
data[i] = row
|
|
// Only parse JSON fields if they are selected or no select filter is provided
|
|
if filter.Select == nil || len(filter.Select) == 0 {
|
|
conv.parseJSONFields(data[i], jsonFields)
|
|
} else {
|
|
// Parse only selected JSON fields
|
|
selectedJSONFields := []string{}
|
|
for _, field := range jsonFields {
|
|
for _, selected := range filter.Select {
|
|
if selected == field {
|
|
selectedJSONFields = append(selectedJSONFields, field)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if len(selectedJSONFields) > 0 {
|
|
conv.parseJSONFields(data[i], selectedJSONFields)
|
|
}
|
|
}
|
|
}
|
|
|
|
return &KnowledgeResponse{
|
|
Data: data,
|
|
Page: filter.Page,
|
|
PageSize: filter.PageSize,
|
|
PageCnt: totalPages,
|
|
Next: nextPage,
|
|
Prev: prevPage,
|
|
Total: total,
|
|
}, nil
|
|
}
|
|
|
|
// GetKnowledge retrieves a single knowledge collection by collection_id
|
|
func (conv *Xun) GetKnowledge(collectionID string, locale ...string) (map[string]interface{}, error) {
|
|
row, err := conv.query.New().
|
|
Table(conv.getKnowledgeTable()).
|
|
Where("collection_id", collectionID).
|
|
First()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if row == nil {
|
|
return nil, fmt.Errorf("knowledge collection %s not found", collectionID)
|
|
}
|
|
|
|
data := row.ToMap()
|
|
if data == nil || len(data) == 0 {
|
|
return nil, fmt.Errorf("the knowledge collection %s is empty", collectionID)
|
|
}
|
|
|
|
// Parse JSON fields
|
|
jsonFields := []string{"scope", "option"}
|
|
conv.parseJSONFields(data, jsonFields)
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// DeleteKnowledges deletes knowledge collections based on filter conditions
|
|
func (conv *Xun) DeleteKnowledges(filter KnowledgeFilter) (int64, error) {
|
|
qb := conv.query.New().
|
|
Table(conv.getKnowledgeTable())
|
|
|
|
// Apply UID filter if provided
|
|
if filter.UID != "" {
|
|
qb.Where("uid", filter.UID)
|
|
}
|
|
|
|
// Apply name filter if provided
|
|
if filter.Name != "" {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Name))
|
|
}
|
|
|
|
// Apply keyword filter if provided
|
|
if filter.Keywords != "" {
|
|
qb.Where(func(qb query.Query) {
|
|
qb.Where("name", "like", fmt.Sprintf("%%%s%%", filter.Keywords)).
|
|
OrWhere("description", "like", fmt.Sprintf("%%%s%%", filter.Keywords))
|
|
})
|
|
}
|
|
|
|
// Apply public filter if provided
|
|
if filter.Public != nil {
|
|
qb.Where("public", *filter.Public)
|
|
}
|
|
|
|
// Apply readonly filter if provided
|
|
if filter.Readonly != nil {
|
|
qb.Where("readonly", *filter.Readonly)
|
|
}
|
|
|
|
// Apply system filter if provided
|
|
if filter.System != nil {
|
|
qb.Where("system", *filter.System)
|
|
}
|
|
|
|
// Execute delete and return number of deleted records
|
|
return qb.Delete()
|
|
}
|