fix(agent): route cron job messages to originating chat session

When a cron job fires, ProcessDirectWithChannel creates an
InboundMessage with Peer.Kind empty. extractPeer returned nil in this
case, causing resolveMessageRoute to fall through to the default route
(agent:main:main) instead of routing to the originating Telegram/DingTalk
chat session.

This change makes extractPeer treat empty Peer.Kind with a non-empty
ChatID as a direct message (Kind="direct", ID=ChatID), ensuring the
cron job response is routed to the correct chat session.

Fixes #2275

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
merlinmiao 2026-04-05 18:34:48 +08:00
parent 15a70ac45c
commit 12eb3a7e9f
2 changed files with 106 additions and 4 deletions

View file

@ -3362,18 +3362,25 @@ func mapCommandError(result commands.ExecuteResult) string {
// extractPeer extracts the routing peer from the inbound message's structured Peer field.
func extractPeer(msg bus.InboundMessage) *routing.RoutePeer {
if msg.Peer.Kind == "" {
peerKind := msg.Peer.Kind
peerID := msg.Peer.ID
// When Peer.Kind is empty but ChatID is set (e.g., cron jobs), treat as a
// direct message so session routing resolves to the correct chat session.
if peerKind == "" && msg.ChatID != "" {
peerKind = "direct"
peerID = msg.ChatID
}
if peerKind == "" {
return nil
}
peerID := msg.Peer.ID
if peerID == "" {
if msg.Peer.Kind == "direct" {
if peerKind == "direct" {
peerID = msg.SenderID
} else {
peerID = msg.ChatID
}
}
return &routing.RoutePeer{Kind: msg.Peer.Kind, ID: peerID}
return &routing.RoutePeer{Kind: peerKind, ID: peerID}
}
func inboundMetadata(msg bus.InboundMessage, key string) string {

View file

@ -3046,3 +3046,98 @@ func TestProcessMessage_ContextOverflow_AnthropicStyle(t *testing.T) {
t.Fatalf("expected 2 calls for retry, got %d", provider.calls)
}
}
func TestExtractPeer_CronJobWithEmptyPeerKind(t *testing.T) {
// When Peer.Kind is empty but ChatID is set (e.g., cron jobs),
// extractPeer should treat it as a direct message using ChatID.
// This ensures cron job responses route to the correct chat session.
msg := bus.InboundMessage{
Channel: "telegram",
SenderID: "cron",
ChatID: "439850467",
Peer: bus.Peer{}, // Kind is empty
}
got := extractPeer(msg)
if got == nil {
t.Fatal("extractPeer returned nil, want Peer with Kind=direct")
}
if got.Kind != "direct" {
t.Errorf("got.Kind = %q, want %q", got.Kind, "direct")
}
if got.ID != "439850467" {
t.Errorf("got.ID = %q, want %q", got.ID, "439850467")
}
}
func TestExtractPeer_EmptyPeerKindAndEmptyChatID(t *testing.T) {
// When both Peer.Kind and ChatID are empty, extractPeer should return nil.
msg := bus.InboundMessage{
Channel: "telegram",
SenderID: "cron",
Peer: bus.Peer{},
}
got := extractPeer(msg)
if got != nil {
t.Errorf("extractPeer returned %+v, want nil", got)
}
}
func TestExtractPeer_NormalDirectMessage(t *testing.T) {
// Normal direct message with Peer.Kind and Peer.ID set.
msg := bus.InboundMessage{
Channel: "telegram",
SenderID: "user1",
ChatID: "chat1",
Peer: bus.Peer{
Kind: "direct",
ID: "user1",
},
}
got := extractPeer(msg)
if got == nil {
t.Fatal("extractPeer returned nil")
}
if got.Kind != "direct" || got.ID != "user1" {
t.Errorf("extractPeer = %+v, want Kind=direct, ID=user1", got)
}
}
func TestExtractPeer_DirectMessageWithEmptyPeerID(t *testing.T) {
// When Peer.Kind is "direct" but Peer.ID is empty, should use SenderID.
msg := bus.InboundMessage{
Channel: "telegram",
SenderID: "user1",
ChatID: "chat1",
Peer: bus.Peer{
Kind: "direct",
ID: "",
},
}
got := extractPeer(msg)
if got == nil {
t.Fatal("extractPeer returned nil")
}
if got.ID != "user1" {
t.Errorf("got.ID = %q, want %q", got.ID, "user1")
}
}
func TestExtractPeer_GroupMessageWithEmptyPeerID(t *testing.T) {
// When Peer.Kind is "group" but Peer.ID is empty, should use ChatID.
msg := bus.InboundMessage{
Channel: "telegram",
SenderID: "user1",
ChatID: "group_chat_123",
Peer: bus.Peer{
Kind: "group",
ID: "",
},
}
got := extractPeer(msg)
if got == nil {
t.Fatal("extractPeer returned nil")
}
if got.ID != "group_chat_123" {
t.Errorf("got.ID = %q, want %q", got.ID, "group_chat_123")
}
}