diff --git a/agent/agent.go b/agent/agent.go index 6986c107..bade29d0 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -21,6 +21,7 @@ import ( "nofx/manager" "nofx/market" + "nofx/mcp" "nofx/store" ) @@ -29,6 +30,7 @@ type Agent struct { // NOFX core (injected) traderManager *manager.TraderManager store *store.Store + aiClient mcp.AIClient // Agent components config *Config @@ -88,6 +90,11 @@ func New( return a } +// SetAIClient sets the MCP AI client for LLM calls. +func (a *Agent) SetAIClient(c mcp.AIClient) { + a.aiClient = c +} + // Start starts all agent services. @@ -339,8 +346,18 @@ func (a *Agent) handleAnalyze(ctx context.Context, L string, intent Intent) (str // Build rich analysis prompt with real data prompt := buildAnalysisPrompt(symbol, md, L) - // For now, format the raw market data as analysis - return fmt.Sprintf(a.msg(L, "analysis_header"), strings.TrimSuffix(symbol, "USDT")) + "\n\n" + prompt, nil + // Use AI to analyze the real market data + if a.aiClient != nil { + resp, err := a.aiClient.CallWithMessages(a.msg(L, "system_prompt"), prompt) + if err == nil && resp != "" { + return fmt.Sprintf(a.msg(L, "analysis_header"), strings.TrimSuffix(symbol, "USDT")) + "\n\n" + resp, nil + } + a.logger.Error("AI analysis failed", "error", err) + } + + // Fallback: format raw data directly + header := fmt.Sprintf(a.msg(L, "analysis_header"), strings.TrimSuffix(symbol, "USDT")) + return header + "\n\n" + market.Format(md), nil } func buildAnalysisPrompt(symbol string, md *market.Data, L string) string { @@ -477,12 +494,38 @@ func (a *Agent) handleStrategyCmd(L string, intent Intent) string { } func (a *Agent) handleChat(ctx context.Context, L string, userID int64, text string) (string, error) { - // TODO: integrate with NOFX's MCP client for AI chat - // For now, route to the existing Telegram agent or return guidance - if L == "zh" { - return "🤖 收到。AI 对话功能正在接入 NOFX 的 AI 引擎。\n\n你可以试试:\n• `/analyze BTC` — 市场分析\n• `/positions` — 查看持仓\n• `/status` — 系统状态", nil + if a.aiClient == nil { + if L == "zh" { + return "🤖 AI 模型未配置。请在 Web UI (:8080) 中创建 Trader 并配置 AI 模型。\n\n命令仍然可用:\n• `/analyze BTC` — 市场分析\n• `/positions` — 查看持仓\n• `/status` — 系统状态", nil + } + return "🤖 AI model not configured. Create a Trader in Web UI (:8080) first.\n\nCommands still work:\n• `/analyze BTC`\n• `/positions`\n• `/status`", nil } - return "🤖 Got it. AI chat is being integrated with NOFX's AI engine.\n\nTry:\n• `/analyze BTC` — market analysis\n• `/positions` — view positions\n• `/status` — system status", nil + + systemPrompt := a.msg(L, "system_prompt") + + // If user seems to be asking about a specific coin, enrich with real data + enrichment := "" + for _, sym := range []string{"BTC", "ETH", "SOL", "BNB", "XRP", "DOGE"} { + if strings.Contains(strings.ToUpper(text), sym) { + md, err := market.Get(sym + "USDT") + if err == nil { + enrichment += fmt.Sprintf("\n\n[Real-time %s data]\nPrice: $%.4f | 1h: %.2f%% | 4h: %.2f%% | RSI7: %.1f | EMA20: %.4f | MACD: %.6f | Funding: %.4f%%", + sym, md.CurrentPrice, md.PriceChange1h, md.PriceChange4h, md.CurrentRSI7, md.CurrentEMA20, md.CurrentMACD, md.FundingRate*100) + } + break + } + } + + userPrompt := text + if enrichment != "" { + userPrompt = text + enrichment + } + + resp, err := a.aiClient.CallWithMessages(systemPrompt, userPrompt) + if err != nil { + return "", fmt.Errorf("AI: %w", err) + } + return resp, nil } func (a *Agent) handleSignal(sig Signal) { diff --git a/agent/router.go b/agent/router.go index e463cec3..d1dcf235 100644 --- a/agent/router.go +++ b/agent/router.go @@ -38,11 +38,14 @@ func NewRouter() *Router { regexp.MustCompile(`(?i)(做多|做空|买入|卖出|开仓|平仓)\s*(.+)`), }, queryPatterns: []*regexp.Regexp{ - regexp.MustCompile(`(?i)(position|balance|pnl|profit|loss|持仓|余额|盈亏|trader|交易员)`), + regexp.MustCompile(`(?i)(position|balance|pnl|profit|loss|持仓|余额|盈亏|trader|交易员|账户|仓位|资金)`), + regexp.MustCompile(`(?i)(多少钱|赚了|亏了|收益|回报)`), }, analyzePatterns: []*regexp.Regexp{ - regexp.MustCompile(`(?i)(analyze|analysis|分析|看看)\s+(.+)`), - regexp.MustCompile(`(?i)(what.*think|怎么看|你觉得)\s*(.+)?`), + regexp.MustCompile(`(?i)(analyze|analysis|分析|看看|研究)\s+(.+)`), + regexp.MustCompile(`(?i)(what.*think|怎么看|你觉得|走势|趋势|行情)\s*(.+)?`), + regexp.MustCompile(`(?i)(该不该|适合|能不能|要不要).*(买|卖|做多|做空|入场|开仓).*`), + regexp.MustCompile(`(?i)(should\s+i|is\s+it\s+good).*(buy|sell|long|short).*`), }, } } diff --git a/agent/web.go b/agent/web.go index af6b853d..9eefb898 100644 --- a/agent/web.go +++ b/agent/web.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "net/http" + "os" "time" ) @@ -76,9 +77,27 @@ func (w *WebHandler) StartStandalone(port int) { // 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("

NOFXi Agent

API is running. Web UI not found.

")) + }) + go func() { addr := fmt.Sprintf(":%d", port) - w.logger.Info("NOFXi agent web API starting", "port", 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) } diff --git a/main.go b/main.go index 8ed526b2..16a4a4bd 100644 --- a/main.go +++ b/main.go @@ -145,6 +145,16 @@ func main() { // 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() diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 5874af02..030012e2 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -588,6 +588,11 @@ func (at *AutoTrader) GetStore() *store.Store { return at.store } +// GetMCPClient returns the AI client for NOFXi agent integration. +func (at *AutoTrader) GetMCPClient() mcp.AIClient { + return at.mcpClient +} + // calculatePnLPercentage calculates P&L percentage (based on margin, automatically considers leverage) // Return rate = Unrealized P&L / Margin x 100% func calculatePnLPercentage(unrealizedPnl, marginUsed float64) float64 { diff --git a/web/nofxi.html b/web/nofxi.html index a34b1fb6..b1427c65 100644 --- a/web/nofxi.html +++ b/web/nofxi.html @@ -291,7 +291,7 @@ checkSt(); setInterval(checkSt,30000); // === Ticker Info Bar === async function loadInfoBar(){ try{ - const r=await fetch(API+'/api/ticker?symbol=BTCUSDT'); + const r=await fetch(API+'/api/agent/ticker?symbol=BTCUSDT'); const d=await r.json(); if(!d.lastPrice) return; const bar=document.getElementById('infoBar'); @@ -337,7 +337,7 @@ async function send(text){ addM('user',text); typEl.classList.add('on');sBtnEl.disabled=true;inpEl.focus(); try{ - const r=await fetch(API+'/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:text,user_id:1,lang:lang})}); + const r=await fetch(API+'/api/agent/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:text,user_id:1,lang:lang})}); const d=await r.json(); addM('bot',d.response||d.error||'No response'); }catch(e){addM('bot','⚠️ Error: '+e.message)} @@ -380,7 +380,7 @@ function setTF(tf){ async function loadChart(){ const sym=document.getElementById('symSel').value; try{ - const[kr,tr]=await Promise.all([fetch(`${API}/api/klines?symbol=${sym}&interval=${curTF}&limit=300`),fetch(`${API}/api/ticker?symbol=${sym}`)]); + const[kr,tr]=await Promise.all([fetch(`${API}/api/agent/klines?symbol=${sym}&interval=${curTF}&limit=300`),fetch(`${API}/api/agent/ticker?symbol=${sym}`)]); const klines=await kr.json(), ticker=await tr.json(); if(klines?.length){ cSeries.setData(klines.map(k=>({time:k.time,open:k.open,high:k.high,low:k.low,close:k.close})));