mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 17:34:39 +08:00
feat: init nofxi - AI Trading Agent module
NOFXi — Your AI Trading Agent, built on NOFX. Architecture: - Agent Core: intent routing, conversation memory, trade confirmation - Memory: SQLite (trades, conversations, preferences, strategies) - Thinking: LLM client (OpenAI/claw402/DeepSeek compatible) - Execution: Bridge to NOFX trader engine (9 exchanges) - Perception: Market monitor, price alerts, anomaly detection - Interaction: Telegram bot + REST API + OpenAI-compatible endpoint Features: - Natural language trading (中英文) - /buy /sell /analyze /watch /alert /positions /balance - Daily P/L reports, portfolio risk checks - Trade confirmation flow (safety first) - Price polling and alert notifications
This commit is contained in:
128
nofxi/internal/interaction/telegram.go
Normal file
128
nofxi/internal/interaction/telegram.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package interaction
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
// MessageHandler is called for each incoming message.
|
||||
type MessageHandler func(ctx context.Context, userID int64, text string) (string, error)
|
||||
|
||||
// TelegramBot wraps the Telegram Bot API.
|
||||
type TelegramBot struct {
|
||||
bot *tgbotapi.BotAPI
|
||||
handler MessageHandler
|
||||
allowedIDs map[int64]bool // Empty = allow all
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewTelegramBot creates a new Telegram bot.
|
||||
func NewTelegramBot(token string, allowedIDs []int64, handler MessageHandler, logger *slog.Logger) (*TelegramBot, error) {
|
||||
bot, err := tgbotapi.NewBotAPI(token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create bot: %w", err)
|
||||
}
|
||||
|
||||
allowed := make(map[int64]bool)
|
||||
for _, id := range allowedIDs {
|
||||
allowed[id] = true
|
||||
}
|
||||
|
||||
logger.Info("telegram bot authorized", "username", bot.Self.UserName)
|
||||
|
||||
return &TelegramBot{
|
||||
bot: bot,
|
||||
handler: handler,
|
||||
allowedIDs: allowed,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start begins polling for updates. Blocks until context is cancelled.
|
||||
func (t *TelegramBot) Start(ctx context.Context) error {
|
||||
u := tgbotapi.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
|
||||
updates := t.bot.GetUpdatesChan(u)
|
||||
|
||||
t.logger.Info("telegram bot started, listening for messages...")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.bot.StopReceivingUpdates()
|
||||
t.logger.Info("telegram bot stopped")
|
||||
return nil
|
||||
case update := <-updates:
|
||||
if update.Message == nil {
|
||||
continue
|
||||
}
|
||||
go t.handleUpdate(ctx, update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TelegramBot) handleUpdate(ctx context.Context, update tgbotapi.Update) {
|
||||
msg := update.Message
|
||||
userID := msg.From.ID
|
||||
|
||||
// Access control
|
||||
if len(t.allowedIDs) > 0 && !t.allowedIDs[userID] {
|
||||
t.logger.Warn("unauthorized user", "user_id", userID, "username", msg.From.UserName)
|
||||
t.sendText(msg.Chat.ID, "⛔ Unauthorized. Contact the admin to get access.")
|
||||
return
|
||||
}
|
||||
|
||||
text := msg.Text
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
|
||||
t.logger.Info("received message",
|
||||
"user_id", userID,
|
||||
"username", msg.From.UserName,
|
||||
"text", text,
|
||||
)
|
||||
|
||||
// Send "typing" indicator
|
||||
typing := tgbotapi.NewChatAction(msg.Chat.ID, tgbotapi.ChatTyping)
|
||||
t.bot.Send(typing)
|
||||
|
||||
// Process message
|
||||
response, err := t.handler(ctx, userID, text)
|
||||
if err != nil {
|
||||
t.logger.Error("handle message", "error", err)
|
||||
response = fmt.Sprintf("⚠️ Error: %v", err)
|
||||
}
|
||||
|
||||
t.sendMarkdown(msg.Chat.ID, response)
|
||||
}
|
||||
|
||||
func (t *TelegramBot) sendText(chatID int64, text string) {
|
||||
msg := tgbotapi.NewMessage(chatID, text)
|
||||
if _, err := t.bot.Send(msg); err != nil {
|
||||
t.logger.Error("send message", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TelegramBot) sendMarkdown(chatID int64, text string) {
|
||||
msg := tgbotapi.NewMessage(chatID, text)
|
||||
msg.ParseMode = tgbotapi.ModeMarkdown
|
||||
|
||||
if _, err := t.bot.Send(msg); err != nil {
|
||||
// Retry without markdown if parsing fails
|
||||
t.logger.Warn("markdown send failed, retrying plain", "error", err)
|
||||
t.sendText(chatID, text)
|
||||
}
|
||||
}
|
||||
|
||||
// SendNotification sends a proactive message to a user.
|
||||
func (t *TelegramBot) SendNotification(userID int64, text string) error {
|
||||
msg := tgbotapi.NewMessage(userID, text)
|
||||
msg.ParseMode = tgbotapi.ModeMarkdown
|
||||
_, err := t.bot.Send(msg)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user