mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-11 23:07:01 +08:00
feat: unified NOFXi — one system, one port, one UI
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.
This commit is contained in:
188
agent/web.go
188
agent/web.go
@@ -4,104 +4,94 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WebHandler provides HTTP endpoints for the NOFXi agent.
|
||||
// These are registered on the existing NOFX API server.
|
||||
type WebHandler struct {
|
||||
agent *Agent
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewWebHandler creates a new web handler.
|
||||
func NewWebHandler(agent *Agent, logger *slog.Logger) *WebHandler {
|
||||
return &WebHandler{agent: agent, logger: logger}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers agent API routes on an existing mux or gin router.
|
||||
// For now we use a standalone http server on a separate port.
|
||||
func (w *WebHandler) StartStandalone(port int) {
|
||||
mux := http.NewServeMux()
|
||||
// HandleHealth handles GET /api/agent/health.
|
||||
func (w *WebHandler) HandleHealth(rw http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(rw, 200, map[string]string{"status": "ok", "agent": "NOFXi", "time": time.Now().Format(time.RFC3339)})
|
||||
}
|
||||
|
||||
// Health
|
||||
mux.HandleFunc("/api/agent/health", func(rw http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(rw, 200, map[string]string{"status": "ok", "agent": "NOFXi", "time": time.Now().Format(time.RFC3339)})
|
||||
})
|
||||
// HandleChat handles POST /api/agent/chat.
|
||||
func (w *WebHandler) HandleChat(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", 405)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Message string `json:"message"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Lang string `json:"lang"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(rw, 400, map[string]string{"error": "invalid request"})
|
||||
return
|
||||
}
|
||||
if req.Message == "" {
|
||||
writeJSON(rw, 400, map[string]string{"error": "message required"})
|
||||
return
|
||||
}
|
||||
if req.UserID == 0 {
|
||||
req.UserID = 1
|
||||
}
|
||||
msg := req.Message
|
||||
if req.Lang != "" {
|
||||
msg = "[lang:" + req.Lang + "] " + msg
|
||||
}
|
||||
|
||||
// Chat endpoint
|
||||
mux.HandleFunc("/api/agent/chat", func(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "method not allowed", 405)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Message string `json:"message"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Lang string `json:"lang"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(rw, 400, map[string]string{"error": "invalid request"})
|
||||
return
|
||||
}
|
||||
if req.Message == "" {
|
||||
writeJSON(rw, 400, map[string]string{"error": "message required"})
|
||||
return
|
||||
}
|
||||
if req.UserID == 0 {
|
||||
req.UserID = 1
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 55*time.Second)
|
||||
defer cancel()
|
||||
|
||||
msg := req.Message
|
||||
if req.Lang != "" {
|
||||
msg = "[lang:" + req.Lang + "] " + msg
|
||||
}
|
||||
resp, err := w.agent.HandleMessage(ctx, req.UserID, msg)
|
||||
if err != nil {
|
||||
writeJSON(rw, 500, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(rw, 200, map[string]string{"response": resp})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 55*time.Second)
|
||||
defer cancel()
|
||||
// HandleKlines proxies kline data from Binance.
|
||||
func (w *WebHandler) HandleKlines(rw http.ResponseWriter, r *http.Request) {
|
||||
symbol := r.URL.Query().Get("symbol")
|
||||
if symbol == "" { symbol = "BTCUSDT" }
|
||||
interval := r.URL.Query().Get("interval")
|
||||
if interval == "" { interval = "1h" }
|
||||
|
||||
resp, err := w.agent.HandleMessage(ctx, req.UserID, msg)
|
||||
if err != nil {
|
||||
writeJSON(rw, 500, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(rw, 200, map[string]string{"response": resp})
|
||||
})
|
||||
proxyBinance(rw, fmt.Sprintf("https://fapi.binance.com/fapi/v1/klines?symbol=%s&interval=%s&limit=300", symbol, interval))
|
||||
}
|
||||
|
||||
// Kline data
|
||||
mux.HandleFunc("/api/agent/klines", handleKlines)
|
||||
// HandleTicker proxies ticker data from Binance.
|
||||
func (w *WebHandler) HandleTicker(rw http.ResponseWriter, r *http.Request) {
|
||||
symbol := r.URL.Query().Get("symbol")
|
||||
if symbol == "" { symbol = "BTCUSDT" }
|
||||
|
||||
// Ticker
|
||||
mux.HandleFunc("/api/agent/ticker", handleTicker)
|
||||
proxyBinance(rw, fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", symbol))
|
||||
}
|
||||
|
||||
// Serve nofxi.html as the root page
|
||||
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(rw, r)
|
||||
return
|
||||
}
|
||||
// Try web/nofxi.html relative to executable, then working dir
|
||||
paths := []string{"web/nofxi.html", "../web/nofxi.html"}
|
||||
for _, p := range paths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
http.ServeFile(rw, r, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
rw.Header().Set("Content-Type", "text/html")
|
||||
rw.Write([]byte("<h1>NOFXi Agent</h1><p>API is running. Web UI not found.</p>"))
|
||||
})
|
||||
|
||||
go func() {
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
w.logger.Info("NOFXi agent web starting", "port", port, "url", fmt.Sprintf("http://localhost:%d", port))
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
w.logger.Error("agent web server error", "error", err)
|
||||
}
|
||||
}()
|
||||
func proxyBinance(rw http.ResponseWriter, url string) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
writeJSON(rw, 502, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
rw.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
io.Copy(rw, resp.Body)
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
||||
@@ -110,51 +100,3 @@ func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func handleKlines(w http.ResponseWriter, r *http.Request) {
|
||||
symbol := r.URL.Query().Get("symbol")
|
||||
if symbol == "" { symbol = "BTCUSDT" }
|
||||
interval := r.URL.Query().Get("interval")
|
||||
if interval == "" { interval = "1h" }
|
||||
|
||||
url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/klines?symbol=%s&interval=%s&limit=300", symbol, interval)
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
writeJSON(w, 502, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.WriteHeader(200)
|
||||
buf := make([]byte, 32768)
|
||||
for {
|
||||
n, err := resp.Body.Read(buf)
|
||||
if n > 0 { w.Write(buf[:n]) }
|
||||
if err != nil { break }
|
||||
}
|
||||
}
|
||||
|
||||
func handleTicker(w http.ResponseWriter, r *http.Request) {
|
||||
symbol := r.URL.Query().Get("symbol")
|
||||
if symbol == "" { symbol = "BTCUSDT" }
|
||||
|
||||
url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", symbol)
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
writeJSON(w, 502, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.WriteHeader(200)
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := resp.Body.Read(buf)
|
||||
if n > 0 { w.Write(buf[:n]) }
|
||||
if err != nil { break }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user