forked from kevin/meshtastic_mqtt_server
安全加固:LLM 会话按 (bot,peer) 隔离+历史上限50条,LLM 入队按 (bot,from_node) 限流(60s内5条),瓦片磁盘缓存单源配额(3000文件/300MB 按mtime淘汰),签到墙读接口限速+全站每日1000条封顶,敏感数据落盘 AES-256-GCM 加密(MESH_SECRET_KEY, 兼容明文迁移),admin 改密需验证当前密码+pwd_version 会话撤销,后端 v1.5.0
This commit is contained in:
@@ -84,17 +84,35 @@ func (s *Store) Get(id string) (*message.Conversation, error) {
|
||||
return &conv, nil
|
||||
}
|
||||
|
||||
// GetOrCreateForBot gets or creates a conversation for a bot
|
||||
// GetOrCreateForBot gets or creates a conversation for a bot.
|
||||
// peerNodeID 非空时按 (bot, peer) 隔离会话:不同对端互不可见上下文,
|
||||
// 防止 A 的私聊内容/提示注入影响 B;peerNodeID 为空时维持旧行为(取最近会话)。
|
||||
func (s *Store) GetOrCreateForBot(botID uint64, botNodeID string, peerNodeID string) (*message.Conversation, error) {
|
||||
// Try to find an existing conversation with this peer
|
||||
peerNodeID = strings.TrimSpace(peerNodeID)
|
||||
convs, err := s.ListForBot(botID)
|
||||
if err == nil && len(convs) > 0 {
|
||||
// Use the most recent conversation (List already sorts by UpdatedAt desc)
|
||||
// Note: List returns convs with Messages = nil, so we need to reload
|
||||
return s.Get(convs[0].ID)
|
||||
if peerNodeID != "" {
|
||||
// List 按 UpdatedAt 倒序,取该对端最近一次会话。
|
||||
for _, conv := range convs {
|
||||
if conv.PeerNodeID == peerNodeID {
|
||||
return s.Get(conv.ID)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return s.Get(convs[0].ID)
|
||||
}
|
||||
}
|
||||
// Create a new conversation
|
||||
return s.Create(botID, botNodeID)
|
||||
conv, err := s.Create(botID, botNodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if peerNodeID != "" {
|
||||
conv.PeerNodeID = peerNodeID
|
||||
if err := s.Save(conv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return conv, nil
|
||||
}
|
||||
|
||||
// List returns all conversations
|
||||
@@ -167,6 +185,9 @@ func (s *Store) DeleteForBot(botID uint64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// maxConversationMessages 限制单会话保留的历史消息数,防止上下文无限增长。
|
||||
const maxConversationMessages = 50
|
||||
|
||||
// AddMessage adds a message to a conversation
|
||||
func (s *Store) AddMessage(convID string, msg message.ChatMessage) error {
|
||||
conv, err := s.Get(convID)
|
||||
@@ -174,6 +195,9 @@ func (s *Store) AddMessage(convID string, msg message.ChatMessage) error {
|
||||
return err
|
||||
}
|
||||
conv.Messages = append(conv.Messages, msg)
|
||||
if len(conv.Messages) > maxConversationMessages {
|
||||
conv.Messages = conv.Messages[len(conv.Messages)-maxConversationMessages:]
|
||||
}
|
||||
if conv.Title == "" || conv.Title == "新对话" {
|
||||
conv.Title = GenerateTitle(conv.Messages)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package conversation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"meshtastic_mqtt_server/internal/message"
|
||||
)
|
||||
|
||||
func newTestStore(t *testing.T) *Store {
|
||||
t.Helper()
|
||||
return NewStore(t.TempDir())
|
||||
}
|
||||
|
||||
func TestGetOrCreateForBotPeerIsolation(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
|
||||
convA, err := s.GetOrCreateForBot(1, "bot1", "nodeA")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
convB, err := s.GetOrCreateForBot(1, "bot1", "nodeB")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if convA.ID == convB.ID {
|
||||
t.Fatal("different peers must get different conversations")
|
||||
}
|
||||
if convA.PeerNodeID != "nodeA" || convB.PeerNodeID != "nodeB" {
|
||||
t.Fatalf("peer ids not set: %q %q", convA.PeerNodeID, convB.PeerNodeID)
|
||||
}
|
||||
|
||||
// 再次获取应命中同一会话(内容互不可见)。
|
||||
convA2, err := s.GetOrCreateForBot(1, "bot1", "nodeA")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if convA2.ID != convA.ID {
|
||||
t.Fatalf("peer A should reuse its conversation, got %s want %s", convA2.ID, convA.ID)
|
||||
}
|
||||
|
||||
// 不同 bot 的会话互不影响。
|
||||
convOtherBot, err := s.GetOrCreateForBot(2, "bot2", "nodeA")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if convOtherBot.ID == convA.ID {
|
||||
t.Fatal("different bots must get different conversations")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrCreateForBotLegacyNoPeer(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
c1, err := s.GetOrCreateForBot(1, "bot1", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c2, err := s.GetOrCreateForBot(1, "bot1", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c1.ID != c2.ID {
|
||||
t.Fatal("no-peer callers should share the most recent conversation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddMessageHistoryCap(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
conv, err := s.GetOrCreateForBot(1, "bot1", "nodeA")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := 0; i < 120; i++ {
|
||||
if err := s.AddMessage(conv.ID, message.ChatMessage{Role: "user", Content: "m"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
conv, err = s.Get(conv.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(conv.Messages) != maxConversationMessages {
|
||||
t.Fatalf("history must be capped at %d, got %d", maxConversationMessages, len(conv.Messages))
|
||||
}
|
||||
if conv.Messages[0].Content != "m" || conv.Messages[len(conv.Messages)-1].Content != "m" {
|
||||
t.Fatal("messages content intact")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user