feat(channels): auto-orchestrate Placeholder/Typing/Reaction via capability interfaces

Define PlaceholderCapable, TypingCapable, and ReactionCapable interfaces
and have BaseChannel.HandleMessage auto-detect and trigger all three as
independent pipelines on inbound messages. This replaces the scattered
manual orchestration code in each channel's handleMessage with a single
unified dispatch in the framework layer.

Changes:
- Add PlaceholderCapable interface to interfaces.go
- Add ReactionCapable + RecordReactionUndo to interfaces.go
- BaseChannel.HandleMessage auto-triggers Typing → Reaction → Placeholder
- Manager gains reactionUndos sync.Map with TTL janitor cleanup
- Telegram: extract SendPlaceholder from manual code, add StartTyping
- Discord: add SendPlaceholder + StartTyping
- Pico: add SendPlaceholder (uses Pico Protocol message.create)
- Slack: extract ReactToMessage from manual code
- OneBot: extract ReactToMessage, remove leaked pendingEmojiMsg sync.Map
- LINE: move group-chat guard into StartTyping, remove manual orchestration
- Config: add Placeholder to PicoConfig; remove from Slack/LINE/OneBot
  (no MessageEditor, so placeholder config was dead code)
This commit is contained in:
Hoshina
2026-02-27 03:02:40 +08:00
committed by 美電球
parent ba98069a00
commit 29ed650107
10 changed files with 268 additions and 142 deletions
+30 -27
View File
@@ -23,21 +23,20 @@ import (
type OneBotChannel struct {
*channels.BaseChannel
config config.OneBotConfig
conn *websocket.Conn
ctx context.Context
cancel context.CancelFunc
dedup map[string]struct{}
dedupRing []string
dedupIdx int
mu sync.Mutex
writeMu sync.Mutex
echoCounter int64
selfID int64
pending map[string]chan json.RawMessage
pendingMu sync.Mutex
lastMessageID sync.Map
pendingEmojiMsg sync.Map
config config.OneBotConfig
conn *websocket.Conn
ctx context.Context
cancel context.CancelFunc
dedup map[string]struct{}
dedupRing []string
dedupIdx int
mu sync.Mutex
writeMu sync.Mutex
echoCounter int64
selfID int64
pending map[string]chan json.RawMessage
pendingMu sync.Mutex
lastMessageID sync.Map
}
type oneBotRawEvent struct {
@@ -129,6 +128,22 @@ func (c *OneBotChannel) setMsgEmojiLike(messageID string, emojiID int, set bool)
}()
}
// ReactToMessage implements channels.ReactionCapable.
// It adds an emoji reaction (ID 289) to group messages and returns an undo function.
// Private messages return a no-op since reactions are only meaningful in groups.
func (c *OneBotChannel) ReactToMessage(ctx context.Context, chatID, messageID string) (func(), error) {
// Only react in group chats
if !strings.HasPrefix(chatID, "group:") {
return func() {}, nil
}
c.setMsgEmojiLike(messageID, 289, true)
return func() {
c.setMsgEmojiLike(messageID, 289, false)
}, nil
}
func (c *OneBotChannel) Start(ctx context.Context) error {
if c.config.WSUrl == "" {
return fmt.Errorf("OneBot ws_url not configured")
@@ -1044,18 +1059,6 @@ func (c *OneBotChannel) handleMessage(raw *oneBotRawEvent) {
c.lastMessageID.Store(chatID, messageID)
if raw.MessageType == "group" && messageID != "" && messageID != "0" {
c.setMsgEmojiLike(messageID, 289, true)
c.pendingEmojiMsg.Store(chatID, messageID)
// Register emoji stop with Manager for outbound orchestration
if rec := c.GetPlaceholderRecorder(); rec != nil {
capturedMsgID := messageID
rec.RecordTypingStop("onebot", chatID, func() {
c.setMsgEmojiLike(capturedMsgID, 289, false)
})
}
}
senderInfo := bus.SenderInfo{
Platform: "onebot",
PlatformID: senderID,