mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 04:50:57 +08:00
Critical fixes: 1. Agent now uses NOFX's MCP client for AI calls (real models, not placeholder) - Gets AI client from first configured trader's MCP config - Added AutoTrader.GetMCPClient() export 2. /analyze uses REAL market data (market.Get → EMA/MACD/RSI/BB/OI/funding) then sends to AI — no more hallucinated prices 3. AI chat enriches prompts with live market data when user mentions a coin 4. Fallback: if AI unavailable, shows raw formatted market data (still useful) Router improvements: - Better natural language detection for Chinese - '该不该买' '走势' '趋势' '行情' → analyze intent - '多少钱' '赚了' '亏了' → query intent - Fewer messages falling through to generic chat Web UI: - Endpoints updated to /api/agent/* namespace - nofxi.html served at agent root (localhost:8900) Result: AI analysis now shows real BTC price, real RSI, real EMA — not fiction.
161 lines
4.4 KiB
Go
161 lines
4.4 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"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()
|
|
|
|
// 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)})
|
|
})
|
|
|
|
// 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
|
|
}
|
|
|
|
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})
|
|
})
|
|
|
|
// Kline data
|
|
mux.HandleFunc("/api/agent/klines", handleKlines)
|
|
|
|
// Ticker
|
|
mux.HandleFunc("/api/agent/ticker", handleTicker)
|
|
|
|
// 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 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)
|
|
}
|
|
|
|
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 }
|
|
}
|
|
}
|