feat: renderizar Markdown a HTML en mensajes Matrix con goldmark
Se reemplaza SendText por SendMarkdown en todos los puntos donde el agente envía respuestas: runtime.go (comandos built-in y tareas orquestadas), effects/runner.go (acciones Reply) y tools/matrix.go (matrix_send tool). shell/matrix/client.go ahora usa goldmark para convertir Markdown a HTML real en el campo FormattedBody del evento Matrix, cumpliendo con la spec de Matrix para mensajes formateados. El Body conserva el markdown raw como fallback. Se añade dependencia github.com/yuin/goldmark v1.7.16. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -391,7 +391,7 @@ func (a *Agent) handleTaskEvent(ctx context.Context, msg bus.AgentMessage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send reply to Matrix room
|
// Send reply to Matrix room
|
||||||
if sendErr := a.matrix.SendText(ctx, roomID, reply); sendErr != nil {
|
if sendErr := a.matrix.SendMarkdown(ctx, roomID, reply); sendErr != nil {
|
||||||
a.logger.Error("failed to send orchestrated reply to Matrix", "err", sendErr)
|
a.logger.Error("failed to send orchestrated reply to Matrix", "err", sendErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,13 +453,13 @@ func (a *Agent) handleEvent(ctx context.Context, msgCtx decision.MessageContext,
|
|||||||
if handler, ok := a.commands[cmdName]; ok {
|
if handler, ok := a.commands[cmdName]; ok {
|
||||||
a.logger.Info("command_executed", "command", cmdName)
|
a.logger.Info("command_executed", "command", cmdName)
|
||||||
reply := handler(ctx, msgCtx)
|
reply := handler(ctx, msgCtx)
|
||||||
_ = a.matrix.SendText(ctx, roomID, reply)
|
_ = a.matrix.SendMarkdown(ctx, roomID, reply)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown command — never falls through to rules or LLM
|
// Unknown command — never falls through to rules or LLM
|
||||||
a.logger.Info("command_unknown", "command", msgCtx.Command)
|
a.logger.Info("command_unknown", "command", msgCtx.Command)
|
||||||
_ = a.matrix.SendText(ctx, roomID,
|
_ = a.matrix.SendMarkdown(ctx, roomID,
|
||||||
fmt.Sprintf("Comando desconocido: !%s. Usa !help para ver comandos disponibles.", msgCtx.Command))
|
fmt.Sprintf("Comando desconocido: !%s. Usa !help para ver comandos disponibles.", msgCtx.Command))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ require (
|
|||||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||||
|
github.com/yuin/goldmark v1.7.16 // indirect
|
||||||
go.mau.fi/util v0.8.1 // indirect
|
go.mau.fi/util v0.8.1 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
golang.org/x/net v0.30.0 // indirect
|
golang.org/x/net v0.30.0 // indirect
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
|
|||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||||
|
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
|
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
go.mau.fi/util v0.8.1 h1:Ga43cz6esQBYqcjZ/onRoVnYWoUwjWbsxVeJg2jOTSo=
|
go.mau.fi/util v0.8.1 h1:Ga43cz6esQBYqcjZ/onRoVnYWoUwjWbsxVeJg2jOTSo=
|
||||||
go.mau.fi/util v0.8.1/go.mod h1:T1u/rD2rzidVrBLyaUdPpZiJdP/rsyi+aTzn0D+Q6wc=
|
go.mau.fi/util v0.8.1/go.mod h1:T1u/rD2rzidVrBLyaUdPpZiJdP/rsyi+aTzn0D+Q6wc=
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type Result struct {
|
|||||||
// MatrixSender is satisfied by shell/matrix.Client.
|
// MatrixSender is satisfied by shell/matrix.Client.
|
||||||
type MatrixSender interface {
|
type MatrixSender interface {
|
||||||
SendText(ctx context.Context, roomID, text string) error
|
SendText(ctx context.Context, roomID, text string) error
|
||||||
|
SendMarkdown(ctx context.Context, roomID, markdown string) error
|
||||||
SendTyping(ctx context.Context, roomID string, typing bool) error
|
SendTyping(ctx context.Context, roomID string, typing bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ func (r *Runner) executeOne(ctx context.Context, roomID string, a decision.Actio
|
|||||||
if a.Reply.ThreadID != "" {
|
if a.Reply.ThreadID != "" {
|
||||||
target = a.Reply.ThreadID
|
target = a.Reply.ThreadID
|
||||||
}
|
}
|
||||||
err := r.matrix.SendText(ctx, target, a.Reply.Content)
|
err := r.matrix.SendMarkdown(ctx, target, a.Reply.Content)
|
||||||
return Result{Action: a, Output: a.Reply.Content, Err: err}
|
return Result{Action: a, Output: a.Reply.Content, Err: err}
|
||||||
|
|
||||||
case decision.ActionKindSSH:
|
case decision.ActionKindSSH:
|
||||||
|
|||||||
+15
-1
@@ -3,6 +3,7 @@ package matrix
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"bytes"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
"maunium.net/go/mautrix/crypto"
|
"maunium.net/go/mautrix/crypto"
|
||||||
"maunium.net/go/mautrix/crypto/cryptohelper"
|
"maunium.net/go/mautrix/crypto/cryptohelper"
|
||||||
@@ -269,17 +271,29 @@ func (c *Client) SendText(ctx context.Context, roomID, text string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendMarkdown sends a formatted (Markdown) message to a room.
|
// SendMarkdown sends a formatted (Markdown) message to a room.
|
||||||
|
// Body contains the raw markdown (plaintext fallback per Matrix spec).
|
||||||
|
// FormattedBody contains the HTML rendered by goldmark.
|
||||||
func (c *Client) SendMarkdown(ctx context.Context, roomID, markdown string) error {
|
func (c *Client) SendMarkdown(ctx context.Context, roomID, markdown string) error {
|
||||||
|
html := mdToHTML(markdown)
|
||||||
content := event.MessageEventContent{
|
content := event.MessageEventContent{
|
||||||
MsgType: event.MsgText,
|
MsgType: event.MsgText,
|
||||||
Body: markdown,
|
Body: markdown,
|
||||||
Format: event.FormatHTML,
|
Format: event.FormatHTML,
|
||||||
FormattedBody: markdown, // mautrix can render markdown -> HTML if needed
|
FormattedBody: html,
|
||||||
}
|
}
|
||||||
_, err := c.raw.SendMessageEvent(ctx, id.RoomID(roomID), event.EventMessage, content)
|
_, err := c.raw.SendMessageEvent(ctx, id.RoomID(roomID), event.EventMessage, content)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mdToHTML converts a Markdown string to HTML using goldmark.
|
||||||
|
func mdToHTML(md string) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := goldmark.Convert([]byte(md), &buf); err != nil {
|
||||||
|
return md // fallback to raw markdown on error
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
// SendReaction sends a reaction to an event.
|
// SendReaction sends a reaction to an event.
|
||||||
func (c *Client) SendReaction(ctx context.Context, roomID, eventID, reaction string) error {
|
func (c *Client) SendReaction(ctx context.Context, roomID, eventID, reaction string) error {
|
||||||
_, err := c.raw.SendReaction(ctx, id.RoomID(roomID), id.EventID(eventID), reaction)
|
_, err := c.raw.SendReaction(ctx, id.RoomID(roomID), id.EventID(eventID), reaction)
|
||||||
|
|||||||
+2
-1
@@ -9,6 +9,7 @@ import (
|
|||||||
// Satisfied by shell/matrix.Client.
|
// Satisfied by shell/matrix.Client.
|
||||||
type MatrixSender interface {
|
type MatrixSender interface {
|
||||||
SendText(ctx context.Context, roomID, text string) error
|
SendText(ctx context.Context, roomID, text string) error
|
||||||
|
SendMarkdown(ctx context.Context, roomID, markdown string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMatrixSend creates a matrix_send tool that sends a message to a Matrix room.
|
// NewMatrixSend creates a matrix_send tool that sends a message to a Matrix room.
|
||||||
@@ -29,7 +30,7 @@ func NewMatrixSend(sender MatrixSender) Tool {
|
|||||||
return Result{Err: fmt.Errorf("matrix_send: room_id and message are required")}
|
return Result{Err: fmt.Errorf("matrix_send: room_id and message are required")}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := sender.SendText(ctx, roomID, message); err != nil {
|
if err := sender.SendMarkdown(ctx, roomID, message); err != nil {
|
||||||
return Result{Err: fmt.Errorf("matrix_send: %w", err)}
|
return Result{Err: fmt.Errorf("matrix_send: %w", err)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user