mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 04:50:57 +08:00
- Fix thinkAndActStream: emit delta event for non-tool responses (was silent) - Add fallback to streaming on first LLM call failure - Add batch ticker endpoint /api/agent/tickers (3 API calls → 1) - Update MarketTicker to use batch endpoint, remove unused state - Warn loudly when JWT_SECRET uses default value - Fix 华为 mapping (was incorrectly mapped to Tencent) - Replace ioutil.ReadAll with safe.ReadAllLimited in nofxos client - Remove unused variable in hyperliquid trader_account - Wrap CoinAnk WS readers with safe.GoNamed for panic recovery - Use 100dvh for mobile viewport in AgentChatPage - console.log → console.warn for auth events
223 lines
7.0 KiB
Go
223 lines
7.0 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/safe"
|
|
_ "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)
|
|
if cfg.JWTSecret == "default-jwt-secret-change-in-production" {
|
|
logger.Warn("⚠️ JWT_SECRET is using the default value — this is INSECURE for production!")
|
|
logger.Warn("⚠️ Set JWT_SECRET environment variable to a strong random secret")
|
|
} else {
|
|
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"
|
|
}
|
|
tid := t.ID
|
|
if len(tid) > 8 { tid = tid[:8] }
|
|
logger.Infof(" • %s [%s] %s - AI Model: %s, Exchange: %s",
|
|
t.Name, tid, 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)
|
|
|
|
safe.GoNamed("api-server", func() {
|
|
if err := server.Start(); err != nil {
|
|
logger.Fatalf("❌ Failed to start API server: %v", err)
|
|
}
|
|
})
|
|
|
|
// Start NOFXi Agent (proactive intelligence layer) — must be created BEFORE Telegram bot
|
|
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)
|
|
|
|
// Wire NOFXi Agent into Telegram bot — messages route through the unified agent
|
|
telegram.SetNOFXiAgent(nofxiAgent)
|
|
logger.Info("🔗 NOFXi Agent connected to Telegram bot")
|
|
|
|
// Start Telegram bot (if token is configured)
|
|
go telegram.Start(cfg, st, telegramReloadCh)
|
|
|
|
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...")
|
|
|
|
// Gracefully shutdown HTTP server (drain in-flight requests)
|
|
if err := server.Shutdown(); err != nil {
|
|
logger.Warnf("⚠️ HTTP server shutdown error: %v", err)
|
|
}
|
|
logger.Info("✅ HTTP server stopped")
|
|
|
|
// 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)
|
|
}
|