<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.
388 lines
9.5 KiB
Go
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
|
|
}
|