mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 21:39:54 +02:00
Expand an inbound Lark bot message's body before dispatch with the context
a user explicitly attached, so the agent sees a semantically complete
conversation instead of a bare "@bot 总结一下".
- post: flatten rich-text (title + paragraphs, links, @-mentions) to plain
text synchronously in the decoder.
- merge_forward: inline the forwarded transcript via a single GetMessage —
GET /open-apis/im/v1/messages/{id} returns the forward sentinel plus the
bundled children. (The issue's container_id_type=merge_forward query is
undocumented; this avoids it and also handles a forwarded quoted parent.)
- quoted reply: prepend the parent_id message as a <quoted_message> block;
a parent that is itself a forward nests a <forwarded_messages> block.
- new InboundEnricher runs in the WS connector between decode and emit,
bounded by EnrichTimeout and degrading to "[unable to fetch]" placeholders
so it never blocks the ~3s long-conn ACK budget.
/issue stays parseable on a quote-reply by parsing the command from the
user's own text (CommandBody) rather than the enriched body.
Short-window debounce batching (issue item #4) is tracked as a follow-up.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
125 lines
4.5 KiB
Go
125 lines
4.5 KiB
Go
package lark
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const messageGetPrefix = "/open-apis/im/v1/messages/"
|
|
|
|
// TestHTTPClient_GetMessageSingle exercises the happy path: a normal
|
|
// message comes back as a one-element items[] and is normalized with
|
|
// raw body.content, sender, and REST-shaped mentions intact.
|
|
func TestHTTPClient_GetMessageSingle(t *testing.T) {
|
|
fake := newLarkFake(t)
|
|
fake.stubToken("tok", 7200)
|
|
fake.mux.HandleFunc(messageGetPrefix, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
t.Errorf("want GET, got %s", r.Method)
|
|
}
|
|
if id := strings.TrimPrefix(r.URL.Path, messageGetPrefix); id != "om_parent" {
|
|
t.Errorf("path id = %q", id)
|
|
}
|
|
if r.URL.Query().Get("user_id_type") != "open_id" {
|
|
t.Errorf("missing user_id_type=open_id: %q", r.URL.RawQuery)
|
|
}
|
|
writeJSON(w, map[string]any{
|
|
"code": 0, "msg": "ok",
|
|
"data": map[string]any{
|
|
"items": []any{
|
|
map[string]any{
|
|
"message_id": "om_parent",
|
|
"msg_type": "text",
|
|
"create_time": "1000",
|
|
"sender": map[string]any{"id": "ou_a", "id_type": "open_id", "sender_type": "user"},
|
|
"body": map[string]any{"content": `{"text":"hi"}`},
|
|
"mentions": []any{map[string]any{"key": "@_user_1", "id": "ou_b", "name": "Bob"}},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
c := newTestClient(fake, time.Now)
|
|
items, err := c.GetMessage(context.Background(), testCreds(), "om_parent")
|
|
if err != nil {
|
|
t.Fatalf("GetMessage: %v", err)
|
|
}
|
|
if len(items) != 1 {
|
|
t.Fatalf("items = %d, want 1", len(items))
|
|
}
|
|
m := items[0]
|
|
if m.MessageID != "om_parent" || m.MessageType != "text" || m.Content != `{"text":"hi"}` {
|
|
t.Errorf("normalized = %+v", m)
|
|
}
|
|
if m.SenderID != "ou_a" || m.SenderType != "user" {
|
|
t.Errorf("sender = id:%q type:%q", m.SenderID, m.SenderType)
|
|
}
|
|
if len(m.Mentions) != 1 || m.Mentions[0].Key != "@_user_1" || m.Mentions[0].ID != "ou_b" || m.Mentions[0].Name != "Bob" {
|
|
t.Errorf("mentions = %+v", m.Mentions)
|
|
}
|
|
if a := fake.lastAuth(); a != "Bearer tok" {
|
|
t.Errorf("auth header = %q", a)
|
|
}
|
|
}
|
|
|
|
// TestHTTPClient_GetMessageMergeForward pins the merge_forward contract:
|
|
// GetMessage(forward_id) returns the sentinel parent followed by the
|
|
// bundled child messages, all in one items[] array.
|
|
func TestHTTPClient_GetMessageMergeForward(t *testing.T) {
|
|
fake := newLarkFake(t)
|
|
fake.stubToken("tok", 7200)
|
|
fake.mux.HandleFunc(messageGetPrefix, func(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, map[string]any{
|
|
"code": 0, "msg": "ok",
|
|
"data": map[string]any{
|
|
"items": []any{
|
|
map[string]any{"message_id": "om_fwd", "msg_type": "merge_forward", "body": map[string]any{"content": `{"content":"Merged and Forwarded Message"}`}},
|
|
map[string]any{"message_id": "c1", "msg_type": "text", "upper_message_id": "om_fwd", "create_time": "1000", "sender": map[string]any{"id": "ou_a", "sender_type": "user"}, "body": map[string]any{"content": `{"text":"one"}`}},
|
|
map[string]any{"message_id": "c2", "msg_type": "text", "upper_message_id": "om_fwd", "create_time": "2000", "sender": map[string]any{"id": "ou_b", "sender_type": "user"}, "body": map[string]any{"content": `{"text":"two"}`}},
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
c := newTestClient(fake, time.Now)
|
|
items, err := c.GetMessage(context.Background(), testCreds(), "om_fwd")
|
|
if err != nil {
|
|
t.Fatalf("GetMessage: %v", err)
|
|
}
|
|
if len(items) != 3 {
|
|
t.Fatalf("items = %d, want 3 (sentinel + 2 children)", len(items))
|
|
}
|
|
if items[0].MessageType != "merge_forward" {
|
|
t.Errorf("items[0] should be the forward sentinel, got %q", items[0].MessageType)
|
|
}
|
|
if items[1].UpperMessageID != "om_fwd" || items[2].UpperMessageID != "om_fwd" {
|
|
t.Errorf("children should link to the forward via upper_message_id")
|
|
}
|
|
}
|
|
|
|
// TestHTTPClient_GetMessageErrorCode maps a Lark business error (e.g.
|
|
// deleted / not visible) to a Go error so the enricher can degrade.
|
|
func TestHTTPClient_GetMessageErrorCode(t *testing.T) {
|
|
fake := newLarkFake(t)
|
|
fake.stubToken("tok", 7200)
|
|
fake.mux.HandleFunc(messageGetPrefix, func(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, map[string]any{"code": 230110, "msg": "message has been deleted"})
|
|
})
|
|
c := newTestClient(fake, time.Now)
|
|
if _, err := c.GetMessage(context.Background(), testCreds(), "om_gone"); err == nil {
|
|
t.Fatal("expected error for non-zero Lark code")
|
|
}
|
|
}
|
|
|
|
func TestHTTPClient_GetMessageEmptyID(t *testing.T) {
|
|
fake := newLarkFake(t)
|
|
c := newTestClient(fake, time.Now)
|
|
if _, err := c.GetMessage(context.Background(), testCreds(), ""); err == nil {
|
|
t.Fatal("expected error for empty message id")
|
|
}
|
|
}
|