feat(channels): 适配器日志接 WS 实时推送(落库即秒推, channel_persisted)
后端: persistIsolated 由 _ = full(不广播)改为 notifyPersisted(full); BroadcastPersisted 按
sessionId 分流——main/web:<uuid> → message_persisted(Web聊天同步), 隔离 sess-xxx → channel_persisted
(适配器日志专用, 永不写主会话 web, 红线). payload data:{message:historyMessage}(strip thinking).
前端: ws.ts 新增 channel_persisted case→parseAdminChannelMessage→pushChannelEvent; wsParse.ts 纯函数;
useAppStore 新增 channelEvents(最新在前,按id去重,cap200,不持久化); channelLogList 订阅过滤
(单adapter按adapterId/总览取adapterId非空)merge进列表(仅追加、id去重、不整页重载),保留拉历史+10s兜底.
Docs/api-client.md §3.4 补 channel_persisted 契约.
验收: 后端 build/vet/test 全绿+windows/darwin交叉编译绿; 前端 npm run build 99模块绿.
注: 偏离=web:<uuid> 归 message_persisted(保Web副会话同步), 隔离永不带web前缀红线仍守.
This commit is contained in:
1 parent
9e7b4ebaac
commit
3386679d45
10 files changed
+180
-23
No files matched your search
@@ -24,6 +24,7 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/iot"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/persona/loader"
|
||||
personareg "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/registry"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/knowledge"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
|
||||
@@ -935,6 +936,54 @@ func TestServerBroadcastCapacityAwaken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerBroadcastPersistedSplit verifies BroadcastPersisted routes the event
|
||||
// type by session: the main web session and its web sub-sessions broadcast
|
||||
// `message_persisted` (web chat realtime sync), while an isolated channel adapter
|
||||
// session (sess-xxx) broadcasts `channel_persisted` (IM adapter log page) so it is
|
||||
// naturally ignored by the web chat handler.
|
||||
func TestServerBroadcastPersistedSplit(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := newEnv(t, nil, false)
|
||||
c := newFakeConn(env.srv.hub, 32)
|
||||
env.srv.hub.register(c)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
sessionID string
|
||||
wantType string
|
||||
}{
|
||||
{name: "main web session", sessionID: history.MainSessionID, wantType: "message_persisted"},
|
||||
{name: "web sub session", sessionID: history.MainSessionID + ":abc123", wantType: "message_persisted"},
|
||||
{name: "isolated channel session", sessionID: "sess-g", wantType: "channel_persisted"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
env.srv.BroadcastPersisted(history.Message{
|
||||
ID: "m-" + tc.sessionID, SessionID: tc.sessionID, Role: history.RoleUser,
|
||||
Content: "hello", Platform: "telegram", AdapterID: "telegram:abc", CreatedAt: 1000,
|
||||
})
|
||||
ev := readFakeFrame(t, c)
|
||||
if ev.Type != tc.wantType {
|
||||
t.Fatalf("frame type = %q, want %q (frame=%+v)", ev.Type, tc.wantType, ev)
|
||||
}
|
||||
if ev.SessionID != tc.sessionID {
|
||||
t.Fatalf("frame sessionId = %q, want %q", ev.SessionID, tc.sessionID)
|
||||
}
|
||||
data, ok := ev.Data.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("frame data = %+v, want map", ev.Data)
|
||||
}
|
||||
msg, ok := data["message"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("frame data.message = %+v, want map", data["message"])
|
||||
}
|
||||
if msg["content"] != "hello" || msg["adapterId"] != "telegram:abc" {
|
||||
t.Fatalf("frame message = %+v, want content/hello + adapterId/telegram:abc", msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHubStreamChatIdle(t *testing.T) {
|
||||
t.Parallel()
|
||||
hb := newTestHub(nil, nil) // nil runner
|
||||
|
||||
Reference in new issue
Block a user