mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 13:00:59 +08:00
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
138 lines
3.4 KiB
Go
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
|
|
}
|