feat(channels): 管理面板消息日志——chat_messages 加 adapter_id + 配置级/平台/总览三粒度端点
后端: - 迁移 0007:chat_messages 加 adapter_id 列 + 索引 - history.Message 加 AdapterID;persistToMain/persistIsolated 传适配器名(a.Name()) - channelbridge: 4 个持久化点 + registerSilenced 传 a;light.go registerSilenced 签名加 a - Store 新增 ListAdapterLog(?adapter=, 不排除主会话)/ListAllLog(adapter_id!='', 总览);ListPlatformLog 保留 - 新端点 GET /api/v1/admin/channels/messages:?adapter= 配置级 / ?platform= 平台 / 无参=总览,historyMessage 加 adapterId - 测试:ListAdapterLog/ListAllLog 过滤用例 + 端点粒度用例 + 桥接持久化断言 前端:渠道管理每配置(账号)「消息日志」按钮 + 页顶「适配器总览日志」入口 + ChannelLogPanel 弹窗(总览/配置级),Miuix 风格,build 绿。
This commit is contained in:
1 parent
c1bd740f7c
commit
2e044d1d19
15 files changed
+560
-41
No files matched your search
@@ -423,3 +423,116 @@ func TestSenderNameRoundTripOnListRecentAndPlatformLog(t *testing.T) {
|
||||
t.Fatalf("web platform log = %+v, err=%v; want empty", log, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapterLogFiltersByAdapter(t *testing.T) {
|
||||
t.Parallel()
|
||||
st := newStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Seed across two adapters, an empty-adapter web message, and a main-session
|
||||
// owner-private mirror that still carries the adapter (design/16 §4.4).
|
||||
for _, m := range []Message{
|
||||
{SessionID: MainSessionID, Role: RoleUser, Content: "kook-owner", Platform: "kook", AdapterID: "kook:3bef", CreatedAt: 1000},
|
||||
{SessionID: "sess-g1", Role: RoleUser, Content: "kook-group", Platform: "kook", AdapterID: "kook:3bef", SenderName: "张三", CreatedAt: 2000},
|
||||
{SessionID: "sess-g2", Role: RoleUser, Content: "tg-group", Platform: "telegram", AdapterID: "telegram:conf0", SenderName: "李四", CreatedAt: 3000},
|
||||
{SessionID: MainSessionID, Role: RoleUser, Content: "web-msg", Platform: PlatformWeb, CreatedAt: 4000},
|
||||
} {
|
||||
if err := st.Append(ctx, m); err != nil {
|
||||
t.Fatalf("seed %q: %v", m.Content, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Per-adapter log includes BOTH the owner-private main mirror and the group
|
||||
// message, newest first.
|
||||
kook, err := st.ListAdapterLog(ctx, "kook:3bef", 0, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("adapter log kook:3bef: %v", err)
|
||||
}
|
||||
if len(kook) != 2 {
|
||||
t.Fatalf("kook adapter log = %d, want 2 (%+v)", len(kook), kook)
|
||||
}
|
||||
if kook[0].Content != "kook-group" || kook[0].SessionID != "sess-g1" {
|
||||
t.Fatalf("kook[0] = %+v, want newest kook-group", kook[0])
|
||||
}
|
||||
if kook[1].Content != "kook-owner" || kook[1].SessionID != MainSessionID {
|
||||
t.Fatalf("kook[1] = %+v, want owner mirror on main session", kook[1])
|
||||
}
|
||||
|
||||
// A different adapter yields only its own lane.
|
||||
tg, err := st.ListAdapterLog(ctx, "telegram:conf0", 0, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("adapter log telegram:conf0: %v", err)
|
||||
}
|
||||
if len(tg) != 1 || tg[0].Content != "tg-group" {
|
||||
t.Fatalf("telegram adapter log = %+v, want [tg-group]", tg)
|
||||
}
|
||||
|
||||
// An unknown / empty adapter yields a non-nil empty slice.
|
||||
none, err := st.ListAdapterLog(ctx, "kook:unknown", 0, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("adapter log unknown: %v", err)
|
||||
}
|
||||
if none == nil || len(none) != 0 {
|
||||
t.Fatalf("unknown adapter log = %#v, want non-nil empty slice", none)
|
||||
}
|
||||
empty, err := st.ListAdapterLog(ctx, "", 0, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("adapter log empty: %v", err)
|
||||
}
|
||||
if empty == nil || len(empty) != 0 {
|
||||
t.Fatalf("empty adapter log = %#v, want non-nil empty slice", empty)
|
||||
}
|
||||
|
||||
// The before cursor pages backward through the adapter's stream.
|
||||
older, err := st.ListAdapterLog(ctx, "kook:3bef", 2000, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("adapter log before: %v", err)
|
||||
}
|
||||
if len(older) != 1 || older[0].Content != "kook-owner" {
|
||||
t.Fatalf("kook before page = %+v, want [kook-owner]", older)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllLogSurfacesOnlyAdapterMessages(t *testing.T) {
|
||||
t.Parallel()
|
||||
st := newStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
for _, m := range []Message{
|
||||
{SessionID: MainSessionID, Role: RoleUser, Content: "kook-owner", Platform: "kook", AdapterID: "kook:3bef", CreatedAt: 1000},
|
||||
{SessionID: MainSessionID, Role: RoleUser, Content: "web-msg", Platform: PlatformWeb, CreatedAt: 2000},
|
||||
{SessionID: "sess-g1", Role: RoleUser, Content: "tg-group", Platform: "telegram", AdapterID: "telegram:conf0", CreatedAt: 3000},
|
||||
} {
|
||||
if err := st.Append(ctx, m); err != nil {
|
||||
t.Fatalf("seed %q: %v", m.Content, err)
|
||||
}
|
||||
}
|
||||
|
||||
// The overview only surfaces IM adapter sources; the web message is excluded.
|
||||
all, err := st.ListAllLog(ctx, 0, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("all log: %v", err)
|
||||
}
|
||||
if len(all) != 2 {
|
||||
t.Fatalf("all log = %d, want 2 (%+v)", len(all), all)
|
||||
}
|
||||
if all[0].Content != "tg-group" || all[1].Content != "kook-owner" {
|
||||
t.Fatalf("all log order = %+v, want [tg-group kook-owner]", all)
|
||||
}
|
||||
// The web message never leaks into the adapter overview.
|
||||
for _, m := range all {
|
||||
if m.Content == "web-msg" {
|
||||
t.Fatalf("web message leaked into adapter overview: %+v", m)
|
||||
}
|
||||
}
|
||||
|
||||
// An empty store yields a non-nil empty slice.
|
||||
st2 := newStore(t)
|
||||
e, err := st2.ListAllLog(ctx, 0, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("empty all log: %v", err)
|
||||
}
|
||||
if e == nil || len(e) != 0 {
|
||||
t.Fatalf("empty all log = %#v, want non-nil empty slice", e)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user