diff --git a/admin_bot_routes.go b/admin_bot_routes.go index cab08b8..a010e01 100644 --- a/admin_bot_routes.go +++ b/admin_bot_routes.go @@ -19,6 +19,8 @@ type botNodeRequest struct { PSK string `json:"psk"` NodeInfoBroadcastEnabled bool `json:"nodeinfo_broadcast_enabled"` NodeInfoBroadcastIntervalSeconds int64 `json:"nodeinfo_broadcast_interval_seconds"` + LLMQueueEnabled bool `json:"llm_queue_enabled"` + LLMIncludeChannelMessages bool `json:"llm_include_channel_messages"` } type botSendMessageRequest struct { @@ -253,7 +255,7 @@ func registerAdminBotRoutes(r gin.IRouter, store *store, sender botTextSender) { } func botNodeInputFromRequest(req botNodeRequest) botNodeInput { - return botNodeInput{NodeNum: req.NodeNum, LongName: req.LongName, ShortName: req.ShortName, Enabled: req.Enabled, DefaultChannelID: req.DefaultChannelID, TopicPrefix: req.TopicPrefix, PSK: req.PSK, NodeInfoBroadcastEnabled: req.NodeInfoBroadcastEnabled, NodeInfoBroadcastIntervalSeconds: req.NodeInfoBroadcastIntervalSeconds} + return botNodeInput{NodeNum: req.NodeNum, LongName: req.LongName, ShortName: req.ShortName, Enabled: req.Enabled, DefaultChannelID: req.DefaultChannelID, TopicPrefix: req.TopicPrefix, PSK: req.PSK, NodeInfoBroadcastEnabled: req.NodeInfoBroadcastEnabled, NodeInfoBroadcastIntervalSeconds: req.NodeInfoBroadcastIntervalSeconds, LLMQueueEnabled: req.LLMQueueEnabled, LLMIncludeChannelMessages: req.LLMIncludeChannelMessages} } func parseBotID(c *gin.Context, message string) (uint64, bool) { @@ -299,7 +301,7 @@ func writeBotNodeMutationResponse(c *gin.Context, status int, row *botNodeRecord } func botNodeDTO(row botNodeRecord) gin.H { - return gin.H{"id": row.ID, "node_id": row.NodeID, "node_num": row.NodeNum, "long_name": row.LongName, "short_name": row.ShortName, "enabled": row.Enabled, "default_channel_id": row.DefaultChannelID, "topic_prefix": row.TopicPrefix, "psk": row.PSK, "public_key": row.PublicKey, "private_key_set": row.PrivateKey != "", "nodeinfo_broadcast_enabled": row.NodeInfoBroadcastEnabled, "nodeinfo_broadcast_interval_seconds": row.NodeInfoBroadcastIntervalSeconds, "last_nodeinfo_broadcast_at": row.LastNodeInfoBroadcastAt, "created_at": row.CreatedAt, "updated_at": row.UpdatedAt} + return gin.H{"id": row.ID, "node_id": row.NodeID, "node_num": row.NodeNum, "long_name": row.LongName, "short_name": row.ShortName, "enabled": row.Enabled, "default_channel_id": row.DefaultChannelID, "topic_prefix": row.TopicPrefix, "psk": row.PSK, "public_key": row.PublicKey, "private_key_set": row.PrivateKey != "", "nodeinfo_broadcast_enabled": row.NodeInfoBroadcastEnabled, "nodeinfo_broadcast_interval_seconds": row.NodeInfoBroadcastIntervalSeconds, "last_nodeinfo_broadcast_at": row.LastNodeInfoBroadcastAt, "llm_queue_enabled": row.LLMQueueEnabled, "llm_include_channel_messages": row.LLMIncludeChannelMessages, "created_at": row.CreatedAt, "updated_at": row.UpdatedAt} } func botMessageDTO(row botMessageRecord) gin.H { diff --git a/bot_store.go b/bot_store.go index 8f358bc..9e9117d 100644 --- a/bot_store.go +++ b/bot_store.go @@ -39,6 +39,8 @@ type botNodeInput struct { PSK string NodeInfoBroadcastEnabled bool NodeInfoBroadcastIntervalSeconds int64 + LLMQueueEnabled bool + LLMIncludeChannelMessages bool } type botMessageListOptions struct { @@ -126,6 +128,8 @@ func (s *store) UpdateBotNode(id uint64, input botNodeInput) (*botNodeRecord, er "psk": row.PSK, "nodeinfo_broadcast_enabled": row.NodeInfoBroadcastEnabled, "nodeinfo_broadcast_interval_seconds": row.NodeInfoBroadcastIntervalSeconds, + "llm_queue_enabled": row.LLMQueueEnabled, + "llm_include_channel_messages": row.LLMIncludeChannelMessages, "updated_at": time.Now(), } if err := s.db.Model(&botNodeRecord{}).Where("id = ?", id).Updates(updates).Error; err != nil { @@ -276,7 +280,7 @@ func (s *store) normalizedBotNodeRecord(input botNodeInput) (*botNodeRecord, err if err := validateBotNodeNum(nodeNum); err != nil { return nil, err } - return &botNodeRecord{NodeID: mqtpp.NodeNumToID(uint32(nodeNum)), NodeNum: nodeNum, LongName: longName, ShortName: shortName, Enabled: input.Enabled, DefaultChannelID: channelID, TopicPrefix: topicPrefix, PSK: psk, NodeInfoBroadcastEnabled: input.NodeInfoBroadcastEnabled, NodeInfoBroadcastIntervalSeconds: interval}, nil + return &botNodeRecord{NodeID: mqtpp.NodeNumToID(uint32(nodeNum)), NodeNum: nodeNum, LongName: longName, ShortName: shortName, Enabled: input.Enabled, DefaultChannelID: channelID, TopicPrefix: topicPrefix, PSK: psk, NodeInfoBroadcastEnabled: input.NodeInfoBroadcastEnabled, NodeInfoBroadcastIntervalSeconds: interval, LLMQueueEnabled: input.LLMQueueEnabled, LLMIncludeChannelMessages: input.LLMIncludeChannelMessages}, nil } func populateBotNodeKeys(row *botNodeRecord) error { diff --git a/db.go b/db.go index db02c75..d4787e3 100644 --- a/db.go +++ b/db.go @@ -266,6 +266,8 @@ type botNodeRecord struct { NodeInfoBroadcastIntervalSeconds int64 `gorm:"column:nodeinfo_broadcast_interval_seconds;not null"` LastNodeInfoBroadcastAt *time.Time `gorm:"column:last_nodeinfo_broadcast_at;index"` LastPacketID int64 `gorm:"column:last_packet_id;not null"` + LLMQueueEnabled bool `gorm:"column:llm_queue_enabled;not null;default:1;index"` + LLMIncludeChannelMessages bool `gorm:"column:llm_include_channel_messages;not null;default:0;index"` CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;index"` } @@ -667,6 +669,16 @@ func migrateBotNodePSK(tx *gorm.DB, migrator gorm.Migrator, driver string) error return fmt.Errorf("migrate bot_nodes last_nodeinfo_broadcast_at column: %w", err) } } + if !migrator.HasColumn(&botNodeRecord{}, "LLMQueueEnabled") { + if err := tx.Exec("ALTER TABLE bot_nodes ADD COLUMN llm_queue_enabled numeric NOT NULL DEFAULT 1").Error; err != nil { + return fmt.Errorf("migrate bot_nodes llm_queue_enabled column: %w", err) + } + } + if !migrator.HasColumn(&botNodeRecord{}, "LLMIncludeChannelMessages") { + if err := tx.Exec("ALTER TABLE bot_nodes ADD COLUMN llm_include_channel_messages numeric NOT NULL DEFAULT 0").Error; err != nil { + return fmt.Errorf("migrate bot_nodes llm_include_channel_messages column: %w", err) + } + } return nil } diff --git a/llm_store.go b/llm_store.go index 464fe56..96806ec 100644 --- a/llm_store.go +++ b/llm_store.go @@ -27,24 +27,19 @@ type LLMMessageQueueInput struct { // EnqueueLLMMessage 将消息添加到 LLM 队列 func (s *store) EnqueueLLMMessage(input LLMMessageQueueInput) (*llmMessageQueueRecord, error) { - // 检查 LLM 队列是否启用 - enabled, err := s.GetBoolRuntimeSetting(runtimeSettingLLMQueueEnabled, true) - if err != nil { - return nil, fmt.Errorf("check llm queue enabled: %w", err) - } - if !enabled { - return nil, nil // 静默返回,不报错 + var err error + + if input.BotID == 0 { + return nil, nil // bot_id 为 0 的消息不再入队 } - // 如果是频道消息,检查是否启用了频道消息入队 - if input.BotID == 0 { - includeChannel, err := s.GetBoolRuntimeSetting(runtimeSettingLLMQueueIncludeChannel, false) - if err != nil { - return nil, fmt.Errorf("check llm include channel: %w", err) - } - if !includeChannel { - return nil, nil // 频道消息入队未启用,静默返回 - } + // 检查机器人级别的 LLM 队列设置 + bot, err := s.GetBotNode(input.BotID) + if err != nil { + return nil, nil // 机器人不存在,静默返回 + } + if !bot.LLMQueueEnabled { + return nil, nil // 机器人的 LLM 队列未启用,静默返回 } if input.FromNodeID == "" { @@ -54,21 +49,10 @@ func (s *store) EnqueueLLMMessage(input LLMMessageQueueInput) (*llmMessageQueueR return nil, fmt.Errorf("text is required") } - // 检查是否存在重复消息 + // 检查是否存在重复消息:相同 bot_id + packet_id 且未删除 var existing llmMessageQueueRecord - if input.BotID > 0 { - // 机器人私聊:相同 bot_id + packet_id 且未删除 - err = s.db.Where("bot_id = ? AND packet_id = ? AND deleted_at IS NULL", input.BotID, input.PacketID). - Take(&existing).Error - } else { - // 频道消息:相同 from_node_id + channel_id + packet_id 且未删除 - channelID := "" - if input.ChannelID != nil { - channelID = *input.ChannelID - } - err = s.db.Where("bot_id = 0 AND from_node_id = ? AND channel_id = ? AND packet_id = ? AND deleted_at IS NULL", input.FromNodeID, channelID, input.PacketID). - Take(&existing).Error - } + err = s.db.Where("bot_id = ? AND packet_id = ? AND deleted_at IS NULL", input.BotID, input.PacketID). + Take(&existing).Error if err == nil { // 重复消息,直接返回已存在的 return &existing, nil @@ -214,6 +198,7 @@ func llmMessageDTO(row llmMessageQueueRecord) map[string]any { } // enqueueChannelMessageToLLM 将频道消息添加到 LLM 队列 +// 为每个启用了「包含频道消息」的机器人都创建一条独立的队列记录 func enqueueChannelMessageToLLM(s *store, record map[string]any) error { if s == nil { return nil @@ -261,20 +246,30 @@ func enqueueChannelMessageToLLM(s *store, record map[string]any) error { contentPtr = &s } - _, _ = s.EnqueueLLMMessage(LLMMessageQueueInput{ - BotID: 0, // 0 表示频道消息 - BotNodeID: "", - BotNodeNum: 0, - FromNodeID: fromNodeID, - FromNodeNum: fromNodeNum, - LongName: longName, - ShortName: shortName, - Text: text, - PacketID: packetID, - ChannelID: channelID, - Topic: topic, - ContentJSON: contentPtr, - }) + // 查询所有启用了 LLM 队列且包含频道消息的机器人 + var bots []botNodeRecord + err = s.db.Where("llm_queue_enabled = ? AND llm_include_channel_messages = ?", true, true).Find(&bots).Error + if err != nil { + return fmt.Errorf("query bots for channel message enqueue: %w", err) + } + + // 为每个符合条件的机器人创建一条队列记录 + for _, bot := range bots { + _, _ = s.EnqueueLLMMessage(LLMMessageQueueInput{ + BotID: bot.ID, + BotNodeID: bot.NodeID, + BotNodeNum: bot.NodeNum, + FromNodeID: fromNodeID, + FromNodeNum: fromNodeNum, + LongName: longName, + ShortName: shortName, + Text: text, + PacketID: packetID, + ChannelID: channelID, + Topic: topic, + ContentJSON: contentPtr, + }) + } return nil } diff --git a/meshmap_frontend/src/components/AdminLLM.vue b/meshmap_frontend/src/components/AdminLLM.vue index 803d611..6b94a81 100644 --- a/meshmap_frontend/src/components/AdminLLM.vue +++ b/meshmap_frontend/src/components/AdminLLM.vue @@ -1,10 +1,10 @@