mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-12 15:04:53 +00:00
ad893908a5
* fix(telegram): propagate local_key for forum topic routing in team notifications
Team task status messages (dispatched, completed, progress) were always
delivered to the General topic in Telegram forum groups because the
notification pipeline had no access to the originating topic's local_key.
Root cause: wireTeamProgressNotifySubscriber in gateway_events.go published
OutboundMessage with no Metadata, so the Telegram adapter had no
message_thread_id to route to the correct forum topic.
Fix has two parts:
1. Team notify path (root cause):
- Add LocalKey field to TeamTaskEventPayload (protocol)
- Extract local_key from tool context in WithContextInfo()
- Add LocalKey to NotifyRoutingMeta
- Pass LocalKey through to OutboundMessage Metadata in both
leader mode (InboundMessage) and direct mode (OutboundMessage)
2. MCP bridge context (supporting):
- Propagate local_key and session_key through bridge HTTP headers
- Add X-Local-Key and X-Session-Key to BridgeContext
- Extract and inject into tool context in gateway middleware
- Include in HMAC signature for integrity
Closes #798
* fix(telegram): pass LocalKey from task metadata in all dispatch/fail broadcast sites
The initial fix added LocalKey to the event payload and WithContextInfo(),
but 4 broadcast call sites use individual With* options instead of
WithContextInfo — so LocalKey was never populated for:
- fallback_dispatch (team_tasks_create.go)
- dispatch_unblocked (team_tool_dispatch.go)
- post_turn dispatch (team_tool_validation.go)
- blocker/fail (team_tasks_blocker.go)
Add WithLocalKey() option function and extract TaskMetaLocalKey from task
metadata at each site, matching the existing TaskMetaPeerKind pattern.
* test(mcp): add HMAC verification tests for extra params (localKey, sessionKey)
- Add tests for SignBridgeContext/VerifyBridgeContext with extra params
- Test backward compat fallback for pre-localKey sessions
- Test that param order matters in signature
- Add clarifying comment for routing context injection security model
---------
Co-authored-by: Jens Henke <jens@henke.dk>
Co-authored-by: viettranx <viettranx@gmail.com>
105 lines
2.5 KiB
Go
105 lines
2.5 KiB
Go
package tools
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// NotifyRoutingMeta carries routing info for batched team notifications.
|
|
type NotifyRoutingMeta struct {
|
|
Mode string // "direct" or "leader"
|
|
Channel string
|
|
ChatID string
|
|
UserID string
|
|
LeadAgent string // agent key (only used in leader mode)
|
|
PeerKind string // "group" or "direct" — routes to correct session (#266)
|
|
LocalKey string // composite key with topic suffix for forum routing
|
|
}
|
|
|
|
// TeamNotifyQueue batches team task notifications per chat with debounce,
|
|
// following the same pattern as AnnounceQueue for subagent results.
|
|
type TeamNotifyQueue struct {
|
|
mu sync.Mutex
|
|
batches map[string]*notifyBatch // key: "teamID:chatID"
|
|
debounce time.Duration
|
|
cap int // immediate drain threshold
|
|
onDrain func(items []string, meta NotifyRoutingMeta)
|
|
}
|
|
|
|
type notifyBatch struct {
|
|
items []string
|
|
timer *time.Timer
|
|
meta NotifyRoutingMeta
|
|
}
|
|
|
|
// NewTeamNotifyQueue creates a notification queue with debounce and drain callback.
|
|
func NewTeamNotifyQueue(debounceMs int, onDrain func(items []string, meta NotifyRoutingMeta)) *TeamNotifyQueue {
|
|
if debounceMs <= 0 {
|
|
debounceMs = 2000
|
|
}
|
|
return &TeamNotifyQueue{
|
|
batches: make(map[string]*notifyBatch),
|
|
debounce: time.Duration(debounceMs) * time.Millisecond,
|
|
cap: 20,
|
|
onDrain: onDrain,
|
|
}
|
|
}
|
|
|
|
// Enqueue adds a formatted notification line to the batch for the given key.
|
|
// Resets debounce timer. Drains immediately if cap is reached.
|
|
func (q *TeamNotifyQueue) Enqueue(key string, content string, meta NotifyRoutingMeta) {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
|
|
b, ok := q.batches[key]
|
|
if !ok {
|
|
b = ¬ifyBatch{meta: meta}
|
|
q.batches[key] = b
|
|
}
|
|
b.items = append(b.items, content)
|
|
|
|
// Immediate drain if cap reached.
|
|
if len(b.items) >= q.cap {
|
|
if b.timer != nil {
|
|
b.timer.Stop()
|
|
}
|
|
items := b.items
|
|
bMeta := b.meta
|
|
delete(q.batches, key)
|
|
go q.drain(items, bMeta)
|
|
return
|
|
}
|
|
|
|
// Reset debounce timer.
|
|
if b.timer != nil {
|
|
b.timer.Stop()
|
|
}
|
|
b.timer = time.AfterFunc(q.debounce, func() {
|
|
q.mu.Lock()
|
|
b, ok := q.batches[key]
|
|
if !ok {
|
|
q.mu.Unlock()
|
|
return
|
|
}
|
|
items := b.items
|
|
bMeta := b.meta
|
|
delete(q.batches, key)
|
|
q.mu.Unlock()
|
|
|
|
q.drain(items, bMeta)
|
|
})
|
|
}
|
|
|
|
func (q *TeamNotifyQueue) drain(items []string, meta NotifyRoutingMeta) {
|
|
if len(items) == 0 || q.onDrain == nil {
|
|
return
|
|
}
|
|
q.onDrain(items, meta)
|
|
}
|
|
|
|
// FormatBatchedNotify joins notification lines into a single message.
|
|
func FormatBatchedNotify(items []string) string {
|
|
return strings.Join(items, "\n")
|
|
}
|