Files
nofx/nofxi/internal/interaction/telegram.go
shinchan-zhai 34f5e6fe71 feat(nofxi): Phase 3 complete - Exchange factory, Web UI, Strategy runner, Docker
Exchange Factory:
- CreateTrader() supports Binance/OKX/Bybit/Bitget/KuCoin/Gate
- Auto-registers traders from config on startup
- Direct import of nofx/trader packages (merged into main module)

Web UI:
- Dark theme chat interface at :8900
- Quick action sidebar (analyze, watch, positions, balance)
- Real-time health check indicator
- Mobile responsive

Strategy Runner:
- /strategy start BTC 1h - AI auto-analyzes on interval
- /strategy stop <id> - stop strategy
- /strategy list - view active strategies
- Notifications pushed to Telegram on signals
- Configurable intervals: 15m/30m/1h/4h

Docker:
- Multi-stage Dockerfile (alpine, ~20MB)
- docker-compose.yml with volume persistence

Bug fixes:
- Fixed panic on empty exchanges config
- Fixed thinking mode response parsing (qwen3 content:null)
- Added request timeout (55s) in Telegram handler
- Better error logging
2026-03-22 22:04:37 +08:00

138 lines
3.4 KiB
Go

package interaction
import (
"context"
"fmt"
"log/slog"
"time"
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(parentCtx 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 with timeout
ctx, cancel := context.WithTimeout(parentCtx, 55*time.Second)
defer cancel()
response, err := t.handler(ctx, userID, text)
if err != nil {
t.logger.Error("handle message", "error", err)
if ctx.Err() != nil {
response = "⏱️ AI 响应超时,请稍后再试。"
} else {
response = fmt.Sprintf("⚠️ Error: %v", err)
}
}
t.logger.Info("sending response", "user_id", userID, "len", len(response))
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
}