feat: add msglib receive adapter

This commit is contained in:
zhaoyilun
2026-07-10 08:59:36 +08:00
parent 44d27879f6
commit 0820717767
8 changed files with 295 additions and 13 deletions

View File

@@ -0,0 +1,98 @@
package isphere
import (
"context"
"fmt"
"strings"
"isphere-ai-bridge/internal/msglib"
)
type MsgLibMessageLister interface {
ListMessages(ctx context.Context, opts msglib.ListMessagesOptions) (msglib.ListMessagesResult, error)
}
type MsgLibMessageSource struct {
Lister MsgLibMessageLister
IncludeBody bool
IncludeAttachmentMetadata bool
}
func (s MsgLibMessageSource) ReceiveMessages(ctx context.Context, query ReceiveMessagesQuery) (ReceiveMessagesResult, error) {
if s.Lister == nil {
return ReceiveMessagesResult{}, fmt.Errorf("msglib message source requires a lister")
}
result, err := s.Lister.ListMessages(ctx, msglib.ListMessagesOptions{
ConversationID: query.ConversationID,
ConversationType: "auto",
Query: query.Query,
Since: query.Since,
Limit: query.Limit,
IncludeBody: s.IncludeBody,
IncludeAttachmentMetadata: s.IncludeAttachmentMetadata,
})
if err != nil {
return ReceiveMessagesResult{}, err
}
if !result.ReadOnly {
return ReceiveMessagesResult{}, fmt.Errorf("msglib list_messages did not report read_only=true")
}
if result.RawRowsReturned {
return ReceiveMessagesResult{}, fmt.Errorf("msglib list_messages returned raw rows")
}
if result.FilePathsReturned {
return ReceiveMessagesResult{}, fmt.Errorf("msglib list_messages returned file path values")
}
messages := make([]Message, 0, len(result.Messages))
for _, item := range result.Messages {
messages = append(messages, msgLibListMessageToReceiveMessage(item))
}
return ReceiveMessagesResult{Messages: messages}, nil
}
func msgLibListMessageToReceiveMessage(item msglib.ListMessage) Message {
return Message{
ID: firstNonEmpty(item.ID, item.MessageID),
ConversationID: item.ConversationID,
ConversationType: item.ConversationType,
SenderID: item.SenderID,
ReceiverID: item.ReceiverID,
Text: firstNonEmpty(item.ContentText, item.Text),
Timestamp: firstNonEmpty(item.Timestamp, item.CreatedAt),
Subject: item.Subject,
Read: item.Read,
Source: firstNonEmpty(item.Source, "local_readonly"),
RawRef: item.RawRef,
Attachments: msgLibAttachmentsToReceiveAttachments(item.Attachments),
}
}
func msgLibAttachmentsToReceiveAttachments(items []msglib.ListMessageAttachment) []MessageAttachment {
if len(items) == 0 {
return nil
}
attachments := make([]MessageAttachment, 0, len(items))
for _, item := range items {
attachment := MessageAttachment{
FileID: item.FileID,
FileName: item.FileName,
DownloadRef: item.DownloadRef,
}
if item.SizeBytes > 0 {
size := item.SizeBytes
attachment.SizeBytes = &size
}
attachments = append(attachments, attachment)
}
return attachments
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}

View File

@@ -0,0 +1,98 @@
package isphere
import (
"context"
"testing"
"isphere-ai-bridge/internal/msglib"
)
func TestMsgLibMessageSourceMapsListMessagesToReceiveMessages(t *testing.T) {
fake := &fakeMsgLibMessageLister{
result: msglib.ListMessagesResult{
ReadOnly: true,
MessageBodyValuesReturned: true,
RawRowsReturned: false,
FilePathsReturned: false,
SourceTables: []string{"tblMsgGroupPersonMsg"},
Messages: []msglib.ListMessage{{
MessageID: "db-msg-1",
ID: "db-msg-1",
ConversationID: "group-redacted",
ConversationType: "group",
SenderID: "sender-redacted",
SenderName: "Sender Redacted",
ReceiverID: "",
Text: "redacted body",
ContentText: "redacted body",
ContentType: "file",
Timestamp: "1783423807000",
CreatedAt: "2026-07-10T00:00:00Z",
Subject: "redacted subject",
Read: true,
Source: "local_readonly",
RawRef: "msglib:tblMsgGroupPersonMsg",
Attachments: []msglib.ListMessageAttachment{{
FileID: "file-redacted-1",
FileName: "redacted.docx",
SizeBytes: 1234,
DownloadRef: "",
}},
}},
},
}
source := MsgLibMessageSource{
Lister: fake,
IncludeBody: true,
IncludeAttachmentMetadata: true,
}
got, err := source.ReceiveMessages(context.Background(), ReceiveMessagesQuery{
ConversationID: "group-redacted",
Query: "redacted",
Since: "2026-07-10T00:00:00Z",
Limit: 5,
})
if err != nil {
t.Fatalf("ReceiveMessages returned error: %v", err)
}
if len(fake.calls) != 1 {
t.Fatalf("ListMessages calls = %d, want 1", len(fake.calls))
}
call := fake.calls[0]
if call.ConversationID != "group-redacted" || call.Query != "redacted" || call.Since != "2026-07-10T00:00:00Z" || call.Limit != 5 {
t.Fatalf("ListMessages options = %+v", call)
}
if !call.IncludeBody || !call.IncludeAttachmentMetadata {
t.Fatalf("ListMessages include flags = body:%v attachments:%v", call.IncludeBody, call.IncludeAttachmentMetadata)
}
if len(got.Messages) != 1 {
t.Fatalf("messages = %+v, want one", got.Messages)
}
msg := got.Messages[0]
if msg.ID != "db-msg-1" || msg.ConversationID != "group-redacted" || msg.ConversationType != "group" {
t.Fatalf("unexpected message identity: %+v", msg)
}
if msg.SenderID != "sender-redacted" || msg.ReceiverID != "" || msg.Text != "redacted body" || msg.Subject != "redacted subject" || !msg.Read {
t.Fatalf("unexpected message fields: %+v", msg)
}
if msg.Source != "local_readonly" || msg.RawRef != "msglib:tblMsgGroupPersonMsg" {
t.Fatalf("source refs = %q/%q", msg.Source, msg.RawRef)
}
if len(msg.Attachments) != 1 || msg.Attachments[0].FileName != "redacted.docx" || msg.Attachments[0].DownloadRef != "" {
t.Fatalf("attachments = %+v", msg.Attachments)
}
if msg.Attachments[0].SizeBytes == nil || *msg.Attachments[0].SizeBytes != 1234 {
t.Fatalf("attachment size = %+v", msg.Attachments[0].SizeBytes)
}
}
type fakeMsgLibMessageLister struct {
calls []msglib.ListMessagesOptions
result msglib.ListMessagesResult
}
func (f *fakeMsgLibMessageLister) ListMessages(_ context.Context, opts msglib.ListMessagesOptions) (msglib.ListMessagesResult, error) {
f.calls = append(f.calls, opts)
return f.result, nil
}

View File

@@ -22,6 +22,16 @@ type Message struct {
Subject string
ReceiptID string
Read bool
Source string
RawRef string
Attachments []MessageAttachment
}
type MessageAttachment struct {
FileID string
FileName string
SizeBytes *int64
DownloadRef string
}
type ReceiveMessagesQuery struct {