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
@@ -356,6 +356,18 @@ func basePlatform(name string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
// adapterName returns the account-adapter registration name (e.g. "kook:3bef...")
|
||||
// for the adapter that received a message, or "" when a is nil. The single-account
|
||||
// convenience path (process) and a platform with no registered adapter can pass a nil
|
||||
// adapter, so the persist hooks tolerate it and store an empty adapter_id rather than
|
||||
// dereferencing a nil adapter.
|
||||
func adapterName(a adapter.PlatformAdapter) string {
|
||||
if a == nil {
|
||||
return ""
|
||||
}
|
||||
return a.Name()
|
||||
}
|
||||
|
||||
// readLoop runs an adapter's blocking read path until the bridge stops or the
|
||||
// adapter's own context is cancelled (a hot reload). The emit sink is wired to
|
||||
// onMessage so poll-style adapters (Telegram getUpdates, weixin long poll)
|
||||
@@ -485,7 +497,7 @@ func (b *Bridge) processFrom(ctx context.Context, msg adapter.UnifiedMessage, a
|
||||
// owner-private chat and owner group @/high-value replies are unchanged.
|
||||
if !actor.IsOwner() {
|
||||
if ok, reason := b.shouldRespond(ctx, msg, actor); !ok {
|
||||
b.registerSilenced(ctx, msg, actor, sess)
|
||||
b.registerSilenced(ctx, msg, actor, sess, a)
|
||||
b.log.Debug("non-owner message silenced (minimal disturbance)",
|
||||
"platform", msg.Platform, "channel", msg.ChannelID,
|
||||
"session", sess.SessionID, "reason", reason)
|
||||
@@ -519,9 +531,9 @@ func (b *Bridge) processFrom(ctx context.Context, msg adapter.UnifiedMessage, a
|
||||
// are broadcast to the Web client; restricted messages land in their isolated
|
||||
// session (the adapter log page) and are NOT broadcast to the main session.
|
||||
if isOwnerPrivate {
|
||||
b.persistToMain(ctx, history.RoleUser, text, platform, deviceIDFor(msg))
|
||||
b.persistToMain(ctx, history.RoleUser, text, platform, deviceIDFor(msg), adapterName(a))
|
||||
} else {
|
||||
b.persistIsolated(ctx, history.RoleUser, text, deviceIDFor(msg), sess, msg.Platform, senderName)
|
||||
b.persistIsolated(ctx, history.RoleUser, text, deviceIDFor(msg), sess, msg.Platform, senderName, adapterName(a))
|
||||
}
|
||||
|
||||
res, err := b.run.Run(loop.WithSessionEnv(ctx, runEnv), sessID, loopMsgs)
|
||||
@@ -539,9 +551,9 @@ func (b *Bridge) processFrom(ctx context.Context, msg adapter.UnifiedMessage, a
|
||||
// Persist the assistant reply (owner-private → main session + broadcast;
|
||||
// restricted → isolated log) then deliver it back to the channel.
|
||||
if isOwnerPrivate {
|
||||
b.persistToMain(ctx, history.RoleAssistant, reply, platform, deviceIDFor(msg))
|
||||
b.persistToMain(ctx, history.RoleAssistant, reply, platform, deviceIDFor(msg), adapterName(a))
|
||||
} else {
|
||||
b.persistIsolated(ctx, history.RoleAssistant, reply, "", sess, msg.Platform, "")
|
||||
b.persistIsolated(ctx, history.RoleAssistant, reply, "", sess, msg.Platform, "", adapterName(a))
|
||||
}
|
||||
if err := b.sendReply(ctx, msg, a, reply); err != nil {
|
||||
if errors.Is(err, adapter.ErrUnsupported) {
|
||||
|
||||
@@ -255,6 +255,12 @@ func (c *captureHistory) Recent(context.Context, string, int) ([]history.Message
|
||||
func (c *captureHistory) ListPlatformLog(context.Context, string, int64, int) ([]history.Message, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (c *captureHistory) ListAdapterLog(context.Context, string, int64, int) ([]history.Message, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (c *captureHistory) ListAllLog(context.Context, int64, int) ([]history.Message, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (c *captureHistory) DeleteSession(context.Context, string) error { return nil }
|
||||
func (c *captureHistory) NewSubSession(context.Context, string) (string, error) {
|
||||
return "", nil
|
||||
@@ -284,6 +290,9 @@ func TestOwnerPrivatePersistsToMainSessionWithPlatform(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
// Register the account adapter that pretends to receive the message, so the
|
||||
// persisted stream carries the adapter registration name (e.g. "telegram").
|
||||
b.Add(&fakeAdapter{name: "telegram"})
|
||||
|
||||
msg := directMessage() // Platform = "telegram"
|
||||
b.process(context.Background(), msg)
|
||||
@@ -292,14 +301,14 @@ func TestOwnerPrivatePersistsToMainSessionWithPlatform(t *testing.T) {
|
||||
if len(recs) != 2 {
|
||||
t.Fatalf("appends = %d, want 2 (user + assistant)", len(recs))
|
||||
}
|
||||
// Both land on the main web session and carry the channel platform.
|
||||
// Both land on the main web session and carry the channel platform + adapter.
|
||||
if recs[0].SessionID != history.MainSessionID || recs[0].Role != history.RoleUser ||
|
||||
recs[0].Content != "hello" || recs[0].Platform != "telegram" {
|
||||
t.Fatalf("user record = %+v, want web/telegram/hello", recs[0])
|
||||
recs[0].Content != "hello" || recs[0].Platform != "telegram" || recs[0].AdapterID != "telegram" {
|
||||
t.Fatalf("user record = %+v, want web/telegram/hello/telegram", recs[0])
|
||||
}
|
||||
if recs[1].SessionID != history.MainSessionID || recs[1].Role != history.RoleAssistant ||
|
||||
recs[1].Content != "微信回复" || recs[1].Platform != "telegram" {
|
||||
t.Fatalf("assistant record = %+v, want web/telegram/微信回复", recs[1])
|
||||
recs[1].Content != "微信回复" || recs[1].Platform != "telegram" || recs[1].AdapterID != "telegram" {
|
||||
t.Fatalf("assistant record = %+v, want web/telegram/微信回复/telegram", recs[1])
|
||||
}
|
||||
// Every persisted message was handed to the broadcast hook.
|
||||
if len(persisted) != 2 {
|
||||
|
||||
@@ -168,10 +168,11 @@ func (b *Bridge) shouldRespond(ctx context.Context, msg adapter.UnifiedMessage,
|
||||
|
||||
// registerSilenced persists a silenced non-owner inbound turn into its isolated
|
||||
// session's platform log so the adapter log page still renders the non-owner
|
||||
// stream (design/16 §4.6 §4), then stops — no loop, no reply. It is the 登记 side
|
||||
// of the minimal-disturbance path.
|
||||
func (b *Bridge) registerSilenced(ctx context.Context, msg adapter.UnifiedMessage, actor adapter.Actor, sess session.Session) {
|
||||
b.persistIsolated(ctx, history.RoleUser, msg.Content, deviceIDFor(msg), sess, msg.Platform, senderDisplayName(msg, actor))
|
||||
// stream (design/16 §4.6 §4), then stops — no loop, no reply. a is the account
|
||||
// adapter that received the message, so the log is tagged with the adapter config.
|
||||
// It is the 登记 side of the minimal-disturbance path.
|
||||
func (b *Bridge) registerSilenced(ctx context.Context, msg adapter.UnifiedMessage, actor adapter.Actor, sess session.Session, a adapter.PlatformAdapter) {
|
||||
b.persistIsolated(ctx, history.RoleUser, msg.Content, deviceIDFor(msg), sess, msg.Platform, senderDisplayName(msg, actor), adapterName(a))
|
||||
b.log.Debug("non-owner message silenced, registered only",
|
||||
"platform", msg.Platform, "channel", msg.ChannelID, "session", sess.SessionID)
|
||||
}
|
||||
|
||||
@@ -50,17 +50,19 @@ func (b *Bridge) notifyPersisted(msg history.Message) {
|
||||
// platform and client deviceID, then notifies the transport hook so Web clients receive
|
||||
// a `message_persisted` event without a refresh (design/16 §4.4). deviceID is the
|
||||
// concrete client that sent the message (design/16 §4.5) — e.g. "weixin-ilink:conf0:wx_uid"
|
||||
// for IM. It is a no-op when no history store is wired, so the bridge runs unchanged
|
||||
// without storage.
|
||||
func (b *Bridge) persistToMain(ctx context.Context, role, content, platform, deviceID string) {
|
||||
// for IM. adapterID is the account adapter registration name that received the message
|
||||
// (e.g. "kook:3bef..."), so the admin log can query a per-adapter send/receive stream;
|
||||
// it is empty for Web-originated messages. It is a no-op when no history store is wired,
|
||||
// so the bridge runs unchanged without storage.
|
||||
func (b *Bridge) persistToMain(ctx context.Context, role, content, platform, deviceID, adapterID string) {
|
||||
if b.history == nil {
|
||||
return
|
||||
}
|
||||
full, err := b.history.AppendFull(ctx, history.Message{
|
||||
SessionID: history.MainSessionID, Role: role, Content: content, Platform: platform, DeviceID: deviceID,
|
||||
SessionID: history.MainSessionID, Role: role, Content: content, Platform: platform, DeviceID: deviceID, AdapterID: adapterID,
|
||||
})
|
||||
if err != nil {
|
||||
b.log.Warn("persist main session", "err", err, "role", role, "platform", platform, "device_id", deviceID)
|
||||
b.log.Warn("persist main session", "err", err, "role", role, "platform", platform, "device_id", deviceID, "adapter", adapterID)
|
||||
return
|
||||
}
|
||||
b.notifyPersisted(full)
|
||||
@@ -69,21 +71,22 @@ func (b *Bridge) persistToMain(ctx context.Context, role, content, platform, dev
|
||||
// persistIsolated writes one message into a restricted (group / stranger) session's
|
||||
// own platform log (design/16 §4.6 §4): the message lands in the isolated sess-xxx
|
||||
// session (NOT the main web session) and carries the channel platform, the client
|
||||
// deviceID and the sender's display name, so the IM adapter log page can render the
|
||||
// non-owner message stream. It is deliberately NOT broadcast to the Web main session
|
||||
// (only owner-private messages mirror to the Web). It is a no-op when no history
|
||||
// store is wired, so the bridge runs unchanged without storage.
|
||||
func (b *Bridge) persistIsolated(ctx context.Context, role, content, deviceID string, sess session.Session, platform, senderName string) {
|
||||
// deviceID, the sender's display name and the account adapter registration name, so
|
||||
// the IM adapter log page can render the non-owner message stream per adapter config.
|
||||
// It is deliberately NOT broadcast to the Web main session (only owner-private
|
||||
// messages mirror to the Web). It is a no-op when no history store is wired, so the
|
||||
// bridge runs unchanged without storage.
|
||||
func (b *Bridge) persistIsolated(ctx context.Context, role, content, deviceID string, sess session.Session, platform, senderName, adapterID string) {
|
||||
if b.history == nil {
|
||||
return
|
||||
}
|
||||
full, err := b.history.AppendFull(ctx, history.Message{
|
||||
SessionID: sess.SessionID, Role: role, Content: content, Platform: platform,
|
||||
DeviceID: deviceID, SenderName: senderName,
|
||||
DeviceID: deviceID, SenderName: senderName, AdapterID: adapterID,
|
||||
})
|
||||
if err != nil {
|
||||
b.log.Warn("persist isolated session", "err", err, "role", role,
|
||||
"session", sess.SessionID, "platform", platform, "sender_name", senderName)
|
||||
"session", sess.SessionID, "platform", platform, "sender_name", senderName, "adapter", adapterID)
|
||||
return
|
||||
}
|
||||
// No broadcast: third-party messages must not reach the Web main session (design/16 §4.6 §3).
|
||||
|
||||
@@ -38,6 +38,10 @@ type historyMessage struct {
|
||||
// message (group/stranger, design/16 §4.6). It is blank for the owner's
|
||||
// private chat on the main session, so empty means "labeled as the owner".
|
||||
SenderName string `json:"senderName,omitempty"`
|
||||
// AdapterID is the adapter (platform + account) registration name of the IM
|
||||
// source that produced the message (e.g. "kook:3bef..."). It is blank for
|
||||
// Web-originated messages.
|
||||
AdapterID string `json:"adapterId,omitempty"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
@@ -175,8 +179,62 @@ func (s *Server) handleChannelPlatformLog(w http.ResponseWriter, r *http.Request
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// handleDeleteSession deletes a sub session and all its messages. The main session is
|
||||
// immutable (design/16 §6) and returns 403.
|
||||
// handleChannelMessages returns up to limit messages for the admin adapter log page,
|
||||
// newest first, across one of three granularities (design/16 §4.6 §4): a single
|
||||
// adapter config (?adapter=kook:3bef...), a whole platform (?platform=kook), or the
|
||||
// full IM adapter overview (neither, so adapter_id != ''). before/limit reuse the
|
||||
// standard cursor pagination. The owner-private mirror on the main web session is
|
||||
// NOT excluded here because it carries the adapter_id; each granularity filters on
|
||||
// adapter_id / platform directly.
|
||||
func (s *Server) handleChannelMessages(w http.ResponseWriter, r *http.Request) {
|
||||
if s.history == nil {
|
||||
writeError(w, &apperr.AppError{Code: "not_implemented", Message: "会话历史服务未配置(未注入 HistoryStore)", HTTPCode: http.StatusNotImplemented})
|
||||
return
|
||||
}
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
limit := historyDefaultLimit
|
||||
if limitStr != "" {
|
||||
v, err := strconv.Atoi(limitStr)
|
||||
if err != nil || v < 1 {
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid limit"))
|
||||
return
|
||||
}
|
||||
limit = v
|
||||
}
|
||||
var before int64
|
||||
if b := r.URL.Query().Get("before"); b != "" {
|
||||
v, err := strconv.ParseInt(b, 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid before"))
|
||||
return
|
||||
}
|
||||
before = v
|
||||
}
|
||||
|
||||
adapter := r.URL.Query().Get("adapter")
|
||||
platform := r.URL.Query().Get("platform")
|
||||
|
||||
var msgs []history.Message
|
||||
var err error
|
||||
switch {
|
||||
case adapter != "":
|
||||
msgs, err = s.history.ListAdapterLog(r.Context(), adapter, before, limit)
|
||||
case platform != "":
|
||||
msgs, err = s.history.ListPlatformLog(r.Context(), platform, before, limit)
|
||||
default:
|
||||
msgs, err = s.history.ListAllLog(r.Context(), before, limit)
|
||||
}
|
||||
if err != nil {
|
||||
s.log.Error("history channel messages", "err", err, "adapter", adapter, "platform", platform)
|
||||
writeError(w, apperr.Wrap(apperr.ErrInternal, "channel_messages_failed", "failed to list channel messages"))
|
||||
return
|
||||
}
|
||||
out := make([]historyMessage, 0, len(msgs))
|
||||
for _, m := range msgs {
|
||||
out = append(out, toHistoryMessage(m))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
func (s *Server) handleDeleteSession(w http.ResponseWriter, r *http.Request) {
|
||||
if s.history == nil {
|
||||
writeError(w, &apperr.AppError{Code: "not_implemented", Message: "会话历史服务未配置(未注入 HistoryStore)", HTTPCode: http.StatusNotImplemented})
|
||||
@@ -213,6 +271,7 @@ func toHistoryMessage(m history.Message) historyMessage {
|
||||
Platform: m.Platform,
|
||||
DeviceID: m.DeviceID,
|
||||
SenderName: m.SenderName,
|
||||
AdapterID: m.AdapterID,
|
||||
CreatedAt: m.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -115,6 +115,8 @@ type HistoryStore interface {
|
||||
List(ctx context.Context, sessionID string, before int64, limit int, platform string) ([]history.Message, error)
|
||||
Recent(ctx context.Context, sessionID string, n int) ([]history.Message, error)
|
||||
ListPlatformLog(ctx context.Context, platform string, before int64, limit int) ([]history.Message, error)
|
||||
ListAdapterLog(ctx context.Context, adapterID string, before int64, limit int) ([]history.Message, error)
|
||||
ListAllLog(ctx context.Context, before int64, limit int) ([]history.Message, error)
|
||||
DeleteSession(ctx context.Context, sessionID string) error
|
||||
NewSubSession(ctx context.Context, ownerID string) (string, error)
|
||||
ListSessions(ctx context.Context) ([]history.SessionMeta, error)
|
||||
@@ -380,6 +382,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.Handle("GET /api/v1/admin/memories", adminAuth(http.HandlerFunc(s.adminListMemories)))
|
||||
mux.Handle("GET /api/v1/admin/plugins", adminAuth(http.HandlerFunc(s.adminListPlugins)))
|
||||
mux.Handle("GET /api/v1/admin/channels", adminAuth(http.HandlerFunc(s.adminListChannels)))
|
||||
mux.Handle("GET /api/v1/admin/channels/messages", adminAuth(http.HandlerFunc(s.handleChannelMessages)))
|
||||
mux.Handle("GET /api/v1/admin/channels/{platform}/messages", adminAuth(http.HandlerFunc(s.handleChannelPlatformLog)))
|
||||
mux.Handle("GET /api/v1/admin/iot", adminAuth(http.HandlerFunc(s.adminListIoT)))
|
||||
mux.Handle("GET /api/v1/admin/llm", adminAuth(http.HandlerFunc(s.adminGetLlm)))
|
||||
|
||||
@@ -187,6 +187,14 @@ func (f *fakeHistory) ListPlatformLog(context.Context, string, int64, int) ([]hi
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeHistory) ListAdapterLog(context.Context, string, int64, int) ([]history.Message, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeHistory) ListAllLog(context.Context, int64, int) ([]history.Message, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeHistory) DeleteSession(context.Context, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user