From d5ce331ea72802d0e1721593b41f352cde44c77d Mon Sep 17 00:00:00 2001 From: xxb Date: Thu, 9 Apr 2026 18:56:11 +0800 Subject: [PATCH] fix(bridge): reconstruct relay reply context (#514) --- core/bridge.go | 45 ++++++++++++++++++++++++++++++++++- core/bridge_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/core/bridge.go b/core/bridge.go index 1235e98..e399041 100644 --- a/core/bridge.go +++ b/core/bridge.go @@ -60,6 +60,18 @@ type bridgeReplyCtx struct { ReplyCtx string `json:"reply_ctx"` } +const bridgeReconstructReplyCtxKind = "bridge_reconstruct" + +// bridgeReconstructReplyCtxPayload is a forward-compatible reply envelope for +// reconstruct_reply adapters. Receivers should ignore unknown fields. +type bridgeReconstructReplyCtxPayload struct { + Kind string `json:"kind"` + Version int `json:"v"` + SenderProject string `json:"sender_project"` + TransportChatID string `json:"transport_chat_id"` + TransportSessionKey string `json:"transport_session_key,omitempty"` +} + // --- Wire protocol messages --- type bridgeMsg struct { @@ -305,13 +317,44 @@ func (bp *BridgePlatform) ReconstructReplyCtx(sessionKey string) (any, error) { if !a.capabilities["reconstruct_reply"] { return nil, fmt.Errorf("bridge: adapter %q does not support reconstruct_reply", platform) } + replyCtx, err := buildBridgeReconstructReplyCtx(bp.project, sessionKey) + if err != nil { + return nil, err + } return &bridgeReplyCtx{ Platform: platform, SessionKey: sessionKey, - ReplyCtx: sessionKey, + ReplyCtx: replyCtx, }, nil } +func buildBridgeReconstructReplyCtx(project, sessionKey string) (string, error) { + chatID, err := bridgeTransportChatID(sessionKey) + if err != nil { + return "", err + } + payload := bridgeReconstructReplyCtxPayload{ + Kind: bridgeReconstructReplyCtxKind, + Version: 1, + SenderProject: project, + TransportChatID: chatID, + TransportSessionKey: sessionKey, + } + data, err := json.Marshal(payload) + if err != nil { + return "", fmt.Errorf("bridge: marshal reconstruct reply ctx: %w", err) + } + return string(data), nil +} + +func bridgeTransportChatID(sessionKey string) (string, error) { + parts := strings.SplitN(sessionKey, ":", 3) + if len(parts) < 2 || parts[1] == "" { + return "", fmt.Errorf("bridge: invalid session key %q", sessionKey) + } + return parts[1], nil +} + func (bp *BridgePlatform) SendCard(ctx context.Context, replyCtx any, card *Card) error { rc, ok := replyCtx.(*bridgeReplyCtx) if !ok { diff --git a/core/bridge_test.go b/core/bridge_test.go index 2711e9d..b223fe9 100644 --- a/core/bridge_test.go +++ b/core/bridge_test.go @@ -252,6 +252,63 @@ func TestBridge_ReplyRouting(t *testing.T) { } } +func TestBridge_ReconstructReplyCtx_RequiresCapability(t *testing.T) { + bs, wsURL := startTestBridge(t, "") + bp := bs.NewPlatform("advisor-gemini") + + conn := dialWS(t, wsURL, nil) + register(t, conn, "bridge", []string{"text"}) + + _, err := bp.ReconstructReplyCtx("bridge:1491487450722341088:relay") + if err == nil || !strings.Contains(err.Error(), "does not support reconstruct_reply") { + t.Fatalf("ReconstructReplyCtx() error = %v, want reconstruct_reply capability error", err) + } +} + +func TestBridge_ReconstructReplyCtx_UsesStructuredPayload(t *testing.T) { + bs, wsURL := startTestBridge(t, "") + bp := bs.NewPlatform("advisor-gemini") + + conn := dialWS(t, wsURL, nil) + register(t, conn, "bridge", []string{"text", "reconstruct_reply"}) + + replyCtx, err := bp.ReconstructReplyCtx("bridge:1491487450722341088:relay") + if err != nil { + t.Fatalf("ReconstructReplyCtx() error = %v", err) + } + + rc, ok := replyCtx.(*bridgeReplyCtx) + if !ok { + t.Fatalf("reply ctx type = %T, want *bridgeReplyCtx", replyCtx) + } + if rc.Platform != "bridge" { + t.Fatalf("Platform = %q, want bridge", rc.Platform) + } + if rc.SessionKey != "bridge:1491487450722341088:relay" { + t.Fatalf("SessionKey = %q, want relay session key", rc.SessionKey) + } + + var payload bridgeReconstructReplyCtxPayload + if err := json.Unmarshal([]byte(rc.ReplyCtx), &payload); err != nil { + t.Fatalf("unmarshal reply_ctx: %v", err) + } + if payload.Kind != bridgeReconstructReplyCtxKind { + t.Fatalf("kind = %q, want %q", payload.Kind, bridgeReconstructReplyCtxKind) + } + if payload.Version != 1 { + t.Fatalf("version = %d, want 1", payload.Version) + } + if payload.SenderProject != "advisor-gemini" { + t.Fatalf("sender_project = %q, want advisor-gemini", payload.SenderProject) + } + if payload.TransportChatID != "1491487450722341088" { + t.Fatalf("transport_chat_id = %q, want 1491487450722341088", payload.TransportChatID) + } + if payload.TransportSessionKey != "bridge:1491487450722341088:relay" { + t.Fatalf("transport_session_key = %q, want relay session key", payload.TransportSessionKey) + } +} + func TestBridge_CardFallback(t *testing.T) { bs, wsURL := startTestBridge(t, "")