工具路由还没实现

This commit is contained in:
2026-06-18 08:14:09 +08:00
parent dfb3fdd3e5
commit 45822fd333
18 changed files with 172 additions and 223 deletions
+1
View File
@@ -31,6 +31,7 @@ desktop.ini
# 项目运行时数据(Windows 测试路径)
win/etc/
win/srv/
win/var/
# 数据库文件
*.db
+10 -3
View File
@@ -18,11 +18,17 @@ import (
"gorm.io/gorm"
)
// SystemPromptStore is the interface for getting the system prompt
type SystemPromptStore interface {
GetLLMPrimaryConfigSystemPrompt() (string, error)
}
// Config holds the AI service configuration
type Config struct {
LLMProviders []llm.ProviderConfig
DataDir string
Enabled bool
LLMProviders []llm.ProviderConfig
DataDir string
Enabled bool
SystemPromptStore SystemPromptStore
}
// Service manages all AI-related components
@@ -91,6 +97,7 @@ func NewService(cfg Config, db *gorm.DB, botSender autoreply.BotSender) (*Servic
convStore,
msgQueue,
botSender,
cfg.SystemPromptStore,
)
return &Service{
+19 -8
View File
@@ -7,22 +7,33 @@ import (
// BotServiceAdapter adapts the bot service to the BotSender interface
type BotServiceAdapter struct {
sendTextFn func(ctx context.Context, botID uint64, toNodeNum int64, text string) error
sendDirectTextFn func(ctx context.Context, botID uint64, toNodeNum int64, text string) error
sendChannelTextFn func(ctx context.Context, botID uint64, channelID string, text string) error
}
// NewBotServiceAdapter creates a new bot service adapter
func NewBotServiceAdapter(
sendTextFn func(ctx context.Context, botID uint64, toNodeNum int64, text string) error,
sendDirectTextFn func(ctx context.Context, botID uint64, toNodeNum int64, text string) error,
sendChannelTextFn func(ctx context.Context, botID uint64, channelID string, text string) error,
) *BotServiceAdapter {
return &BotServiceAdapter{
sendTextFn: sendTextFn,
sendDirectTextFn: sendDirectTextFn,
sendChannelTextFn: sendChannelTextFn,
}
}
// SendText sends a text message via the bot service
func (a *BotServiceAdapter) SendText(ctx context.Context, botID uint64, toNodeNum int64, text string) error {
if a.sendTextFn == nil {
return fmt.Errorf("send text function is nil")
// SendDirectText sends a direct/private message to a specific node
func (a *BotServiceAdapter) SendDirectText(ctx context.Context, botID uint64, toNodeNum int64, text string) error {
if a.sendDirectTextFn == nil {
return fmt.Errorf("send direct text function is nil")
}
return a.sendTextFn(ctx, botID, toNodeNum, text)
return a.sendDirectTextFn(ctx, botID, toNodeNum, text)
}
// SendChannelText sends a channel message to a specific channel
func (a *BotServiceAdapter) SendChannelText(ctx context.Context, botID uint64, channelID string, text string) error {
if a.sendChannelTextFn == nil {
return fmt.Errorf("send channel text function is nil")
}
return a.sendChannelTextFn(ctx, botID, channelID, text)
}
+2
View File
@@ -38,6 +38,7 @@ type llmMessageQueueRecord struct {
PacketID int64 `gorm:"column:packet_id;not null"`
ChannelID *string `gorm:"column:channel_id"`
Topic string `gorm:"column:topic;not null"`
MessageType string `gorm:"column:message_type;not null;default:'direct'"` // "channel" 或 "direct"
Status string `gorm:"column:status;not null;index"`
Error string `gorm:"column:error;type:text"`
Reply string `gorm:"column:reply;type:text"`
@@ -79,6 +80,7 @@ func (q *DBMessageQueue) GetPendingMessages(botID uint64, limit int) ([]QueuedMe
PacketID: r.PacketID,
ChannelID: r.ChannelID,
Topic: r.Topic,
MessageType: r.MessageType,
ReceivedAt: r.ReceivedAt,
})
}
+50 -19
View File
@@ -51,22 +51,32 @@ type QueuedMessage struct {
PacketID int64
ChannelID *string
Topic string
MessageType string // "channel" 或 "direct"
ReceivedAt time.Time
}
// BotSender is the interface for sending bot messages
type BotSender interface {
SendText(ctx context.Context, botID uint64, toNodeNum int64, text string) error
// SendDirectText 发送私聊消息给指定节点
SendDirectText(ctx context.Context, botID uint64, toNodeNum int64, text string) error
// SendChannelText 发送频道消息到指定频道
SendChannelText(ctx context.Context, botID uint64, channelID string, text string) error
}
// SystemPromptStore is the interface for getting the system prompt
type SystemPromptStore interface {
GetLLMPrimaryConfigSystemPrompt() (string, error)
}
// Service manages automatic AI replies for bots
type Service struct {
llmState *llm.State
toolRouter *toolrouter.State
toolMgr *toolmanager.Manager
convStore *conversation.Store
msgQueue MessageQueue
botSender BotSender
llmState *llm.State
toolRouter *toolrouter.State
toolMgr *toolmanager.Manager
convStore *conversation.Store
msgQueue MessageQueue
botSender BotSender
systemPromptStore SystemPromptStore
running bool
mu sync.Mutex
@@ -82,14 +92,16 @@ func NewService(
convStore *conversation.Store,
msgQueue MessageQueue,
botSender BotSender,
systemPromptStore SystemPromptStore,
) *Service {
return &Service{
llmState: llmState,
toolRouter: toolRouter,
toolMgr: toolMgr,
convStore: convStore,
msgQueue: msgQueue,
botSender: botSender,
llmState: llmState,
toolRouter: toolRouter,
toolMgr: toolMgr,
convStore: convStore,
msgQueue: msgQueue,
botSender: botSender,
systemPromptStore: systemPromptStore,
}
}
@@ -241,13 +253,22 @@ func (s *Service) processMessage(ctx context.Context, msg QueuedMessage) {
return
}
// Get system prompt from primary config
var systemPrompt string
if s.systemPromptStore != nil {
systemPrompt, err = s.systemPromptStore.GetLLMPrimaryConfigSystemPrompt()
if err != nil {
printJSON(map[string]any{"event": "llm_system_prompt_warning", "msg_id": msg.ID, "error": err.Error()})
}
}
// Run the tool loop to get augmented messages
augmentedMessages, err := toolrouter.RunAgentToolLoop(procCtx, s.toolRouter, profile, conv.Messages, s.toolMgr, nil)
_ = augmentedMessages // We'll use this in the future with proper tool support
// For now, use simple completion since we don't have tools registered yet
printJSON(map[string]any{"event": "llm_process_completion_start", "msg_id": msg.ID})
reply, err := completion.CompleteText(procCtx, profile, conv.Messages, 512)
printJSON(map[string]any{"event": "llm_process_completion_start", "msg_id": msg.ID, "has_system_prompt": systemPrompt != ""})
reply, err := completion.CompleteText(procCtx, profile, systemPrompt, conv.Messages, 512)
if err != nil {
errMsg := fmt.Sprintf("LLM completion failed: %v", err)
printJSON(map[string]any{"event": "llm_process_failed", "msg_id": msg.ID, "step": "llm_completion", "error": errMsg})
@@ -304,10 +325,20 @@ func (s *Service) processMessage(ctx context.Context, msg QueuedMessage) {
// Non-fatal, continue
}
// Send the reply via the bot
printJSON(map[string]any{"event": "llm_process_send_start", "msg_id": msg.ID, "to_node_num": msg.FromNodeNum})
if err := s.botSender.SendText(procCtx, msg.BotID, msg.FromNodeNum, reply); err != nil {
errMsg := fmt.Sprintf("failed to send reply: %v", err)
// Send the reply via the bot - 根据消息类型决定发送方式
// 频道消息:回复到原频道;私聊消息:回复给发送节点
var sendErr error
if msg.MessageType == "channel" && msg.ChannelID != nil && *msg.ChannelID != "" {
// 频道消息 - 回复到原频道
printJSON(map[string]any{"event": "llm_process_send_start", "msg_id": msg.ID, "channel_id": *msg.ChannelID, "message_type": "channel"})
sendErr = s.botSender.SendChannelText(procCtx, msg.BotID, *msg.ChannelID, reply)
} else {
// 私聊消息 - 回复给发送节点
printJSON(map[string]any{"event": "llm_process_send_start", "msg_id": msg.ID, "to_node_num": msg.FromNodeNum, "message_type": "direct"})
sendErr = s.botSender.SendDirectText(procCtx, msg.BotID, msg.FromNodeNum, reply)
}
if sendErr != nil {
errMsg := fmt.Sprintf("failed to send reply: %v", sendErr)
printJSON(map[string]any{"event": "llm_process_failed", "msg_id": msg.ID, "step": "send_reply", "error": errMsg})
_ = s.msgQueue.MarkAsFailed(msg.ID, errMsg)
return
+21 -18
View File
@@ -290,24 +290,27 @@ func insertInboundBotDirectMessage(s *store, record map[string]any, clientInfo m
}
_ = clientInfo // mqtt 元数据已经记录在 content_json 里,这里保留参数以保持队列签名一致
// 同时将消息添加到 LLM 队列
longName := nullableString(record["long_name"])
shortName := nullableString(record["short_name"])
channelID := nullableString(record["channel_id"])
_, err = s.EnqueueLLMMessage(LLMMessageQueueInput{
BotID: bot.ID,
BotNodeID: bot.NodeID,
BotNodeNum: bot.NodeNum,
FromNodeID: peerNodeID,
FromNodeNum: int64(peerNum),
LongName: longName,
ShortName: shortName,
Text: text,
PacketID: int64(packetID),
ChannelID: channelID,
Topic: topic,
ContentJSON: contentPtr,
})
// 同时将消息添加到 LLM 队列(忽略机器人自己发送的消息)
if peerNodeID != bot.NodeID {
longName := nullableString(record["long_name"])
shortName := nullableString(record["short_name"])
channelID := nullableString(record["channel_id"])
_, err = s.EnqueueLLMMessage(LLMMessageQueueInput{
BotID: bot.ID,
BotNodeID: bot.NodeID,
BotNodeNum: bot.NodeNum,
FromNodeID: peerNodeID,
FromNodeNum: int64(peerNum),
LongName: longName,
ShortName: shortName,
Text: text,
PacketID: int64(packetID),
ChannelID: channelID,
Topic: topic,
MessageType: "direct",
ContentJSON: contentPtr,
})
}
if err != nil {
printJSON(map[string]any{
"event": "llm_queue_enqueue_failed",
+16 -2
View File
@@ -3,6 +3,7 @@ package completion
import (
"context"
"fmt"
"strings"
"time"
"meshtastic_mqtt_server/llm"
@@ -35,12 +36,25 @@ func CompleteChat(ctx context.Context, profile *llm.Profile, req model.CreateCha
}
// CompleteText completes a text prompt using conversation messages
func CompleteText(ctx context.Context, profile *llm.Profile, messages []message.ChatMessage, maxTokens int) (string, error) {
// If systemPrompt is not empty, it will be added as the first message
func CompleteText(ctx context.Context, profile *llm.Profile, systemPrompt string, messages []message.ChatMessage, maxTokens int) (string, error) {
if profile == nil || profile.Client == nil {
return "", fmt.Errorf("llm profile or client is nil")
}
arkMessages := make([]*model.ChatCompletionMessage, 0, len(messages))
arkMessages := make([]*model.ChatCompletionMessage, 0, len(messages)+1)
// Add system prompt if provided
if strings.TrimSpace(systemPrompt) != "" {
content := &model.ChatCompletionMessageContent{
StringValue: &systemPrompt,
}
arkMessages = append(arkMessages, &model.ChatCompletionMessage{
Role: "system",
Content: content,
})
}
for _, msg := range messages {
content := &model.ChatCompletionMessageContent{
StringValue: &msg.Content,
+7
View File
@@ -363,6 +363,7 @@ type llmMessageQueueRecord struct {
PacketID int64 `gorm:"column:packet_id;not null;index"`
ChannelID *string `gorm:"column:channel_id"`
Topic string `gorm:"column:topic;not null"`
MessageType string `gorm:"column:message_type;not null;default:'direct'"` // "channel" 或 "direct"
Status string `gorm:"column:status;not null;index"`
Error string `gorm:"column:error;type:text"`
Reply string `gorm:"column:reply;type:text"`
@@ -752,6 +753,12 @@ func migrateBotNodePSK(tx *gorm.DB, migrator gorm.Migrator, driver string) error
return fmt.Errorf("migrate llm_message_queue reply column: %w", err)
}
}
// 迁移 LLM 消息队列 message_type 列
if migrator.HasTable(&llmMessageQueueRecord{}) && !migrator.HasColumn(&llmMessageQueueRecord{}, "MessageType") {
if err := tx.Exec("ALTER TABLE llm_message_queue ADD COLUMN message_type text NOT NULL DEFAULT 'direct'").Error; err != nil {
return fmt.Errorf("migrate llm_message_queue message_type column: %w", err)
}
}
return nil
}
+30 -1
View File
@@ -156,6 +156,20 @@ func (s *store) GetLLMPrimaryConfig() (*llmPrimaryConfigRecord, error) {
return &record, nil
}
// GetLLMPrimaryConfigSystemPrompt 获取主 AI 回复配置中的系统提示词
// 如果没有配置或出错,返回空字符串(autoreply service 会处理这种情况)
func (s *store) GetLLMPrimaryConfigSystemPrompt() (string, error) {
record, err := s.GetLLMPrimaryConfig()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 没有配置时返回空字符串,使用默认行为
return "", nil
}
return "", err
}
return record.SystemPrompt, nil
}
// CreateLLMPrimaryConfig 创建主 AI 回复配置
func (s *store) CreateLLMPrimaryConfig(record *llmPrimaryConfigRecord) error {
if err := s.db.Create(record).Error; err != nil {
@@ -206,6 +220,7 @@ type LLMMessageQueueInput struct {
PacketID int64
ChannelID *string
Topic string
MessageType string // "channel" 或 "direct"
ContentJSON *string
}
@@ -226,6 +241,11 @@ func (s *store) EnqueueLLMMessage(input LLMMessageQueueInput) (*llmMessageQueueR
return nil, nil // 机器人的 LLM 队列未启用,静默返回
}
// 忽略机器人自己发送的消息,避免自循环
if input.FromNodeID == bot.NodeID {
return nil, nil
}
if input.FromNodeID == "" {
return nil, fmt.Errorf("from_node_id is required")
}
@@ -258,6 +278,10 @@ func (s *store) EnqueueLLMMessage(input LLMMessageQueueInput) (*llmMessageQueueR
}
now := time.Now()
messageType := input.MessageType
if messageType == "" {
messageType = "direct"
}
record := &llmMessageQueueRecord{
BotID: input.BotID,
BotNodeID: input.BotNodeID,
@@ -270,6 +294,7 @@ func (s *store) EnqueueLLMMessage(input LLMMessageQueueInput) (*llmMessageQueueR
PacketID: input.PacketID,
ChannelID: input.ChannelID,
Topic: input.Topic,
MessageType: messageType,
Status: llmMessageStatusPending,
ReceivedAt: now,
ContentJSON: input.ContentJSON,
@@ -450,8 +475,11 @@ func enqueueChannelMessageToLLM(s *store, record map[string]any) error {
return fmt.Errorf("query bots for channel message enqueue: %w", err)
}
// 为每个符合条件的机器人创建一条队列记录
// 为每个符合条件的机器人创建一条队列记录(忽略机器人自己发送的消息)
for _, bot := range bots {
if fromNodeID == bot.NodeID {
continue
}
_, err = s.EnqueueLLMMessage(LLMMessageQueueInput{
BotID: bot.ID,
BotNodeID: bot.NodeID,
@@ -464,6 +492,7 @@ func enqueueChannelMessageToLLM(s *store, record map[string]any) error {
PacketID: packetID,
ChannelID: channelID,
Topic: topic,
MessageType: "channel",
ContentJSON: contentPtr,
})
if err != nil {
+16 -4
View File
@@ -272,8 +272,9 @@ func run(cfg *config) error {
})
}
// Create bot sender adapter
// Create bot sender adapter - 支持频道消息和私聊消息两种发送方式
botSenderAdapter := autoreply.NewBotServiceAdapter(
// SendDirectText: 发送私聊消息
func(ctx context.Context, botID uint64, toNodeNum int64, text string) error {
_, err := botSender.SendText(ctx, botSendTextRequest{
BotID: botID,
@@ -283,12 +284,23 @@ func run(cfg *config) error {
})
return err
},
// SendChannelText: 发送频道消息
func(ctx context.Context, botID uint64, channelID string, text string) error {
_, err := botSender.SendText(ctx, botSendTextRequest{
BotID: botID,
MessageType: "channel",
ChannelID: channelID,
Text: text,
})
return err
},
)
aiService, err = ai.NewService(ai.Config{
LLMProviders: providerConfigs,
DataDir: cfg.DataDir,
Enabled: cfg.AI.Enabled,
LLMProviders: providerConfigs,
DataDir: cfg.DataDir,
Enabled: cfg.AI.Enabled,
SystemPromptStore: store,
}, store.db, botSenderAdapter)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to initialize AI service: %v\n", err)
@@ -1,18 +0,0 @@
{
"id": "bot_1_1781711397025279500",
"bot_id": 1,
"bot_node_id": "!4217495a",
"title": "[来自 !8f821aa5] 🌞",
"created_at": "2026-06-17T23:49:57.0252795+08:00",
"updated_at": "2026-06-17T23:50:03.9827715+08:00",
"messages": [
{
"role": "user",
"content": "[来自 !8f821aa5] 🌞"
},
{
"role": "assistant",
"content": "你好呀!☀️\n\n看到你发来的这个像是系统标识符的字符串,还有那个温暖的太阳表情,感觉像是某种问候或者测试信号?\n\n如果你是想让我解读这个 ..."
}
]
}
@@ -1,18 +0,0 @@
{
"id": "bot_1_1781713830520078500",
"bot_id": 1,
"bot_node_id": "!1abcead5",
"title": "[来自 !72d17be4] 你好",
"created_at": "2026-06-18T00:30:30.5200785+08:00",
"updated_at": "2026-06-18T00:30:34.3123551+08:00",
"messages": [
{
"role": "user",
"content": "[来自 !72d17be4] 你好"
},
{
"role": "assistant",
"content": "你好!很高兴见到你!😊\n\n我是DeepSeek,由深度求索公司创造的AI助手。无论你有什么问题、需要帮助,还是只是想聊聊天,我都很乐意陪你!\n\n有什\ufffd\ufffd..."
}
]
}
@@ -1,18 +0,0 @@
{
"id": "bot_1_1781713890525027000",
"bot_id": 1,
"bot_node_id": "!1abcead5",
"title": "[来自 !72d17be4] 你好",
"created_at": "2026-06-18T00:31:30.525027+08:00",
"updated_at": "2026-06-18T00:31:33.1653132+08:00",
"messages": [
{
"role": "user",
"content": "[来自 !72d17be4] 你好"
},
{
"role": "assistant",
"content": "你好!😊 有什么可以帮你的吗?"
}
]
}
@@ -1,18 +0,0 @@
{
"id": "bot_1_1781713920523402600",
"bot_id": 1,
"bot_node_id": "!1abcead5",
"title": "[来自 !72d17be4] 你好",
"created_at": "2026-06-18T00:32:00.5234026+08:00",
"updated_at": "2026-06-18T00:32:04.2936935+08:00",
"messages": [
{
"role": "user",
"content": "[来自 !72d17be4] 你好"
},
{
"role": "assistant",
"content": "你好!很高兴见到你!😊\n\n我是DeepSeek,一个由深度求索公司创造的AI助手。无论你有什么问题、需要帮助,还是只是想聊聊天,我都很乐意陪伴你、\ufffd..."
}
]
}
@@ -1,18 +0,0 @@
{
"id": "bot_1_1781714154187832400",
"bot_id": 1,
"bot_node_id": "!1abcead5",
"title": "[来自 !72d17be4] 你好",
"created_at": "2026-06-18T00:35:54.1878324+08:00",
"updated_at": "2026-06-18T00:35:58.3371209+08:00",
"messages": [
{
"role": "user",
"content": "[来自 !72d17be4] 你好"
},
{
"role": "assistant",
"content": "你好!很高兴见到你!😊\n\n我是DeepSeek,由深度求索公司创造的AI助手。无论你有什么问题、需要帮助,还是只是想聊聊天,我都很乐意陪你一起探索\ufffd..."
}
]
}
@@ -1,18 +0,0 @@
{
"id": "bot_1_1781714169190665700",
"bot_id": 1,
"bot_node_id": "!1abcead5",
"title": "[来自 !72d17be4] 你好",
"created_at": "2026-06-18T00:36:09.1906657+08:00",
"updated_at": "2026-06-18T00:36:13.5432077+08:00",
"messages": [
{
"role": "user",
"content": "[来自 !72d17be4] 你好"
},
{
"role": "assistant",
"content": "你好!很高兴见到你。😊\n\n我是DeepSeek,由深度求索公司创造的AI助手。我可以帮你解答问题、处理文档、进行创作、分析数据……几乎任何文字相关\ufffd..."
}
]
}
@@ -1,18 +0,0 @@
{
"id": "bot_1_1781714179192265800",
"bot_id": 1,
"bot_node_id": "!1abcead5",
"title": "[来自 !72d17be4] 111",
"created_at": "2026-06-18T00:36:19.1922658+08:00",
"updated_at": "2026-06-18T00:36:23.4404859+08:00",
"messages": [
{
"role": "user",
"content": "[来自 !72d17be4] 111"
},
{
"role": "assistant",
"content": "你好!你发送了“111”,请问有什么需要帮助的吗?如果有具体问题或指令,请告诉我。"
}
]
}
@@ -1,42 +0,0 @@
{
"id": "bot_1_1781714224192405100",
"bot_id": 1,
"bot_node_id": "!1abcead5",
"title": "[来自 !72d17be4] 你好",
"created_at": "2026-06-18T00:37:04.1924051+08:00",
"updated_at": "2026-06-18T00:47:31.9941255+08:00",
"messages": [
{
"role": "user",
"content": "[来自 !72d17be4] 你好"
},
{
"role": "assistant",
"content": "你好!很高兴见到你!😊\n\n我收到了你的消息,不过看起来前面那个“!72d17be4”像是一个标识符或者系统生成的代码。如果你有什么具体问题或者需..."
},
{
"role": "user",
"content": "[来自 !72d17be4] 你好"
},
{
"role": "assistant",
"content": "你好!再次见到你很高兴!😊\n\n我看到你又一次带着“!72d17be4”这个标识出现了。这个标识似乎一直在跟随你的消息。如果你需要我帮你处理、解析..."
},
{
"role": "user",
"content": "[来自 !72d17be4] 你好"
},
{
"role": "assistant",
"content": "你好!😊\n\n我看到你第三次发来了相同的消息,并且每次前面都带着 **`!72d17be4`** 这个标识。\n\n如果你是在**测试**或者**重复发送**,我理解这可能是..."
},
{
"role": "user",
"content": "[来自 !72d17be4] 你好"
},
{
"role": "assistant",
"content": "你好! 我注意到你连续四次发送了相同格式的消息,前面都带有 `[来自 !72d17be4]` 标识。这可能是系统自动发送测试或者你希望引起我的注意。 无论..."
}
]
}