yao/agent/store/xun/message.go
Max 33efd3e890 Add Feishu and DingTalk integration support in robot lifecycle</message>
<message>
- Introduce Feishu and DingTalk adapters in the robot lifecycle for enhanced integration capabilities.
- Update the integration dispatcher to include new adapters for handling events from Feishu and DingTalk.
- Modify the configuration structure to support settings for both Feishu and DingTalk integrations.
- Enhance the integration parsing logic to recognize and process configurations for Feishu and DingTalk.
2026-03-02 07:36:34 +08:00

388 lines
9.5 KiB
Go

package xun
import (
"fmt"
"time"
"github.com/google/uuid"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/yao/agent/store/types"
)
// =============================================================================
// Message Management
// =============================================================================
// SaveMessages batch saves messages for a chat using a single database call
// This is the primary write method - messages are buffered during execution
// and batch-written at the end of a request
func (store *Xun) SaveMessages(chatID string, messages []*types.Message) error {
if chatID == "" {
return fmt.Errorf("chat_id is required")
}
if len(messages) == 0 {
return nil // Nothing to save
}
// Prepare batch insert data
now := time.Now()
rows := make([]map[string]interface{}, 0, len(messages))
for _, msg := range messages {
if msg == nil {
continue
}
// Generate message_id if not provided
messageID := msg.MessageID
if messageID == "" {
messageID = uuid.New().String()
}
// Validate required fields
if msg.Role == "" {
return fmt.Errorf("message role is required")
}
if msg.Type == "" {
return fmt.Errorf("message type is required")
}
if msg.Props == nil {
return fmt.Errorf("message props is required")
}
// Serialize JSON fields
propsJSON, err := jsoniter.MarshalToString(msg.Props)
if err != nil {
return fmt.Errorf("failed to marshal props: %w", err)
}
// Build row with all fields (including nullable ones for consistent batch insert)
row := map[string]interface{}{
"message_id": messageID,
"chat_id": chatID,
"role": msg.Role,
"type": msg.Type,
"props": propsJSON,
"sequence": msg.Sequence,
"request_id": nil,
"block_id": nil,
"thread_id": nil,
"assistant_id": nil,
"connector": nil,
"mode": nil,
"metadata": nil,
"created_at": now,
"updated_at": now,
}
// Set nullable fields if they have values
if msg.RequestID != "" {
row["request_id"] = msg.RequestID
}
if msg.BlockID != "" {
row["block_id"] = msg.BlockID
}
if msg.ThreadID != "" {
row["thread_id"] = msg.ThreadID
}
if msg.AssistantID != "" {
row["assistant_id"] = msg.AssistantID
}
if msg.Connector != "" {
row["connector"] = msg.Connector
}
if msg.Mode != "" {
row["mode"] = msg.Mode
}
if msg.Metadata != nil {
metadataJSON, err := jsoniter.MarshalToString(msg.Metadata)
if err != nil {
return fmt.Errorf("failed to marshal metadata: %w", err)
}
row["metadata"] = metadataJSON
}
rows = append(rows, row)
}
if len(rows) == 0 {
return nil
}
// Single batch insert - one database call for all messages
return store.newQueryMessage().Insert(rows)
}
// GetMessages retrieves messages for a chat with filtering
func (store *Xun) GetMessages(chatID string, filter types.MessageFilter) ([]*types.Message, error) {
if chatID == "" {
return nil, fmt.Errorf("chat_id is required")
}
qb := store.newQueryMessage().
Where("chat_id", chatID).
WhereNull("deleted_at")
// Apply filters
if filter.RequestID != "" {
qb.Where("request_id", filter.RequestID)
}
if filter.Role != "" {
qb.Where("role", filter.Role)
}
if filter.BlockID != "" {
qb.Where("block_id", filter.BlockID)
}
if filter.ThreadID != "" {
qb.Where("thread_id", filter.ThreadID)
}
if filter.Type != "" {
qb.Where("type", filter.Type)
}
// When Limit is specified WITHOUT Offset, we want the N most-recent
// messages. Strategy: query DESC to get the latest rows, then reverse
// the slice so the caller receives them in chronological (ASC) order.
// When Offset is also present, the caller is doing forward pagination,
// so we keep ASC order and apply Limit+Offset normally.
needReverse := false
if filter.Limit > 0 && filter.Offset <= 0 {
qb.Limit(filter.Limit)
qb.OrderBy("id", "desc")
needReverse = true
} else if filter.Limit > 0 && filter.Offset > 0 {
qb.Limit(filter.Limit).Offset(filter.Offset)
qb.OrderBy("id", "asc")
} else {
if filter.Offset > 0 {
qb.Limit(1000000).Offset(filter.Offset)
}
qb.OrderBy("id", "asc")
}
rows, err := qb.Get()
if err != nil {
return nil, err
}
messages := make([]*types.Message, 0, len(rows))
for _, row := range rows {
data := row.ToMap()
if data == nil || data["message_id"] == nil {
continue
}
msg, err := store.rowToMessage(data)
if err != nil {
continue
}
messages = append(messages, msg)
}
if needReverse {
for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
messages[i], messages[j] = messages[j], messages[i]
}
}
return messages, nil
}
// UpdateMessage updates a single message
func (store *Xun) UpdateMessage(messageID string, updates map[string]interface{}) error {
if messageID == "" {
return fmt.Errorf("message_id is required")
}
if len(updates) == 0 {
return fmt.Errorf("no fields to update")
}
// Check if message exists
exists, err := store.newQueryMessage().
Where("message_id", messageID).
WhereNull("deleted_at").
Exists()
if err != nil {
return err
}
if !exists {
return fmt.Errorf("message %s not found", messageID)
}
// Prepare update data
data := make(map[string]interface{})
for key, value := range updates {
// Skip system fields
if key == "message_id" || key == "chat_id" || key == "created_at" {
continue
}
// Handle JSON fields
if key == "props" || key == "metadata" {
if value != nil {
jsonStr, err := jsoniter.MarshalToString(value)
if err != nil {
return fmt.Errorf("failed to marshal %s: %w", key, err)
}
data[key] = jsonStr
} else {
data[key] = nil
}
continue
}
data[key] = value
}
// Always update updated_at
data["updated_at"] = time.Now()
if len(data) == 0 {
return fmt.Errorf("no valid fields to update")
}
_, err = store.newQueryMessage().
Where("message_id", messageID).
Update(data)
return err
}
// DeleteMessages soft deletes specific messages from a chat
func (store *Xun) DeleteMessages(chatID string, messageIDs []string) error {
if chatID == "" {
return fmt.Errorf("chat_id is required")
}
if len(messageIDs) == 0 {
return nil // Nothing to delete
}
// Soft delete all specified messages in one query
_, err := store.newQueryMessage().
Where("chat_id", chatID).
WhereIn("message_id", messageIDs).
WhereNull("deleted_at").
Update(map[string]interface{}{
"deleted_at": time.Now(),
"updated_at": time.Now(),
})
return err
}
// GetMessageByID retrieves a single message by ID
func (store *Xun) GetMessageByID(messageID string) (*types.Message, error) {
if messageID == "" {
return nil, fmt.Errorf("message_id is required")
}
row, err := store.newQueryMessage().
Where("message_id", messageID).
WhereNull("deleted_at").
First()
if err != nil {
return nil, err
}
if row == nil {
return nil, fmt.Errorf("message %s not found", messageID)
}
data := row.ToMap()
if len(data) == 0 || data["message_id"] == nil {
return nil, fmt.Errorf("message %s not found", messageID)
}
return store.rowToMessage(data)
}
// GetMessageCount returns the count of messages for a chat
func (store *Xun) GetMessageCount(chatID string) (int64, error) {
if chatID == "" {
return 0, fmt.Errorf("chat_id is required")
}
return store.newQueryMessage().
Where("chat_id", chatID).
WhereNull("deleted_at").
Count()
}
// GetLastSequence returns the last sequence number for a chat
func (store *Xun) GetLastSequence(chatID string) (int, error) {
if chatID == "" {
return 0, fmt.Errorf("chat_id is required")
}
row, err := store.newQueryMessage().
Where("chat_id", chatID).
WhereNull("deleted_at").
OrderBy("sequence", "desc").
First()
if err != nil {
return 0, err
}
if row == nil {
return 0, nil
}
data := row.ToMap()
return getInt(data, "sequence"), nil
}
// =============================================================================
// Helper Functions
// =============================================================================
// rowToMessage converts a database row to a Message struct
func (store *Xun) rowToMessage(data map[string]interface{}) (*types.Message, error) {
msg := &types.Message{
MessageID: getString(data, "message_id"),
ChatID: getString(data, "chat_id"),
RequestID: getString(data, "request_id"),
Role: getString(data, "role"),
Type: getString(data, "type"),
BlockID: getString(data, "block_id"),
ThreadID: getString(data, "thread_id"),
AssistantID: getString(data, "assistant_id"),
Connector: getString(data, "connector"),
Mode: getString(data, "mode"),
Sequence: getInt(data, "sequence"),
}
// Handle timestamps
if createdAt := getTime(data, "created_at"); createdAt != nil {
msg.CreatedAt = *createdAt
}
if updatedAt := getTime(data, "updated_at"); updatedAt != nil {
msg.UpdatedAt = *updatedAt
}
// Handle props (required)
if props := data["props"]; props != nil {
if propsStr, ok := props.(string); ok && propsStr != "" {
var propsMap map[string]interface{}
if err := jsoniter.UnmarshalFromString(propsStr, &propsMap); err == nil {
msg.Props = propsMap
}
} else if propsMap, ok := props.(map[string]interface{}); ok {
msg.Props = propsMap
}
}
// Handle metadata (optional)
if metadata := data["metadata"]; metadata != nil {
if metaStr, ok := metadata.(string); ok && metaStr != "" {
var metaMap map[string]interface{}
if err := jsoniter.UnmarshalFromString(metaStr, &metaMap); err == nil {
msg.Metadata = metaMap
}
} else if metaMap, ok := metadata.(map[string]interface{}); ok {
msg.Metadata = metaMap
}
}
return msg, nil
}