Skip to content

Commit 17b8300

Browse files
committed
fix(chat): 修复跨端会话同步和搜索合并
1 parent 73133a4 commit 17b8300

28 files changed

Lines changed: 1157 additions & 448 deletions

crates/agent-gateway/internal/proto/v1/gateway.pb.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/agent-gateway/internal/proto/v1/gateway_grpc.pb.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/agent-gateway/internal/server/websocket.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,8 @@ func (c *websocketConnection) dispatch(req websocketRequest) {
323323
c.handleFsListDirs(req)
324324
case "history.list":
325325
c.handleHistoryList(req)
326+
case "history.shared_list":
327+
c.handleHistorySharedList(req)
326328
case "history.get":
327329
c.handleHistoryGet(req)
328330
case "history.rename":
@@ -546,6 +548,79 @@ func (c *websocketConnection) handleHistoryList(req websocketRequest) {
546548
})
547549
}
548550

551+
func (c *websocketConnection) handleHistorySharedList(req websocketRequest) {
552+
type payload struct {
553+
Page int `json:"page"`
554+
PageSize int `json:"page_size"`
555+
}
556+
557+
var body payload
558+
if err := decodeWebSocketPayload(req.Payload, &body); err != nil {
559+
_ = c.writeError(req.ID, "invalid history.shared_list payload")
560+
return
561+
}
562+
page := body.Page
563+
if page <= 0 {
564+
_ = c.writeError(req.ID, "history.shared_list page must be greater than 0")
565+
return
566+
}
567+
pageSize := body.PageSize
568+
if pageSize <= 0 {
569+
_ = c.writeError(req.ID, "history.shared_list page_size must be greater than 0")
570+
return
571+
} else if pageSize > maxHistoryListLimit {
572+
pageSize = maxHistoryListLimit
573+
}
574+
575+
argsJSON, err := json.Marshal(map[string]any{
576+
"page": page,
577+
"page_size": pageSize,
578+
})
579+
if err != nil {
580+
_ = c.writeError(req.ID, "invalid history.shared_list payload")
581+
return
582+
}
583+
584+
response, err := c.awaitAgentResponse(req.ID, &gatewayv1.GatewayEnvelope{
585+
RequestId: req.ID,
586+
Timestamp: time.Now().Unix(),
587+
Payload: &gatewayv1.GatewayEnvelope_MemoryManage{
588+
MemoryManage: &gatewayv1.MemoryManageRequest{
589+
Command: "history_shared_list",
590+
ArgsJson: string(argsJSON),
591+
},
592+
},
593+
})
594+
if err != nil {
595+
_ = c.writeError(req.ID, websocketErrorMessage(err))
596+
return
597+
}
598+
if errResp := response.GetError(); errResp != nil {
599+
_ = c.writeError(req.ID, errResp.GetMessage())
600+
return
601+
}
602+
603+
resp := response.GetMemoryManageResp()
604+
if resp == nil {
605+
_ = c.writeError(req.ID, "unexpected agent response")
606+
return
607+
}
608+
609+
var result struct {
610+
Conversations []map[string]any `json:"conversations"`
611+
TotalCount int `json:"total_count"`
612+
}
613+
if err := json.Unmarshal([]byte(resp.GetResultJson()), &result); err != nil {
614+
_ = c.writeError(req.ID, "invalid history.shared_list response")
615+
return
616+
}
617+
618+
_ = c.writeResponse(req.ID, map[string]any{
619+
"conversations": result.Conversations,
620+
"total_count": result.TotalCount,
621+
})
622+
}
623+
549624
func (c *websocketConnection) handleHistoryGet(req websocketRequest) {
550625
type payload struct {
551626
ConversationID string `json:"conversation_id"`

crates/agent-gateway/internal/session/manager.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,9 @@ func (m *Manager) broadcastChatEvent(requestID string, event *gatewayv1.ChatEven
798798
}
799799

800800
requestID = strings.TrimSpace(requestID)
801+
conversationID := strings.TrimSpace(event.GetConversationId())
801802
now := time.Now()
803+
sessionEpoch := m.currentSessionEpoch()
802804

803805
m.chatMu.Lock()
804806
m.pruneExpiredChatRunsLocked(now)
@@ -807,10 +809,24 @@ func (m *Manager) broadcastChatEvent(requestID string, event *gatewayv1.ChatEven
807809
Event: event,
808810
}
809811
var runSubscribers []*chatRunSubscriber
810-
if run := m.chatRuns[requestID]; run != nil {
812+
run := m.chatRuns[requestID]
813+
if run == nil && requestID != "" {
814+
run = &chatRun{
815+
requestID: requestID,
816+
conversationID: conversationID,
817+
sessionEpoch: sessionEpoch,
818+
updatedAt: now,
819+
subscribers: make(map[int]*chatRunSubscriber),
820+
}
821+
m.chatRuns[requestID] = run
822+
if conversationID != "" {
823+
m.chatRunByConversation[conversationID] = requestID
824+
}
825+
}
826+
if run != nil {
811827
run.nextSeq += 1
812828
run.updatedAt = now
813-
if conversationID := strings.TrimSpace(event.GetConversationId()); conversationID != "" {
829+
if conversationID != "" {
814830
if run.conversationID != "" && run.conversationID != conversationID {
815831
if m.chatRunByConversation[run.conversationID] == requestID {
816832
delete(m.chatRunByConversation, run.conversationID)

crates/agent-gateway/test/helpers/load-web-module.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from "node:fs";
22
import path from "node:path";
33
import vm from "node:vm";
44
import { createRequire } from "node:module";
5-
import { pathToFileURL } from "node:url";
5+
import { fileURLToPath, pathToFileURL } from "node:url";
66

77
const DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".css"];
88

@@ -106,7 +106,7 @@ function resolveAsFileOrDirectory(candidate) {
106106
export function createWebModuleLoader(options = {}) {
107107
const rootDir = options.rootDir
108108
? path.resolve(options.rootDir)
109-
: path.resolve(new URL("../../web", import.meta.url).pathname);
109+
: path.resolve(fileURLToPath(new URL("../../web", import.meta.url)));
110110
const requireFromRoot = createRequire(path.join(rootDir, "package.json"));
111111
const ts = requireFromRoot("typescript");
112112
const cache = new Map();

crates/agent-gateway/test/session/manager_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,49 @@ func TestStartChatRunWithClientRequestReusesExistingRun(t *testing.T) {
252252
}
253253
}
254254

255+
func TestDesktopBroadcastChatEventCreatesAttachableRun(t *testing.T) {
256+
t.Parallel()
257+
258+
sm := newTestSessionManager()
259+
sm.SetSession(session.NewAgentSession(sm.LatestAuthSnapshot()))
260+
261+
sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
262+
RequestId: "conversation-live-conversation-1",
263+
Payload: &gatewayv1.AgentEnvelope_ChatEvent{
264+
ChatEvent: &gatewayv1.ChatEvent{
265+
Type: gatewayv1.ChatEvent_TOKEN,
266+
ConversationId: "conversation-1",
267+
Data: `{"text":"hello"}`,
268+
},
269+
},
270+
})
271+
272+
ch, done, cleanup, snapshot, err := sm.SubscribeChatRun("", "conversation-1", 0)
273+
if err != nil {
274+
t.Fatalf("SubscribeChatRun: %v", err)
275+
}
276+
defer cleanup()
277+
assertDoneOpen(t, done)
278+
if snapshot.RequestID != "conversation-live-conversation-1" {
279+
t.Fatalf("snapshot request id = %q, want conversation-live-conversation-1", snapshot.RequestID)
280+
}
281+
282+
select {
283+
case event := <-ch:
284+
if event.Seq != 1 {
285+
t.Fatalf("event seq = %d, want 1", event.Seq)
286+
}
287+
if event.Event.GetType() != gatewayv1.ChatEvent_TOKEN {
288+
t.Fatalf("event type = %v, want TOKEN", event.Event.GetType())
289+
}
290+
if event.Event.GetConversationId() != "conversation-1" {
291+
t.Fatalf("conversation id = %q, want conversation-1", event.Event.GetConversationId())
292+
}
293+
case <-time.After(time.Second):
294+
t.Fatalf("timed out waiting for replayed desktop chat event")
295+
}
296+
}
297+
255298
func TestCompletedHistoryUpsertDoesNotPreemptTerminalChatEvent(t *testing.T) {
256299
t.Parallel()
257300

crates/agent-gateway/test/websocket/chat_bridge_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,49 @@ func TestWebSocketForwardsHistorySettingsAndFsRPCs(t *testing.T) {
896896
t.Fatalf("history conversation share field = %#v", historyConversation)
897897
}
898898

899+
sendEnvelope(t, conn, "history-shared-1", "history.shared_list", map[string]any{
900+
"page": 1,
901+
"page_size": 50,
902+
})
903+
sharedHistoryOutbound := readOutboundEnvelope(t, agentSession)
904+
sharedHistoryReq := sharedHistoryOutbound.GetMemoryManage()
905+
if sharedHistoryReq == nil {
906+
t.Fatalf("shared history outbound payload = %T, want MemoryManageRequest", sharedHistoryOutbound.GetPayload())
907+
}
908+
if sharedHistoryReq.GetCommand() != "history_shared_list" {
909+
t.Fatalf("shared history list request = %#v", sharedHistoryReq)
910+
}
911+
var sharedHistoryArgs map[string]any
912+
if err := json.Unmarshal([]byte(sharedHistoryReq.GetArgsJson()), &sharedHistoryArgs); err != nil {
913+
t.Fatalf("decode shared history args: %v", err)
914+
}
915+
if sharedHistoryArgs["page"] != float64(1) || sharedHistoryArgs["page_size"] != float64(50) {
916+
t.Fatalf("shared history args = %#v", sharedHistoryArgs)
917+
}
918+
sm.DispatchFromAgent(&gatewayv1.AgentEnvelope{
919+
RequestId: sharedHistoryOutbound.GetRequestId(),
920+
Timestamp: time.Now().Unix(),
921+
Payload: &gatewayv1.AgentEnvelope_MemoryManageResp{
922+
MemoryManageResp: &gatewayv1.MemoryManageResponse{
923+
ResultJson: `{"total_count":1,"conversations":[{"id":"conversation-1","title":"Gateway test","created_at":10,"updated_at":11,"message_count":3,"provider_id":"codex-provider","model":"gpt-test","session_id":"session-1","cwd":"/workspace","is_shared":true}]}`,
924+
},
925+
},
926+
})
927+
sharedHistoryResponse := receiveEnvelope(t, conn)
928+
if sharedHistoryResponse.ID != "history-shared-1" || sharedHistoryResponse.Type != "response" {
929+
t.Fatalf("shared history response = %#v", sharedHistoryResponse)
930+
}
931+
var sharedHistoryPayload map[string]any
932+
if err := json.Unmarshal(sharedHistoryResponse.Payload, &sharedHistoryPayload); err != nil {
933+
t.Fatalf("decode shared history response: %v", err)
934+
}
935+
if sharedHistoryPayload["total_count"] != float64(1) {
936+
t.Fatalf("shared history payload = %#v", sharedHistoryPayload)
937+
}
938+
if _, ok := sharedHistoryPayload["running_conversation_ids"]; ok {
939+
t.Fatalf("shared history response should not include running ids: %#v", sharedHistoryPayload)
940+
}
941+
899942
sendEnvelope(t, conn, "history-get-1", "history.get", map[string]any{
900943
"conversation_id": "conversation-1",
901944
"max_messages": 360,

crates/agent-gateway/test/webui/gateway-socket-client.test.mjs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,43 @@ test("GatewayWebSocketClient sends skill manage payloads", async () => {
581581
resetGatewayWebSocketClient();
582582
});
583583

584+
test("GatewayWebSocketClient sends history list requests", async () => {
585+
installBrowser();
586+
const loader = createWebModuleLoader();
587+
const { getGatewayWebSocketClient, resetGatewayWebSocketClient } = loader.loadModule("src/lib/gatewaySocket.ts");
588+
resetGatewayWebSocketClient();
589+
590+
const client = getGatewayWebSocketClient("token");
591+
const listPromise = client.listHistory(2, 50);
592+
const socket = await connectAndAuth();
593+
await waitFor(() => socket.sent.length >= 2, "history list envelope");
594+
assert.equal(socket.sent[1].type, "history.list");
595+
assert.deepEqual(socket.sent[1].payload, { page: 2, page_size: 50 });
596+
socket.receive({
597+
id: socket.sent[1].id,
598+
type: "response",
599+
payload: { conversations: [], total_count: 0, running_conversation_ids: [] },
600+
});
601+
assert.deepEqual(await listPromise, {
602+
conversations: [],
603+
total_count: 0,
604+
running_conversation_ids: [],
605+
});
606+
607+
const sharedListPromise = client.listSharedHistory(1, 25);
608+
await waitFor(() => socket.sent.length >= 3, "shared history list envelope");
609+
assert.equal(socket.sent[2].type, "history.shared_list");
610+
assert.deepEqual(socket.sent[2].payload, { page: 1, page_size: 25 });
611+
socket.receive({
612+
id: socket.sent[2].id,
613+
type: "response",
614+
payload: { conversations: [], total_count: 0 },
615+
});
616+
assert.deepEqual(await sharedListPromise, { conversations: [], total_count: 0 });
617+
618+
resetGatewayWebSocketClient();
619+
});
620+
584621
test("GatewayWebSocketClient sends history share requests", async () => {
585622
installBrowser();
586623
const loader = createWebModuleLoader();

0 commit comments

Comments
 (0)