feat: IM平台列 + deviceID标识 + 会话持久化完善
- agent-96: chat_messages 加 platform 列(迁移0004) + IM主人私聊落主会话web(带platform) + 前端按平台过滤契约 + message_persisted 带 platform(复用 BroadcastPersisted) - agent-97: chat_messages 加 device_id 列(迁移0005) + history.Message.DeviceID + Web发消息带deviceId(localStorage getClientId) + IM persistToMain 带 deviceID + 前端气泡展示 SourceLabel + 修主会话基础2个bug(fn scanMessage语法/DDL缺列) - design/16 §4.4(平台) + §4.5(deviceID) 已入档 - 实机: POST /chat 带 deviceId 读回 user/assistant 均带 deviceId+platform; 后端 build/vet/test 绿 + 前端 build 绿 - 红线: 身份靠 actor 元数据; 禁止 shell 读 .env(JWT冒烟由主会话跑)
This commit is contained in:
1 parent
1bf60c1ae4
commit
37e98ec584
26 files changed
+828
-116
No files matched your search
@@ -51,7 +51,7 @@ func TestAppendAndListNewestFirst(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
msgs, err := st.List(ctx, MainSessionID, 0, 50)
|
||||
msgs, err := st.List(ctx, MainSessionID, 0, 50, "")
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func TestAppendAndListNewestFirst(t *testing.T) {
|
||||
if err := st.Append(ctx, empty); err != nil {
|
||||
t.Fatalf("append no ts: %v", err)
|
||||
}
|
||||
msgs, err = st.List(ctx, MainSessionID, 0, 50)
|
||||
msgs, err = st.List(ctx, MainSessionID, 0, 50, "")
|
||||
if err != nil {
|
||||
t.Fatalf("list after append: %v", err)
|
||||
}
|
||||
@@ -96,7 +96,7 @@ func TestAppendFullReturnsID(t *testing.T) {
|
||||
}
|
||||
|
||||
// The message is actually persisted under the returned id.
|
||||
msgs, err := st.List(ctx, MainSessionID, 0, 50)
|
||||
msgs, err := st.List(ctx, MainSessionID, 0, 50, "")
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
@@ -114,6 +114,133 @@ func TestAppendFullReturnsID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendFullListPlatformRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
st := newStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// An IM owner-private message carries its channel platform through the round
|
||||
// trip: AppendFull → List.
|
||||
full, err := st.AppendFull(ctx, Message{SessionID: MainSessionID, Role: RoleUser, Content: "从微信来", Platform: "weixin-ilink"})
|
||||
if err != nil {
|
||||
t.Fatalf("append full: %v", err)
|
||||
}
|
||||
if full.Platform != "weixin-ilink" {
|
||||
t.Fatalf("AppendFull platform = %q, want weixin-ilink", full.Platform)
|
||||
}
|
||||
msgs, err := st.List(ctx, MainSessionID, 0, 50, "")
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
if len(msgs) != 1 || msgs[0].Platform != "weixin-ilink" || msgs[0].Content != "从微信来" {
|
||||
t.Fatalf("list round trip = %+v, want platform weixin-ilink", msgs)
|
||||
}
|
||||
|
||||
// A message without an explicit platform defaults to the web lane.
|
||||
web, err := st.AppendFull(ctx, Message{SessionID: MainSessionID, Role: RoleAssistant, Content: "ok"})
|
||||
if err != nil {
|
||||
t.Fatalf("append web default: %v", err)
|
||||
}
|
||||
if web.Platform != PlatformWeb {
|
||||
t.Fatalf("default platform = %q, want web", web.Platform)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendFullListDeviceIDRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
st := newStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// A Web message tagged with a browser deviceID survives the AppendFull → List
|
||||
// round trip (design/16 §4.5).
|
||||
full, err := st.AppendFull(ctx, Message{SessionID: MainSessionID, Role: RoleUser, Content: "来自浏览器", DeviceID: "web:chrome:a1b2c3"})
|
||||
if err != nil {
|
||||
t.Fatalf("append full: %v", err)
|
||||
}
|
||||
if full.DeviceID != "web:chrome:a1b2c3" {
|
||||
t.Fatalf("AppendFull DeviceID = %q, want web:chrome:a1b2c3", full.DeviceID)
|
||||
}
|
||||
msgs, err := st.List(ctx, MainSessionID, 0, 50, "")
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
if len(msgs) != 1 || msgs[0].DeviceID != "web:chrome:a1b2c3" || msgs[0].Content != "来自浏览器" {
|
||||
t.Fatalf("list round trip = %+v, want DeviceID web:chrome:a1b2c3", msgs)
|
||||
}
|
||||
|
||||
// Recent (WS replay / context tail) reads the same device_id column.
|
||||
recent, err := st.Recent(ctx, MainSessionID, 5)
|
||||
if err != nil {
|
||||
t.Fatalf("recent: %v", err)
|
||||
}
|
||||
if len(recent) != 1 || recent[0].DeviceID != "web:chrome:a1b2c3" {
|
||||
t.Fatalf("recent = %+v, want DeviceID web:chrome:a1b2c3", recent)
|
||||
}
|
||||
|
||||
// A message without an explicit deviceID persists the column default ('').
|
||||
om, err := st.AppendFull(ctx, Message{SessionID: MainSessionID, Role: RoleAssistant, Content: "ok"})
|
||||
if err != nil {
|
||||
t.Fatalf("append empty device: %v", err)
|
||||
}
|
||||
if om.DeviceID != "" {
|
||||
t.Fatalf("empty DeviceID = %q, want empty", om.DeviceID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPlatformFilter(t *testing.T) {
|
||||
t.Parallel()
|
||||
st := newStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Seed across the web lane (explicit + default) and one IM lane.
|
||||
for _, m := range []Message{
|
||||
{SessionID: MainSessionID, Role: RoleUser, Content: "web1", Platform: PlatformWeb, CreatedAt: 1000},
|
||||
{SessionID: MainSessionID, Role: RoleUser, Content: "tg", Platform: "telegram", CreatedAt: 2000},
|
||||
{SessionID: MainSessionID, Role: RoleUser, Content: "web2", CreatedAt: 3000},
|
||||
} {
|
||||
if err := st.Append(ctx, m); err != nil {
|
||||
t.Fatalf("seed %q: %v", m.Content, err)
|
||||
}
|
||||
}
|
||||
|
||||
// No platform filter → all three (the empty-platform message defaulted to web).
|
||||
all, err := st.List(ctx, MainSessionID, 0, 50, "")
|
||||
if err != nil {
|
||||
t.Fatalf("list all: %v", err)
|
||||
}
|
||||
if len(all) != 3 {
|
||||
t.Fatalf("all = %d, want 3", len(all))
|
||||
}
|
||||
|
||||
// Filter to the IM lane → only its message.
|
||||
tg, err := st.List(ctx, MainSessionID, 0, 50, "telegram")
|
||||
if err != nil {
|
||||
t.Fatalf("list telegram: %v", err)
|
||||
}
|
||||
if len(tg) != 1 || tg[0].Content != "tg" {
|
||||
t.Fatalf("telegram page = %+v, want [tg]", tg)
|
||||
}
|
||||
|
||||
// Filter to the web lane → web1 + web2 (the one left without an explicit
|
||||
// platform defaulted to web on insert).
|
||||
web, err := st.List(ctx, MainSessionID, 0, 50, PlatformWeb)
|
||||
if err != nil {
|
||||
t.Fatalf("list web: %v", err)
|
||||
}
|
||||
if len(web) != 2 {
|
||||
t.Fatalf("web page = %d, want 2 (%+v)", len(web), web)
|
||||
}
|
||||
|
||||
// The filter composes with the before cursor.
|
||||
older, err := st.List(ctx, MainSessionID, 2000, 50, PlatformWeb)
|
||||
if err != nil {
|
||||
t.Fatalf("list web before: %v", err)
|
||||
}
|
||||
if len(older) != 1 || older[0].Content != "web1" {
|
||||
t.Fatalf("web before page = %+v, want [web1]", older)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPaginationBefore(t *testing.T) {
|
||||
t.Parallel()
|
||||
st := newStore(t)
|
||||
@@ -126,7 +253,7 @@ func TestListPaginationBefore(t *testing.T) {
|
||||
}
|
||||
|
||||
// First page: newest 2.
|
||||
page, err := st.List(ctx, MainSessionID, 0, 2)
|
||||
page, err := st.List(ctx, MainSessionID, 0, 2, "")
|
||||
if err != nil {
|
||||
t.Fatalf("first page: %v", err)
|
||||
}
|
||||
@@ -134,7 +261,7 @@ func TestListPaginationBefore(t *testing.T) {
|
||||
t.Fatalf("first page = %+v, want [6000 5000]", page)
|
||||
}
|
||||
// Cursor on the page's oldest (5000) → older page [4000 3000].
|
||||
next, err := st.List(ctx, MainSessionID, page[1].CreatedAt, 2)
|
||||
next, err := st.List(ctx, MainSessionID, page[1].CreatedAt, 2, "")
|
||||
if err != nil {
|
||||
t.Fatalf("next page: %v", err)
|
||||
}
|
||||
@@ -142,7 +269,7 @@ func TestListPaginationBefore(t *testing.T) {
|
||||
t.Fatalf("next page = %+v, want [4000 3000]", next)
|
||||
}
|
||||
// Past the start → empty.
|
||||
tail, err := st.List(ctx, MainSessionID, 1000, 50)
|
||||
tail, err := st.List(ctx, MainSessionID, 1000, 50, "")
|
||||
if err != nil {
|
||||
t.Fatalf("tail: %v", err)
|
||||
}
|
||||
@@ -220,7 +347,7 @@ func TestDeleteSession(t *testing.T) {
|
||||
if err := st.DeleteSession(ctx, sub); err != nil {
|
||||
t.Fatalf("delete sub: %v", err)
|
||||
}
|
||||
msgs, err := st.List(ctx, sub, 0, 50)
|
||||
msgs, err := st.List(ctx, sub, 0, 50, "")
|
||||
if err != nil {
|
||||
t.Fatalf("list after delete: %v", err)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user