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
This commit is contained in:
shinchan-zhai
2026-03-22 22:04:37 +08:00
parent cf7bf16c28
commit 34f5e6fe71
16 changed files with 784 additions and 87 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log/slog"
"time"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
@@ -65,7 +66,7 @@ func (t *TelegramBot) Start(ctx context.Context) error {
}
}
func (t *TelegramBot) handleUpdate(ctx context.Context, update tgbotapi.Update) {
func (t *TelegramBot) handleUpdate(parentCtx context.Context, update tgbotapi.Update) {
msg := update.Message
userID := msg.From.ID
@@ -91,13 +92,21 @@ func (t *TelegramBot) handleUpdate(ctx context.Context, update tgbotapi.Update)
typing := tgbotapi.NewChatAction(msg.Chat.ID, tgbotapi.ChatTyping)
t.bot.Send(typing)
// Process message
// 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)
response = fmt.Sprintf("⚠️ Error: %v", 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)
}

View File

@@ -6,23 +6,28 @@ import (
"fmt"
"log/slog"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
)
// WebServer provides a REST API for NOFXi.
// WebServer provides a REST API and Web UI for NOFXi.
type WebServer struct {
handler MessageHandler
port int
webDir string // Path to web/ directory for static files
logger *slog.Logger
server *http.Server
}
// NewWebServer creates a new web API server.
func NewWebServer(port int, handler MessageHandler, logger *slog.Logger) *WebServer {
// webDir is the path to the web/ directory containing index.html.
func NewWebServer(port int, handler MessageHandler, webDir string, logger *slog.Logger) *WebServer {
return &WebServer{
handler: handler,
port: port,
webDir: webDir,
logger: logger,
}
}
@@ -135,6 +140,14 @@ func (w *WebServer) Start(ctx context.Context) error {
})
})
// Serve web UI static files
if w.webDir != "" {
if _, err := os.Stat(filepath.Join(w.webDir, "index.html")); err == nil {
mux.Handle("/", http.FileServer(http.Dir(w.webDir)))
w.logger.Info("serving web UI", "dir", w.webDir)
}
}
addr := fmt.Sprintf(":%d", w.port)
w.server = &http.Server{
Addr: addr,