mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
feat: webui 消息重放双重防护(client_msg_id 去重 + agent 内容级去重)
回应群聊 08-19 消息轰炸诊断(GUI SSE 重连导致消息重放):
1. webui 层 client_msg_id 单飞去重(与 GUI c29abe9 配套):
- /api/v1/chat 解析 client_msg_id, 同 ID 重放等待首次结果直接复用
- 响应带 deduplicated=true 标记; 无 ID 旧客户端完全兼容
- FIFO 缓存上限 256 条防泄漏
2. agent 核心层内容级短窗口去重(兜底无 ID 客户端):
- isDuplicateInput: source+content 为 key, 10s 窗口内重复丢弃
- 持续轰炸时刷新时间戳保持拦截; 过期项自动清理
测试: webui 去重三场景 + agent 核心去重行为验证, 全项目 go test 通过
This commit is contained in:
@ -841,3 +841,141 @@ func TestHandleCompletionsEndToEnd(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ===== client_msg_id 去重测试(防 GUI 断线重连消息重放)=====
|
||||
|
||||
func TestHandleChatClientMsgIDDedup(t *testing.T) {
|
||||
iom := agentIO.NewIOManager()
|
||||
|
||||
memDB, err := memory.NewGraphDB(t.TempDir() + "/graph.db")
|
||||
if err != nil {
|
||||
t.Fatalf("NewGraphDB: %v", err)
|
||||
}
|
||||
defer memDB.Close()
|
||||
|
||||
pm := agentAPI.NewProviderManager()
|
||||
pm.Register("echo", &echoProvider{name: "echo"})
|
||||
|
||||
agent := agentCore.New(agentCore.AgentConfig{
|
||||
ID: "test",
|
||||
SystemPrompt: "你是测试助手",
|
||||
Provider: &echoProvider{name: "echo"},
|
||||
ProviderManager: pm,
|
||||
IO: iom,
|
||||
Memory: memDB,
|
||||
})
|
||||
agent.Start()
|
||||
defer agent.Stop()
|
||||
|
||||
sup := supervisor.New(&types.Config{
|
||||
Daemon: types.DaemonConfig{
|
||||
CheckInterval: time.Minute,
|
||||
HeartbeatInterval: 30 * time.Second,
|
||||
},
|
||||
})
|
||||
sup.Start()
|
||||
defer sup.Shutdown()
|
||||
|
||||
s := testSDK(sdk.SDKConfig{
|
||||
Supervisor: supervisor.NewSDKAdapter(sup),
|
||||
IOManager: iom,
|
||||
Config: sdk.NewConfig(&types.Config{}),
|
||||
})
|
||||
h := NewHandler(s)
|
||||
|
||||
t.Run("same_client_msg_id_replay_returns_cached_response", func(t *testing.T) {
|
||||
body := `{"message":"你好","client_msg_id":"msg-abc-123"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/chat", strings.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
h.handleChat(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("first request: expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var first map[string]interface{}
|
||||
json.NewDecoder(w.Body).Decode(&first)
|
||||
if first["response"] != "echo: 你好" {
|
||||
t.Fatalf("expected echo response, got %v", first["response"])
|
||||
}
|
||||
|
||||
// 同 ID 重放:应直接复用首次结果,不重复注入 agent
|
||||
req2 := httptest.NewRequest(http.MethodPost, "/api/v1/chat", strings.NewReader(body))
|
||||
w2 := httptest.NewRecorder()
|
||||
h.handleChat(w2, req2)
|
||||
if w2.Code != http.StatusOK {
|
||||
t.Fatalf("replay: expected 200, got %d: %s", w2.Code, w2.Body.String())
|
||||
}
|
||||
var second map[string]interface{}
|
||||
json.NewDecoder(w2.Body).Decode(&second)
|
||||
if second["response"] != "echo: 你好" {
|
||||
t.Fatalf("replay expected same response, got %v", second["response"])
|
||||
}
|
||||
if second["deduplicated"] != true {
|
||||
t.Fatalf("replay expected deduplicated=true, got %v", second["deduplicated"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("different_client_msg_id_processed_normally", func(t *testing.T) {
|
||||
body := `{"message":"第二条","client_msg_id":"msg-def-456"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/chat", strings.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
h.handleChat(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
json.NewDecoder(w.Body).Decode(&resp)
|
||||
if resp["deduplicated"] == true {
|
||||
t.Fatal("new msg id should not be deduplicated")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no_client_msg_id_backward_compatible", func(t *testing.T) {
|
||||
body := `{"message":"旧客户端消息"}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/chat", strings.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
h.handleChat(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ===== agent 核心层内容级去重测试 =====
|
||||
|
||||
func TestAgentDuplicateInputDedup(t *testing.T) {
|
||||
iom := agentIO.NewIOManager()
|
||||
|
||||
memDB, err := memory.NewGraphDB(t.TempDir() + "/graph.db")
|
||||
if err != nil {
|
||||
t.Fatalf("NewGraphDB: %v", err)
|
||||
}
|
||||
defer memDB.Close()
|
||||
|
||||
pm := agentAPI.NewProviderManager()
|
||||
pm.Register("echo", &echoProvider{name: "echo"})
|
||||
|
||||
agent := agentCore.New(agentCore.AgentConfig{
|
||||
ID: "test",
|
||||
SystemPrompt: "你是测试助手",
|
||||
Provider: &echoProvider{name: "echo"},
|
||||
ProviderManager: pm,
|
||||
IO: iom,
|
||||
Memory: memDB,
|
||||
})
|
||||
agent.Start()
|
||||
defer agent.Stop()
|
||||
|
||||
// 直接验证 isDuplicateInput 行为
|
||||
if agent.IsDuplicateInput("webui", "重复消息") {
|
||||
t.Fatal("first input should not be duplicate")
|
||||
}
|
||||
if !agent.IsDuplicateInput("webui", "重复消息") {
|
||||
t.Fatal("immediate same-content same-source should be duplicate")
|
||||
}
|
||||
if agent.IsDuplicateInput("webui", "不同消息") {
|
||||
t.Fatal("different content should not be duplicate")
|
||||
}
|
||||
if agent.IsDuplicateInput("qq", "重复消息") {
|
||||
t.Fatal("different source should not be duplicate")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user