feat: add msglib bounded message listing

This commit is contained in:
zhaoyilun
2026-07-10 08:54:11 +08:00
parent c0287fa2e6
commit 44d27879f6
8 changed files with 922 additions and 17 deletions

View File

@@ -22,6 +22,9 @@ const (
DefaultDisplayEntityLimit = 25
MaxDisplayEntityLimit = 100
DefaultListMessagesLimit = 20
MaxListMessagesLimit = 50
)
var requestCounter uint64
@@ -97,6 +100,54 @@ type MessageSource struct {
RowCount string `json:"row_count"`
}
type ListMessagesOptions struct {
ConversationID string
ConversationType string
Query string
Since string
Cursor string
Limit int
IncludeBody bool
IncludeAttachmentMetadata bool
}
type ListMessagesResult struct {
ReadOnly bool `json:"read_only"`
MessageBodyValuesReturned bool `json:"message_body_values_returned"`
RawRowsReturned bool `json:"raw_rows_returned"`
FilePathsReturned bool `json:"file_paths_returned"`
SourceTables []string `json:"source_tables"`
Messages []ListMessage `json:"messages"`
NextCursor string `json:"next_cursor"`
}
type ListMessage struct {
MessageID string `json:"message_id"`
ID string `json:"id"`
ConversationID string `json:"conversation_id"`
ConversationType string `json:"conversation_type"`
SenderID string `json:"sender_id"`
SenderName string `json:"sender_name"`
ReceiverID string `json:"receiver_id"`
Text string `json:"text"`
ContentText string `json:"content_text"`
ContentType string `json:"content_type"`
Timestamp string `json:"timestamp"`
CreatedAt string `json:"created_at"`
Subject string `json:"subject"`
Read bool `json:"read"`
Source string `json:"source"`
RawRef string `json:"raw_ref"`
Attachments []ListMessageAttachment `json:"attachments"`
}
type ListMessageAttachment struct {
FileID string `json:"file_id"`
FileName string `json:"file_name"`
SizeBytes int64 `json:"size_bytes"`
DownloadRef string `json:"download_ref"`
}
type DisplayEntitiesOptions struct {
EntityType string
Query string
@@ -162,6 +213,46 @@ func (c *Client) MessageSources(ctx context.Context) ([]MessageSource, error) {
return out.MessageSources, nil
}
func (c *Client) ListMessages(ctx context.Context, opts ListMessagesOptions) (ListMessagesResult, error) {
if strings.TrimSpace(opts.Cursor) != "" {
return ListMessagesResult{}, errors.New("msglib list_messages cursor pagination is not available yet")
}
limit := opts.Limit
if limit <= 0 {
limit = DefaultListMessagesLimit
}
if limit > MaxListMessagesLimit {
limit = MaxListMessagesLimit
}
args := map[string]any{
"sqlite_dll_path": c.config.SQLiteDLLPath,
"db_path": c.config.DBPath,
"password": c.config.Password,
"limit": limit,
"include_body": opts.IncludeBody,
"include_attachment_metadata": opts.IncludeAttachmentMetadata,
}
if strings.TrimSpace(opts.ConversationID) != "" {
args["conversation_id"] = strings.TrimSpace(opts.ConversationID)
}
if strings.TrimSpace(opts.ConversationType) != "" {
args["conversation_type"] = strings.TrimSpace(opts.ConversationType)
}
if strings.TrimSpace(opts.Query) != "" {
args["query"] = strings.TrimSpace(opts.Query)
}
if strings.TrimSpace(opts.Since) != "" {
args["since"] = strings.TrimSpace(opts.Since)
}
var out ListMessagesResult
if err := c.call(ctx, "list_messages", args, &out); err != nil {
return ListMessagesResult{}, err
}
return out, nil
}
func (c *Client) DisplayEntities(ctx context.Context, opts DisplayEntitiesOptions) ([]DisplayEntity, error) {
entityType := strings.TrimSpace(opts.EntityType)
if entityType != EntityTypeContacts && entityType != EntityTypeGroups {

View File

@@ -81,6 +81,46 @@ func TestMessageSourcesPassesConfiguredPathsAndDecodesMetadata(t *testing.T) {
}
}
func TestListMessagesPassesBoundedArgsAndDecodesNormalizedMessages(t *testing.T) {
cfg := Config{
SQLiteDLLPath: `C:\fixture\System.Data.SQLite.dll`,
DBPath: `C:\fixture\MsgLib.db`,
Password: "123",
}
client := newFakeClient(t, "list_messages_ok", cfg)
got, err := client.ListMessages(context.Background(), ListMessagesOptions{
ConversationType: "direct",
Query: "redacted",
Since: "2026-07-10T00:00:00Z",
Limit: 99,
IncludeBody: false,
IncludeAttachmentMetadata: true,
})
if err != nil {
t.Fatalf("ListMessages returned error: %v", err)
}
if !got.ReadOnly || got.MessageBodyValuesReturned || got.RawRowsReturned || got.FilePathsReturned {
t.Fatalf("unexpected safety flags: %+v", got)
}
if strings.Join(got.SourceTables, ",") != "tblPersonMsg" {
t.Fatalf("SourceTables = %#v", got.SourceTables)
}
if len(got.Messages) != 1 {
t.Fatalf("len(Messages) = %d, want 1", len(got.Messages))
}
msg := got.Messages[0]
if msg.MessageID != "msg-redacted-1" || msg.ConversationType != "direct" || msg.RawRef != "msglib:tblPersonMsg" {
t.Fatalf("unexpected message identity: %+v", msg)
}
if msg.ContentText != "" || msg.Text != "" {
t.Fatalf("content should be omitted when include_body=false: %+v", msg)
}
if len(msg.Attachments) != 1 || msg.Attachments[0].FileName != "redacted.docx" || msg.Attachments[0].DownloadRef != "" {
t.Fatalf("unexpected attachments: %+v", msg.Attachments)
}
}
func TestDisplayEntitiesPassesBoundedArgsAndDecodesMetadata(t *testing.T) {
cfg := Config{
SQLiteDLLPath: `C:\fixture\System.Data.SQLite.dll`,
@@ -307,6 +347,49 @@ func fakeSidecarMain() int {
"error": nil,
"warnings": []any{},
})
case "list_messages":
args, _ := req["args"].(map[string]any)
if args["sqlite_dll_path"] != `C:\fixture\System.Data.SQLite.dll` || args["db_path"] != `C:\fixture\MsgLib.db` || args["password"] != "123" || args["conversation_type"] != "direct" || args["query"] != "redacted" || args["since"] != "2026-07-10T00:00:00Z" || args["limit"] != float64(50) || args["include_body"] != false || args["include_attachment_metadata"] != true {
fmt.Fprintf(os.Stderr, "bad list_messages args: %#v\n", args)
return 2
}
return writeFakeResponse(map[string]any{
"protocol": Protocol,
"request_id": requestID,
"op": op,
"ok": true,
"data": map[string]any{
"read_only": true,
"message_body_values_returned": false,
"raw_rows_returned": false,
"file_paths_returned": false,
"source_tables": []any{"tblPersonMsg"},
"messages": []any{
map[string]any{
"message_id": "msg-redacted-1",
"id": "msg-redacted-1",
"conversation_id": "contact-redacted",
"conversation_type": "direct",
"sender_id": "sender-redacted",
"sender_name": "Sender Redacted",
"receiver_id": "receiver-redacted",
"text": "",
"content_text": "",
"content_type": "file",
"timestamp": "2026-07-10T00:01:02Z",
"created_at": "2026-07-10T00:01:02Z",
"read": true,
"source": "local_readonly",
"raw_ref": "msglib:tblPersonMsg",
"attachments": []any{
map[string]any{"file_id": "msg-redacted-1:redacted.docx", "file_name": "redacted.docx", "size_bytes": float64(1234), "download_ref": ""},
},
},
},
},
"error": nil,
"warnings": []any{},
})
case "display_entities":
args, _ := req["args"].(map[string]any)
if args["sqlite_dll_path"] != `C:\fixture\System.Data.SQLite.dll` || args["db_path"] != `C:\fixture\MsgLib.db` || args["password"] != "123" || args["entity_type"] != "contacts" || args["query"] != "alice" || args["limit"] != float64(2) {