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
@@ -168,6 +168,77 @@ func TestHistoryListMessagesPlatformFilter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// handleChannelMessagesGranularity exercises the admin adapter log endpoint at each
|
||||
// granularity: a single adapter config (?adapter=), a whole platform (?platform=),
|
||||
// and the full IM adapter overview (neither). It asserts adapterId is carried on the
|
||||
// DTO so the panel can label the source adapter config.
|
||||
func TestHistoryChannelMessagesGranularity(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ts, jwt, st := newHistoryEnv(t)
|
||||
ctx := context.Background()
|
||||
for _, m := range []history.Message{
|
||||
{SessionID: history.MainSessionID, Role: history.RoleUser, Content: "kook-owner", Platform: "kook", AdapterID: "kook:3bef", CreatedAt: 1000},
|
||||
{SessionID: "sess-g", Role: history.RoleUser, Content: "kook-group", Platform: "kook", AdapterID: "kook:3bef", SenderName: "张三", CreatedAt: 2000},
|
||||
{SessionID: "sess-t", Role: history.RoleUser, Content: "tg-group", Platform: "telegram", AdapterID: "telegram:conf0", CreatedAt: 3000},
|
||||
{SessionID: history.MainSessionID, Role: history.RoleUser, Content: "web-msg", Platform: history.PlatformWeb, CreatedAt: 4000},
|
||||
} {
|
||||
if err := st.Append(ctx, m); err != nil {
|
||||
t.Fatalf("seed %q: %v", m.Content, err)
|
||||
}
|
||||
}
|
||||
tok := signAny(t, jwt, "acct", auth.RoleOwner)
|
||||
|
||||
// ?adapter=kook:3bef → only that adapter config, both the owner mirror and the
|
||||
// group message, newest first, and the DTO carries adapterId.
|
||||
resp, body := doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/channels/messages?adapter=kook:3bef", nil, tok)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("adapter status = %d, want 200 (body=%s)", resp.StatusCode, body)
|
||||
}
|
||||
var byAdapter []historyMessage
|
||||
decodeDataSlice(t, body, &byAdapter)
|
||||
if len(byAdapter) != 2 {
|
||||
t.Fatalf("adapter page = %d, want 2 (%+v)", len(byAdapter), byAdapter)
|
||||
}
|
||||
if byAdapter[0].Content != "kook-group" || byAdapter[0].AdapterID != "kook:3bef" {
|
||||
t.Fatalf("adapter[0] = %+v, want kook-group with adapterId", byAdapter[0])
|
||||
}
|
||||
if byAdapter[1].Content != "kook-owner" || byAdapter[1].SessionID != history.MainSessionID {
|
||||
t.Fatalf("adapter[1] = %+v, want kook-owner on main session", byAdapter[1])
|
||||
}
|
||||
|
||||
// ?platform=kook → the platform lane across isolated sessions (owner-private
|
||||
// mirror excluded, matching handleChannelPlatformLog semantics).
|
||||
resp, body = doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/channels/messages?platform=kook", nil, tok)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("platform status = %d, want 200 (body=%s)", resp.StatusCode, body)
|
||||
}
|
||||
var byPlatform []historyMessage
|
||||
decodeDataSlice(t, body, &byPlatform)
|
||||
if len(byPlatform) != 1 || byPlatform[0].Content != "kook-group" {
|
||||
t.Fatalf("platform page = %+v, want [kook-group]", byPlatform)
|
||||
}
|
||||
|
||||
// No filter → the full IM adapter overview (adapter_id != ''), web excluded.
|
||||
resp, body = doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/channels/messages", nil, tok)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("all status = %d, want 200 (body=%s)", resp.StatusCode, body)
|
||||
}
|
||||
var all []historyMessage
|
||||
decodeDataSlice(t, body, &all)
|
||||
if len(all) != 3 {
|
||||
t.Fatalf("all page = %d, want 3 (%+v)", len(all), all)
|
||||
}
|
||||
if all[0].Content != "tg-group" || all[1].Content != "kook-group" || all[2].Content != "kook-owner" {
|
||||
t.Fatalf("all order = %+v, want [tg-group kook-group kook-owner]", all)
|
||||
}
|
||||
|
||||
// Invalid before → 400.
|
||||
resp, body = doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/channels/messages?before=abc", nil, tok)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("invalid before status = %d, want 400 (body=%s)", resp.StatusCode, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistoryDeleteSubSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ts, jwt, st := newHistoryEnv(t)
|
||||
|
||||
Reference in new issue
Block a user