mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 13:00:59 +08:00
Key changes:
- Agent API routes (/api/agent/*) registered directly on NOFX's :8080 server
- No more separate :8900 port — everything on :8080
- AgentChatPage.tsx added to React frontend
- Agent tab (🤖 Agent) added to navigation bar, set as DEFAULT page
- Branding: NOFX → NOFXi everywhere (title, header, branding.ts, i18n)
- Default landing page is now Agent chat, not competition
This is THE change: there is no more 'NOFX' vs 'NOFXi'.
Users open localhost:8080, they see NOFXi.
Agent chat is the first thing they see.
All other pages (Traders, Dashboard, Strategy, Data) are still there.
One binary. One port. One product. NOFXi.
205 lines
6.4 KiB
Go
205 lines
6.4 KiB
Go
package main
|
|
|
|
import (
|
|
"log/slog"
|
|
"nofx/api"
|
|
nofxiagent "nofx/agent"
|
|
"nofx/auth"
|
|
"nofx/config"
|
|
"nofx/crypto"
|
|
"nofx/telemetry"
|
|
"nofx/logger"
|
|
"nofx/manager"
|
|
_ "nofx/mcp/payment"
|
|
_ "nofx/mcp/provider"
|
|
"nofx/store"
|
|
"nofx/telegram"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"syscall"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/joho/godotenv"
|
|
)
|
|
|
|
func main() {
|
|
// Load .env environment variables
|
|
_ = godotenv.Load()
|
|
|
|
// Initialize logger
|
|
logger.Init(nil)
|
|
|
|
logger.Info("╔════════════════════════════════════════════════════════════╗")
|
|
logger.Info("║ 🚀 NOFXi - AI Trading Agent ║")
|
|
logger.Info("║ Powered by NOFX Engine ║")
|
|
logger.Info("╚════════════════════════════════════════════════════════════╝")
|
|
|
|
// Initialize global configuration (loaded from .env)
|
|
config.Init()
|
|
cfg := config.Get()
|
|
logger.Info("✅ Configuration loaded")
|
|
|
|
// Initialize encryption service BEFORE database (so EncryptedString can decrypt on read)
|
|
logger.Info("🔐 Initializing encryption service...")
|
|
cryptoService, err := crypto.NewCryptoService()
|
|
if err != nil {
|
|
logger.Fatalf("❌ Failed to initialize encryption service: %v", err)
|
|
}
|
|
crypto.SetGlobalCryptoService(cryptoService)
|
|
logger.Info("✅ Encryption service initialized successfully")
|
|
|
|
// Initialize database from configuration
|
|
// For backward compatibility: command line arg overrides config (SQLite only)
|
|
if len(os.Args) > 1 {
|
|
cfg.DBPath = os.Args[1]
|
|
}
|
|
// Ensure data directory exists (for SQLite)
|
|
if cfg.DBType == "sqlite" {
|
|
if dir := filepath.Dir(cfg.DBPath); dir != "." {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
logger.Errorf("Failed to create data directory: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.Infof("📋 Initializing database (%s)...", cfg.DBType)
|
|
dbType := store.DBTypeSQLite
|
|
if cfg.DBType == "postgres" {
|
|
dbType = store.DBTypePostgres
|
|
}
|
|
st, err := store.NewWithConfig(store.DBConfig{
|
|
Type: dbType,
|
|
Path: cfg.DBPath,
|
|
Host: cfg.DBHost,
|
|
Port: cfg.DBPort,
|
|
User: cfg.DBUser,
|
|
Password: cfg.DBPassword,
|
|
DBName: cfg.DBName,
|
|
SSLMode: cfg.DBSSLMode,
|
|
})
|
|
if err != nil {
|
|
logger.Fatalf("❌ Failed to initialize database: %v", err)
|
|
}
|
|
defer st.Close()
|
|
|
|
// Initialize installation ID for experience improvement (anonymous statistics)
|
|
initInstallationID(st)
|
|
|
|
// Set JWT secret
|
|
auth.SetJWTSecret(cfg.JWTSecret)
|
|
logger.Info("🔑 JWT secret configured")
|
|
|
|
// WebSocket market monitor is NO LONGER USED
|
|
// All K-line data now comes from CoinAnk API instead of Binance WebSocket cache
|
|
// Commented out to reduce unnecessary connections:
|
|
// go market.NewWSMonitor(150).Start(nil)
|
|
// logger.Info("📊 WebSocket market monitor started")
|
|
// time.Sleep(500 * time.Millisecond)
|
|
logger.Info("📊 Using CoinAnk API for all market data (WebSocket cache disabled)")
|
|
|
|
// Create TraderManager
|
|
traderManager := manager.NewTraderManager()
|
|
|
|
// Load all traders from database to memory (may auto-start traders with IsRunning=true)
|
|
if err := traderManager.LoadTradersFromStore(st); err != nil {
|
|
logger.Fatalf("❌ Failed to load traders: %v", err)
|
|
}
|
|
|
|
// Display loaded trader information
|
|
traders, err := st.Trader().List("default")
|
|
if err != nil {
|
|
logger.Fatalf("❌ Failed to get trader list: %v", err)
|
|
}
|
|
|
|
logger.Info("🤖 AI Trader Configurations in Database:")
|
|
if len(traders) == 0 {
|
|
logger.Info(" (No trader configurations, please create via Web interface)")
|
|
} else {
|
|
for _, t := range traders {
|
|
status := "❌ Stopped"
|
|
if t.IsRunning {
|
|
status = "✅ Running"
|
|
}
|
|
logger.Infof(" • %s [%s] %s - AI Model: %s, Exchange: %s",
|
|
t.Name, t.ID[:8], status, t.AIModelID, t.ExchangeID)
|
|
}
|
|
}
|
|
|
|
// Start API server
|
|
server := api.NewServer(traderManager, st, cryptoService, cfg.APIServerPort)
|
|
|
|
// Create hot-reload channel for Telegram bot; wire it to the API server
|
|
// so that POST /api/telegram can trigger a bot restart when the token changes.
|
|
telegramReloadCh := make(chan struct{}, 1)
|
|
server.SetTelegramReloadCh(telegramReloadCh)
|
|
|
|
go func() {
|
|
if err := server.Start(); err != nil {
|
|
logger.Fatalf("❌ Failed to start API server: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Start Telegram bot (if TELEGRAM_BOT_TOKEN is configured)
|
|
go telegram.Start(cfg, st, telegramReloadCh)
|
|
|
|
// Start NOFXi Agent (proactive intelligence layer)
|
|
nofxiAgent := nofxiagent.New(traderManager, st, nil, slog.Default())
|
|
|
|
// Try to get an AI client from existing trader configs
|
|
for _, t := range traderManager.GetAllTraders() {
|
|
if mcpClient := t.GetMCPClient(); mcpClient != nil {
|
|
nofxiAgent.SetAIClient(mcpClient)
|
|
logger.Info("🧠 NOFXi Agent using AI model from trader", "trader", t.GetName())
|
|
break
|
|
}
|
|
}
|
|
|
|
nofxiAgent.Start()
|
|
defer nofxiAgent.Stop()
|
|
|
|
// Register NOFXi Agent API on the main server's router
|
|
agentWeb := nofxiagent.NewWebHandler(nofxiAgent, slog.Default())
|
|
server.RegisterAgentHandler(agentWeb)
|
|
|
|
logger.Info("🧠 NOFXi Agent started (sentinel + brain + scheduler + web:8900)")
|
|
|
|
// Wait for interrupt signal
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
logger.Info("✅ System started successfully, waiting for trading commands...")
|
|
logger.Info("📌 Tip: Use Ctrl+C to stop the system")
|
|
|
|
<-quit
|
|
logger.Info("📴 Shutdown signal received, closing system...")
|
|
|
|
// Stop all traders
|
|
traderManager.StopAll()
|
|
logger.Info("✅ System shut down safely")
|
|
}
|
|
|
|
// initInstallationID initializes the anonymous installation ID for experience improvement
|
|
// This ID is persisted in database and used for anonymous usage statistics
|
|
func initInstallationID(st *store.Store) {
|
|
const key = "installation_id"
|
|
|
|
// Try to load from database
|
|
installationID, err := st.GetSystemConfig(key)
|
|
if err != nil {
|
|
logger.Warnf("⚠️ Failed to load installation ID: %v", err)
|
|
}
|
|
|
|
// Generate new ID if not exists
|
|
if installationID == "" {
|
|
installationID = uuid.New().String()
|
|
if err := st.SetSystemConfig(key, installationID); err != nil {
|
|
logger.Warnf("⚠️ Failed to save installation ID: %v", err)
|
|
}
|
|
logger.Infof("📊 Generated new installation ID: %s", installationID[:8]+"...")
|
|
}
|
|
|
|
// Set installation ID in experience module
|
|
telemetry.SetInstallationID(installationID)
|
|
}
|