Files
nofx/agent/web.go
shinchan-zhai 9945e4e10d 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.
2026-03-25 01:05:54 +08:00

103 lines
2.8 KiB
Go

package agent
import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"time"
)
// WebHandler provides HTTP endpoints for the NOFXi agent.
type WebHandler struct {
agent *Agent
logger *slog.Logger
}
func NewWebHandler(agent *Agent, logger *slog.Logger) *WebHandler {
return &WebHandler{agent: agent, logger: logger}
}
// 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)})
}
// 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
}
ctx, cancel := context.WithTimeout(r.Context(), 55*time.Second)
defer cancel()
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})
}
// 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" }
proxyBinance(rw, fmt.Sprintf("https://fapi.binance.com/fapi/v1/klines?symbol=%s&interval=%s&limit=300", symbol, interval))
}
// 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" }
proxyBinance(rw, fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", symbol))
}
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{}) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}