feat: soporte de threads de Matrix (m.thread)
Implementa el soporte completo de threads de Matrix: - Listener extrae ThreadID de m.relates_to con rel_type=m.thread - Client.SendThreadMarkdown envia mensajes como parte de un thread usando SetThread de mautrix con fallback m.in_reply_to - Runner detecta ThreadID en ReplyAction y rutea a SendThreadMarkdown - MatrixSender interfaz actualizada con SendThreadMarkdown - runtime.go propaga ThreadID en todas las respuestas (comandos, LLM, RBAC) - sendReply helper centraliza la logica de envio con/sin thread - Auto-thread: si matrix.threads.auto_thread=true, crea thread nuevo para cada conversacion que no esta ya en un thread - Memoria por thread: usa ThreadID como clave de window cuando el mensaje esta en un thread, permitiendo conversaciones paralelas independientes - Config: matrix.threads.enabled y matrix.threads.auto_thread en ThreadsCfg Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ type MatrixSender interface {
|
||||
SendText(ctx context.Context, roomID, text string) error
|
||||
SendMarkdown(ctx context.Context, roomID, markdown string) error
|
||||
SendReplyMarkdown(ctx context.Context, roomID, inReplyTo, markdown string) error
|
||||
SendThreadMarkdown(ctx context.Context, roomID, threadRootID, inReplyTo, markdown string) error
|
||||
SendTyping(ctx context.Context, roomID string, typing bool) error
|
||||
}
|
||||
|
||||
@@ -63,15 +64,15 @@ func (r *Runner) executeOne(ctx context.Context, roomID string, a decision.Actio
|
||||
if a.Reply == nil {
|
||||
return Result{Action: a, Err: fmt.Errorf("nil reply action")}
|
||||
}
|
||||
target := roomID
|
||||
if a.Reply.ThreadID != "" {
|
||||
target = a.Reply.ThreadID
|
||||
}
|
||||
var err error
|
||||
if a.Reply.InReplyTo != "" {
|
||||
err = r.matrix.SendReplyMarkdown(ctx, target, a.Reply.InReplyTo, a.Reply.Content)
|
||||
} else {
|
||||
err = r.matrix.SendMarkdown(ctx, target, a.Reply.Content)
|
||||
switch {
|
||||
case a.Reply.ThreadID != "":
|
||||
// Thread reply: send as part of the thread with fallback in_reply_to
|
||||
err = r.matrix.SendThreadMarkdown(ctx, roomID, a.Reply.ThreadID, a.Reply.InReplyTo, a.Reply.Content)
|
||||
case a.Reply.InReplyTo != "":
|
||||
err = r.matrix.SendReplyMarkdown(ctx, roomID, a.Reply.InReplyTo, a.Reply.Content)
|
||||
default:
|
||||
err = r.matrix.SendMarkdown(ctx, roomID, a.Reply.Content)
|
||||
}
|
||||
return Result{Action: a, Output: a.Reply.Content, Err: err}
|
||||
|
||||
|
||||
@@ -320,6 +320,26 @@ func (c *Client) SendReplyMarkdown(ctx context.Context, roomID, inReplyTo, markd
|
||||
return err
|
||||
}
|
||||
|
||||
// SendThreadMarkdown sends a formatted message as part of a Matrix thread.
|
||||
// threadRootID is the event that started the thread (always the same for all messages in a thread).
|
||||
// inReplyTo is the specific event being replied to within the thread (used as fallback for non-thread clients).
|
||||
// If inReplyTo is empty, it defaults to threadRootID.
|
||||
func (c *Client) SendThreadMarkdown(ctx context.Context, roomID, threadRootID, inReplyTo, markdown string) error {
|
||||
if inReplyTo == "" {
|
||||
inReplyTo = threadRootID
|
||||
}
|
||||
html := mdToHTML(markdown)
|
||||
content := event.MessageEventContent{
|
||||
MsgType: event.MsgText,
|
||||
Body: markdown,
|
||||
Format: event.FormatHTML,
|
||||
FormattedBody: html,
|
||||
RelatesTo: (&event.RelatesTo{}).SetThread(id.EventID(threadRootID), id.EventID(inReplyTo)),
|
||||
}
|
||||
_, err := c.raw.SendMessageEvent(ctx, id.RoomID(roomID), event.EventMessage, content)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendReaction sends a reaction to an event.
|
||||
func (c *Client) SendReaction(ctx context.Context, roomID, eventID, reaction string) error {
|
||||
_, err := c.raw.SendReaction(ctx, id.RoomID(roomID), id.EventID(eventID), reaction)
|
||||
|
||||
@@ -153,6 +153,17 @@ func (l *Listener) Run(ctx context.Context) error {
|
||||
)
|
||||
msgCtx.EventID = evt.ID.String()
|
||||
|
||||
// Extract thread root from m.relates_to (Matrix thread support).
|
||||
if l.cfg.Threads.Enabled {
|
||||
if relatesTo, ok := evt.Content.Raw["m.relates_to"].(map[string]any); ok {
|
||||
if relType, _ := relatesTo["rel_type"].(string); relType == "m.thread" {
|
||||
if threadRoot, _ := relatesTo["event_id"].(string); threadRoot != "" {
|
||||
msgCtx.ThreadID = threadRoot
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
l.logger.Debug("message parsed",
|
||||
"sender", msgCtx.SenderID,
|
||||
"room", msgCtx.RoomID,
|
||||
|
||||
Reference in New Issue
Block a user