Files
Cyrene_v2/internal/services/channelbridge/bubbles.go
T
Aska bcf7fe5de0
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
refactor(channelbridge): 多气泡分割+旁白剥离+随机延迟上移为IM公共出口——sendReply/SendProactive统一(微信主动推送也分割)
2026-09-08 16:26:35 +08:00

54 lines
2.4 KiB
Go

package channelbridge
import (
"context"
"fmt"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
)
// bubbleSender is the per-bubble outbound call the shared IM dispatcher uses. A
// reply exits through the adapter's SendMessage (UnifiedResponse) path, a proactive
// push through its SendProactive path — both collapse to "dispatch one chat bubble to
// the platform", which is what lets the reply and the proactive push share a single
// sanitize/split/gap pipeline instead of each re-implementing it.
type bubbleSender func(ctx context.Context, msg adapter.UnifiedMessage) error
// sendIMBubbles is the single IM outbound exit shared by sendReply (reply path) and
// SendProactive (proactive push). It turns content into the plain chat bubbles an IM
// channel should receive: role-play/action/narration markup (rendered as muted italic
// on the Web) is stripped and each <p> paragraph is delivered as its own
// bubble/message (design/17 §2), with a multi-bubble burst spacing its sends 0.5-2s
// apart so it does not flood the channel. The Web frontend is never affected — it
// renders the persisted stream directly and does not route through here.
//
// target carries the platform/channel targeting fields and is copied onto every
// bubble (only Content/ContentType/Timestamp differ per bubble); sender is the
// adapter's outbound call. A content that strips to nothing (a pure action line) is
// dropped without a send, and a per-bubble failure is wrapped with its index.
func (b *Bridge) sendIMBubbles(ctx context.Context, target adapter.UnifiedMessage, sender bubbleSender, content string) error {
// splitBubbles applies stripNarration per bubble (so <action>/<narration> and the
// star-drift role-play forms are removed for IM) and splits on <p>. It is the
// single sanitize entry point, shared with the old reply path.
bubbles := splitBubbles(content)
if len(bubbles) == 0 {
b.log.Debug("im outbound has no plain text, skipped", "platform", target.Platform, "channel", target.ChannelID)
return nil
}
for i, bubble := range bubbles {
if i > 0 {
time.Sleep(b.bubbleGapFn())
}
out := target
out.Content = bubble
out.ContentType = adapter.ContentText
out.Timestamp = time.Now().UTC()
// Each bubble is its own send so the gap lands between messages.
if err := sender(ctx, out); err != nil {
return fmt.Errorf("channelbridge: send bubble #%d of %d: %w", i+1, len(bubbles), err)
}
}
return nil
}