From 4996db81d42f53df2260e90bd07a1a31b63a4b14 Mon Sep 17 00:00:00 2001 From: shinchan-zhai Date: Sun, 22 Mar 2026 22:32:38 +0800 Subject: [PATCH] feat(nofxi): full i18n bilingual support (zh/en) Agent i18n: - All command responses bilingual (help, status, positions, balance, trades, analysis, settings, errors) - Language auto-detected from web UI lang toggle - Stored per-user in SQLite preferences - AI chat system prompt switches to Chinese/English based on user lang - Trade confirmation prompts bilingual Web UI: - Sends lang parameter with every API request - Agent extracts [lang:xx] prefix and persists preference i18n message catalog: nofxi/internal/agent/i18n.go --- nofxi/internal/agent/agent.go | 456 ++++++++++-------------------- nofxi/internal/agent/i18n.go | 204 +++++++++++++ nofxi/internal/interaction/web.go | 8 +- nofxi/web/index.html | 2 +- 4 files changed, 355 insertions(+), 315 deletions(-) create mode 100644 nofxi/internal/agent/i18n.go diff --git a/nofxi/internal/agent/agent.go b/nofxi/internal/agent/agent.go index 50cd05bd..8ffddc28 100644 --- a/nofxi/internal/agent/agent.go +++ b/nofxi/internal/agent/agent.go @@ -1,12 +1,4 @@ // Package agent implements the NOFXi Agent Core. -// -// The Agent is the central orchestrator that: -// 1. Receives user messages (via Interaction layer) -// 2. Routes intents (trade, query, analyze, chat) -// 3. Uses Thinking layer for AI decisions -// 4. Stores context in Memory layer -// 5. Executes trades via Execution bridge (→ NOFX engine) -// 6. Monitors markets via Perception layer package agent import ( @@ -25,314 +17,235 @@ import ( // Agent is the NOFXi agent core. type Agent struct { - config *Config - memory *memory.Store - thinker thinking.Engine - bridge *execution.Bridge - monitor *perception.MarketMonitor - logger *slog.Logger - - // NotifyFunc sends proactive notifications to users (set by interaction layer) - NotifyFunc func(userID int64, text string) error - - // Strategy runner + config *Config + memory *memory.Store + thinker thinking.Engine + bridge *execution.Bridge + monitor *perception.MarketMonitor + logger *slog.Logger + NotifyFunc func(userID int64, text string) error strategyRunner *StrategyRunner } -// New creates a new Agent with the given config. func New(cfg *Config, mem *memory.Store, thinker thinking.Engine, logger *slog.Logger) *Agent { - a := &Agent{ - config: cfg, - memory: mem, - thinker: thinker, - logger: logger, - } + a := &Agent{config: cfg, memory: mem, thinker: thinker, logger: logger} a.strategyRunner = NewStrategyRunner(a, logger) return a } -// SetBridge attaches the execution bridge. -func (a *Agent) SetBridge(bridge *execution.Bridge) { - a.bridge = bridge -} +func (a *Agent) SetBridge(b *execution.Bridge) { a.bridge = b } +func (a *Agent) SetMonitor(m *perception.MarketMonitor) { a.monitor = m } -// SetMonitor attaches the market monitor and wires up alert notifications. -func (a *Agent) SetMonitor(monitor *perception.MarketMonitor) { - a.monitor = monitor +func (a *Agent) getLang(userID int64) string { + l, _ := a.memory.GetPreference(userID, "lang") + if l == "" { + l = a.config.Agent.Language + } + if l != "zh" && l != "en" { + l = "en" + } + return l } // HandleMessage processes a user message and returns a response. func (a *Agent) HandleMessage(ctx context.Context, userID int64, text string) (string, error) { - a.logger.Info("incoming message", "user_id", userID, "text", text) - - // Save user message to memory - if err := a.memory.SaveMessage(userID, "user", text); err != nil { - a.logger.Error("save user message", "error", err) + // Extract lang prefix from web UI + if strings.HasPrefix(text, "[lang:") { + if end := strings.Index(text, "] "); end > 0 { + lang := text[6:end] + if lang == "zh" || lang == "en" { + a.memory.SetPreference(userID, "lang", lang) + } + text = text[end+2:] + } } - // Route intent + a.logger.Info("incoming message", "user_id", userID, "text", text) + a.memory.SaveMessage(userID, "user", text) + intent := Route(text) a.logger.Info("routed intent", "type", intent.Type, "params", intent.Params) - var response string + L := a.getLang(userID) + var resp string var err error switch intent.Type { case IntentHelp: - response = a.handleHelp() + resp = msg(L, "help") case IntentStatus: - response = a.handleStatus() + resp = a.handleStatus(L) case IntentQuery: - response, err = a.handleQuery(ctx, intent) + resp, err = a.handleQuery(ctx, L, intent) case IntentAnalyze: - response, err = a.handleAnalyze(ctx, intent) + resp, err = a.handleAnalyze(ctx, L, intent) case IntentTrade: - response, err = a.handleTrade(ctx, userID, intent) + resp, err = a.handleTrade(ctx, userID, L, intent) case IntentWatch: - response = a.HandleWatchCommand(intent.Raw) + resp = a.HandleWatchCommand(intent.Raw) case IntentStrategy: - response = a.handleStrategyCommand(intent.Raw) + resp = a.handleStrategyCommand(intent.Raw) case IntentSettings: - response = a.handleSettings(intent) + resp = fmt.Sprintf(msg(L, "settings"), L, a.config.LLM.Model, a.config.LLM.Provider, len(a.config.Exchanges)) default: - response, err = a.handleChat(ctx, userID, text) + resp, err = a.handleChat(ctx, userID, L, text) } if err != nil { a.logger.Error("handle message", "intent", intent.Type, "error", err) - response = fmt.Sprintf("⚠️ Error: %v", err) + resp = fmt.Sprintf("⚠️ Error: %v", err) } - // Save assistant response to memory - if err := a.memory.SaveMessage(userID, "assistant", response); err != nil { - a.logger.Error("save assistant message", "error", err) - } - - return response, nil + a.memory.SaveMessage(userID, "assistant", resp) + return resp, nil } -func (a *Agent) handleHelp() string { - return `🤖 *NOFXi — Your AI Trading Agent* - -*Trading:* -/buy BTC 0.01 — Open long (market order) -/sell BTC 0.01 — Open short -/close BTC — Close position -/positions — View open positions -/balance — Check balance -/pnl — Profit & Loss history - -*Analysis:* -/analyze BTC — AI market analysis -/watch BTC — Monitor price -/alert BTC above 100000 — Price alert - -*System:* -/status — Agent status -/settings — Preferences -/help — This menu - -*Natural Language:* -• "帮我做多 ETH,2x 杠杆,0.1 个" -• "分析一下 BTC 走势" -• "现在持仓情况怎样" -• "BTC 到 10 万提醒我" - -Just talk to me 💬` -} - -func (a *Agent) handleStatus() string { - // Market monitor status - watchCount := 0 +func (a *Agent) handleStatus(L string) string { + wc := 0 if a.monitor != nil { - watchCount = len(a.monitor.GetAllSnapshots()) + wc = len(a.monitor.GetAllSnapshots()) } - bridgeStatus := "❌ Not connected" + bs := msg(L, "bridge_disconnected") if a.bridge != nil { - bridgeStatus = "✅ Connected" + bs = msg(L, "bridge_connected") } - - return fmt.Sprintf(`📊 *NOFXi Status* - -• Agent: %s -• Model: %s -• Provider: %s -• Memory: ✅ Online -• Execution: %s -• Watching: %d symbols -• Time: %s`, - a.config.Agent.Name, - a.config.LLM.Model, - a.config.LLM.Provider, - bridgeStatus, - watchCount, - time.Now().Format("2006-01-02 15:04:05"), - ) + return fmt.Sprintf(msg(L, "status_title"), + a.config.Agent.Name, a.config.LLM.Model, a.config.LLM.Provider, + bs, wc, time.Now().Format("2006-01-02 15:04:05")) } -func (a *Agent) handleQuery(ctx context.Context, intent Intent) (string, error) { +func (a *Agent) handleQuery(ctx context.Context, L string, intent Intent) (string, error) { raw := strings.ToLower(intent.Raw) - // Try live positions from exchange first if a.bridge != nil && (strings.Contains(raw, "position") || strings.Contains(raw, "持仓")) { - return a.queryPositions() + return a.queryPositions(L) } if a.bridge != nil && (strings.Contains(raw, "balance") || strings.Contains(raw, "余额")) { - return a.queryBalance() + return a.queryBalance(L) } - // Fall back to trade history from memory trades, err := a.memory.GetRecentTrades(10) if err != nil { return "", fmt.Errorf("get trades: %w", err) } - if len(trades) == 0 { - return "📭 No trades yet. Start with `/buy BTC 0.01` or ask me to `/analyze BTC`.", nil + return msg(L, "no_trades"), nil } var sb strings.Builder - sb.WriteString("📋 *Recent Trades*\n\n") - totalPnL := 0.0 + sb.WriteString(msg(L, "recent_trades")) + total := 0.0 for _, t := range trades { - emoji := "🟢" + e := "🟢" if t.PnL < 0 { - emoji = "🔴" + e = "🔴" } sb.WriteString(fmt.Sprintf("%s %s %s %s — $%.2f (P/L: $%.2f)\n", - emoji, t.Side, t.Symbol, t.Exchange, t.Price*t.Quantity, t.PnL)) - totalPnL += t.PnL + e, t.Side, t.Symbol, t.Exchange, t.Price*t.Quantity, t.PnL)) + total += t.PnL } - sb.WriteString(fmt.Sprintf("\n💰 Total P/L: $%.2f", totalPnL)) + sb.WriteString(fmt.Sprintf(msg(L, "total_pnl"), total)) return sb.String(), nil } -func (a *Agent) queryPositions() (string, error) { - var allPositions []execution.Position +func (a *Agent) queryPositions(L string) (string, error) { + var all []execution.Position for _, ex := range a.config.Exchanges { - positions, err := a.bridge.GetPositions(ex.Name) + pos, err := a.bridge.GetPositions(ex.Name) if err != nil { - a.logger.Error("get positions", "exchange", ex.Name, "error", err) continue } - allPositions = append(allPositions, positions...) + all = append(all, pos...) } - - if len(allPositions) == 0 { - return "📭 No open positions.", nil + if len(all) == 0 { + return msg(L, "no_positions"), nil } var sb strings.Builder - sb.WriteString("📊 *Open Positions*\n\n") - totalPnL := 0.0 - for _, p := range allPositions { - emoji := "🟢" + sb.WriteString(msg(L, "open_positions")) + total := 0.0 + for _, p := range all { + e := "🟢" if p.PnL < 0 { - emoji = "🔴" + e = "🔴" } - sb.WriteString(fmt.Sprintf("%s *%s* %s\n", emoji, p.Symbol, strings.ToUpper(p.Side))) - sb.WriteString(fmt.Sprintf(" Size: %.4f | Entry: $%.2f\n", p.Size, p.EntryPrice)) - sb.WriteString(fmt.Sprintf(" Mark: $%.2f | P/L: $%.2f\n", p.MarkPrice, p.PnL)) + sb.WriteString(fmt.Sprintf("%s *%s* %s\n Size: %.4f | Entry: $%.2f\n Mark: $%.2f | P/L: $%.2f\n", + e, p.Symbol, strings.ToUpper(p.Side), p.Size, p.EntryPrice, p.MarkPrice, p.PnL)) if p.Leverage > 0 { sb.WriteString(fmt.Sprintf(" Leverage: %.0fx | Exchange: %s\n", p.Leverage, p.Exchange)) } sb.WriteString("\n") - totalPnL += p.PnL + total += p.PnL } - sb.WriteString(fmt.Sprintf("💰 *Total Unrealized P/L: $%.2f*", totalPnL)) + sb.WriteString(fmt.Sprintf(msg(L, "total_unrealized"), total)) return sb.String(), nil } -func (a *Agent) queryBalance() (string, error) { +func (a *Agent) queryBalance(L string) (string, error) { var sb strings.Builder - sb.WriteString("💰 *Account Balance*\n\n") - + sb.WriteString(msg(L, "account_balance")) for _, ex := range a.config.Exchanges { bal, err := a.bridge.GetBalance(ex.Name) if err != nil { - a.logger.Error("get balance", "exchange", ex.Name, "error", err) sb.WriteString(fmt.Sprintf("• %s: ⚠️ Error\n", ex.Name)) continue } - sb.WriteString(fmt.Sprintf("*%s*\n", strings.Title(ex.Name))) - sb.WriteString(fmt.Sprintf(" Total: $%.2f\n", bal.Total)) - sb.WriteString(fmt.Sprintf(" Available: $%.2f\n", bal.Available)) - sb.WriteString(fmt.Sprintf(" In Position: $%.2f\n\n", bal.InPosition)) + sb.WriteString(fmt.Sprintf("*%s*\n", ex.Name)) + sb.WriteString(fmt.Sprintf(msg(L, "balance_total"), bal.Total)) + sb.WriteString(fmt.Sprintf(msg(L, "balance_available"), bal.Available)) + sb.WriteString(fmt.Sprintf(msg(L, "balance_in_position"), bal.InPosition)) } return sb.String(), nil } -func (a *Agent) handleAnalyze(ctx context.Context, intent Intent) (string, error) { +func (a *Agent) handleAnalyze(ctx context.Context, L string, intent Intent) (string, error) { symbol := "BTC" - if detail, ok := intent.Params["detail"]; ok && detail != "" { - symbol = strings.ToUpper(strings.TrimSpace(detail)) + if d, ok := intent.Params["detail"]; ok && d != "" { + symbol = strings.ToUpper(strings.TrimSpace(d)) } - // Add live price if available priceInfo := "" if a.monitor != nil { if snap, ok := a.monitor.GetSnapshot(symbol + "USDT"); ok && snap.LastPrice > 0 { priceInfo = fmt.Sprintf("\nCurrent price: $%.2f", snap.LastPrice) } } - if priceInfo == "" && a.bridge != nil { - for _, ex := range a.config.Exchanges { - if price, err := a.bridge.GetPrice(ex.Name, symbol+"USDT"); err == nil && price > 0 { - priceInfo = fmt.Sprintf("\nCurrent price: $%.2f (from %s)", price, ex.Name) - break - } - } - } - prompt := fmt.Sprintf( - "Analyze %s/USDT for trading. %s\n"+ - "Consider: trend, support/resistance, momentum indicators, volume, sentiment.\n"+ - "Give specific entry/exit levels and stop loss. Be concise.", - symbol, priceInfo) + prompt := fmt.Sprintf("Analyze %s/USDT for trading. %s\nConsider: trend, support/resistance, momentum, volume, sentiment.\nGive specific entry/exit levels and stop loss. Be concise. Respond in %s.", + symbol, priceInfo, map[string]string{"zh": "Chinese", "en": "English"}[L]) analysis, err := a.thinker.Analyze(ctx, prompt) if err != nil { return "", fmt.Errorf("AI analyze: %w", err) } - emoji := map[string]string{ - "buy": "🟢 BUY", - "sell": "🔴 SELL", - "hold": "🟡 HOLD", - "wait": "⏳ WAIT", - } - - action := emoji[analysis.Action] + emojiMap := map[string]string{"buy": "🟢 BUY", "sell": "🔴 SELL", "hold": "🟡 HOLD", "wait": "⏳ WAIT"} + action := emojiMap[analysis.Action] if action == "" { action = "🤔 " + analysis.Action } - result := fmt.Sprintf("🔍 *%s/USDT Analysis*\n\nSignal: %s\nConfidence: %.0f%%\n\n%s", - symbol, action, analysis.Confidence*100, analysis.Reasoning) - + result := fmt.Sprintf(msg(L, "analysis_signal"), symbol, action, analysis.Confidence*100, analysis.Reasoning) if analysis.StopLoss > 0 { - result += fmt.Sprintf("\n\n🛑 Stop Loss: $%.2f", analysis.StopLoss) + result += fmt.Sprintf(msg(L, "stop_loss"), analysis.StopLoss) } if analysis.TakeProfit > 0 { - result += fmt.Sprintf("\n🎯 Take Profit: $%.2f", analysis.TakeProfit) + result += fmt.Sprintf(msg(L, "take_profit"), analysis.TakeProfit) } - return result, nil } -func (a *Agent) handleTrade(ctx context.Context, userID int64, intent Intent) (string, error) { +func (a *Agent) handleTrade(ctx context.Context, userID int64, L string, intent Intent) (string, error) { action := strings.ToLower(intent.Params["action"]) detail := intent.Params["detail"] - if a.bridge == nil { - return fmt.Sprintf("⚡ *Trade: %s %s*\n\n🔧 No exchange connected. Configure exchanges in config.yaml first.", - strings.ToUpper(action), detail), nil + if a.bridge == nil || len(a.config.Exchanges) == 0 { + return msg(L, "no_exchange"), nil } - // Parse: "BTC 0.01" or "BTCUSDT 0.01 2x" parts := strings.Fields(detail) if len(parts) < 1 { - return "❓ Usage: `/buy BTC 0.01` or `/sell ETH 0.5 3x`", nil + return msg(L, "trade_usage"), nil } symbol := strings.ToUpper(parts[0]) @@ -345,75 +258,53 @@ func (a *Agent) handleTrade(ctx context.Context, userID int64, intent Intent) (s if len(parts) >= 2 { q, err := strconv.ParseFloat(parts[1], 64) if err != nil { - return fmt.Sprintf("❓ Invalid quantity: %s", parts[1]), nil + return fmt.Sprintf(msg(L, "invalid_quantity"), parts[1]), nil } quantity = q } if len(parts) >= 3 { - levStr := strings.TrimSuffix(strings.ToLower(parts[2]), "x") - l, err := strconv.Atoi(levStr) - if err == nil { + if l, err := strconv.Atoi(strings.TrimSuffix(strings.ToLower(parts[2]), "x")); err == nil { leverage = l } } - if quantity <= 0 && (action == "buy" || action == "sell" || action == "long" || action == "short") { - return "❓ Please specify quantity: `/buy BTC 0.01`", nil + if quantity <= 0 { + return msg(L, "specify_quantity"), nil } - // Map action to execution side var side string switch action { - case "buy", "long", "open_long": + case "buy", "long", "open_long", "做多": side = "LONG" - case "sell", "short", "open_short": + case "sell", "short", "open_short", "做空": side = "SHORT" - case "close": - side = "CLOSE_LONG" // TODO: detect which side to close + case "close", "平仓": + side = "CLOSE_LONG" default: side = strings.ToUpper(action) } - // Use first configured exchange - if len(a.config.Exchanges) == 0 { - return "⚠️ 还没有配置交易所。请在 config.yaml 的 exchanges 中添加交易所 API Key。", nil - } exchange := a.config.Exchanges[0].Name - - // Confirm with user before executing - confirmMsg := fmt.Sprintf("⚡ *Confirm Trade*\n\n"+ - "• Action: %s\n"+ - "• Symbol: %s\n"+ - "• Quantity: %.6f\n"+ - "• Leverage: %dx\n"+ - "• Exchange: %s\n\n"+ - "Reply 'yes' to execute.", - side, symbol, quantity, leverage, exchange) - - // Store pending trade for confirmation a.memory.SetPreference(userID, "pending_trade", fmt.Sprintf("%s|%s|%f|%d|%s", side, symbol, quantity, leverage, exchange)) - return confirmMsg, nil + return fmt.Sprintf(msg(L, "confirm_trade"), side, symbol, quantity, leverage, exchange), nil } -// ExecutePendingTrade executes a trade that was previously confirmed. -func (a *Agent) ExecutePendingTrade(ctx context.Context, userID int64) (string, error) { +// ExecutePendingTrade executes a confirmed trade. +func (a *Agent) ExecutePendingTrade(ctx context.Context, userID int64, L string) (string, error) { pending, err := a.memory.GetPreference(userID, "pending_trade") if err != nil || pending == "" { - return "", fmt.Errorf("no pending trade") + return "", fmt.Errorf(msg(L, "no_pending")) } - - // Clear pending a.memory.SetPreference(userID, "pending_trade", "") parts := strings.Split(pending, "|") if len(parts) != 5 { - return "", fmt.Errorf("invalid pending trade data") + return "", fmt.Errorf("invalid pending trade") } - side := parts[0] - symbol := parts[1] + side, symbol := parts[0], parts[1] quantity, _ := strconv.ParseFloat(parts[2], 64) leverage, _ := strconv.Atoi(parts[3]) exchange := parts[4] @@ -423,27 +314,13 @@ func (a *Agent) ExecutePendingTrade(ctx context.Context, userID int64) (string, return "", fmt.Errorf("execute trade: %w", err) } - // Save trade to memory - tradeRecord := &memory.TradeRecord{ - Exchange: exchange, - Symbol: symbol, - Side: strings.ToLower(side), - Type: "market", - Quantity: quantity, - Status: "open", + tr := &memory.TradeRecord{Exchange: exchange, Symbol: symbol, Side: strings.ToLower(side), Type: "market", Quantity: quantity, Status: "open"} + if p, ok := result["avgPrice"].(float64); ok { + tr.Price = p } - if price, ok := result["avgPrice"].(float64); ok { - tradeRecord.Price = price - } - a.memory.SaveTrade(tradeRecord) + a.memory.SaveTrade(tr) - return fmt.Sprintf("✅ *Trade Executed!*\n\n"+ - "• %s %s\n"+ - "• Qty: %.6f\n"+ - "• Leverage: %dx\n"+ - "• Exchange: %s\n"+ - "• Result: %v", - side, symbol, quantity, leverage, exchange, result), nil + return fmt.Sprintf(msg(L, "trade_executed"), side, symbol, quantity, leverage, exchange, result), nil } func (a *Agent) handleStrategyCommand(text string) string { @@ -451,43 +328,37 @@ func (a *Agent) handleStrategyCommand(text string) string { if len(parts) < 2 { return a.strategyRunner.FormatStrategyList() } - - subcmd := strings.ToLower(parts[1]) - switch subcmd { + switch strings.ToLower(parts[1]) { case "list": return a.strategyRunner.FormatStrategyList() case "start": if len(parts) < 3 { - return "Usage: `/strategy start BTC 1h` or `/strategy start ETH 4h binance`" + return "Usage: `/strategy start BTC 1h`" } - symbol := strings.ToUpper(parts[2]) - if !strings.HasSuffix(symbol, "USDT") { - symbol += "USDT" + sym := strings.ToUpper(parts[2]) + if !strings.HasSuffix(sym, "USDT") { + sym += "USDT" } - interval := 1 * time.Hour + iv := 1 * time.Hour if len(parts) >= 4 { switch parts[3] { case "15m": - interval = 15 * time.Minute + iv = 15 * time.Minute case "30m": - interval = 30 * time.Minute - case "1h": - interval = 1 * time.Hour + iv = 30 * time.Minute case "4h": - interval = 4 * time.Hour + iv = 4 * time.Hour } } - exchange := "binance" + ex := "binance" if len(parts) >= 5 { - exchange = parts[4] + ex = parts[4] } - name := fmt.Sprintf("AI-%s", symbol) - id, err := a.strategyRunner.StartStrategy(name, symbol, exchange, interval) + id, err := a.strategyRunner.StartStrategy("AI-"+sym, sym, ex, iv) if err != nil { return fmt.Sprintf("⚠️ %v", err) } - return fmt.Sprintf("🚀 Strategy started!\n\n• ID: `%s`\n• Symbol: %s\n• Interval: %s\n• Exchange: %s\n\nStop with: `/strategy stop %s`", - id, symbol, interval, exchange, id) + return fmt.Sprintf("🚀 Strategy started!\n\n• ID: `%s`\n• Symbol: %s\n• Interval: %s\n• Exchange: %s", id, sym, iv, ex) case "stop": if len(parts) < 3 { return "Usage: `/strategy stop `" @@ -500,67 +371,26 @@ func (a *Agent) handleStrategyCommand(text string) string { a.strategyRunner.StopAll() return "✅ All strategies stopped." default: - return "Unknown subcommand. Use: `/strategy list|start|stop|stopall`" + return "Use: `/strategy list|start|stop|stopall`" } } -func (a *Agent) handleSettings(intent Intent) string { - return `⚙️ *Settings* - -• Language: ` + a.config.Agent.Language + ` -• Model: ` + a.config.LLM.Model + ` -• Provider: ` + a.config.LLM.Provider + ` -• Exchanges: ` + fmt.Sprintf("%d configured", len(a.config.Exchanges)) -} - -func (a *Agent) handleChat(ctx context.Context, userID int64, text string) (string, error) { - // Check for trade confirmation +func (a *Agent) handleChat(ctx context.Context, userID int64, L string, text string) (string, error) { lower := strings.ToLower(text) if lower == "yes" || lower == "y" || lower == "确认" || lower == "是" { - pending, _ := a.memory.GetPreference(userID, "pending_trade") - if pending != "" { - return a.ExecutePendingTrade(ctx, userID) + if p, _ := a.memory.GetPreference(userID, "pending_trade"); p != "" { + return a.ExecutePendingTrade(ctx, userID, L) } } - // Get conversation history for context - history, err := a.memory.GetRecentMessages(userID, 20) - if err != nil { - a.logger.Error("get history", "error", err) + history, _ := a.memory.GetRecentMessages(userID, 20) + + sysPrompt := fmt.Sprintf(msg(L, "system_prompt"), time.Now().Format("2006-01-02 15:04:05")) + msgs := []thinking.Message{{Role: "system", Content: sysPrompt}} + for _, m := range history { + msgs = append(msgs, thinking.Message{Role: m.Role, Content: m.Content}) } + msgs = append(msgs, thinking.Message{Role: "user", Content: text}) - // Build messages with system prompt - messages := []thinking.Message{ - { - Role: "system", - Content: fmt.Sprintf(`You are NOFXi, an AI trading agent built on NOFX. - -Your capabilities: -- Market analysis and trading recommendations -- Real-time position and balance monitoring -- Trade execution (open/close positions on exchanges) -- Price alerts and market monitoring -- Risk management advice - -You support multiple exchanges: Binance, OKX, Bybit, Bitget, KuCoin, Gate, Hyperliquid, etc. - -Be concise, confident, and action-oriented. Use trading emojis. -Respond in the same language the user uses. -Current time: %s`, time.Now().Format("2006-01-02 15:04:05")), - }, - } - - for _, msg := range history { - messages = append(messages, thinking.Message{ - Role: msg.Role, - Content: msg.Content, - }) - } - - messages = append(messages, thinking.Message{ - Role: "user", - Content: text, - }) - - return a.thinker.Chat(ctx, messages) + return a.thinker.Chat(ctx, msgs) } diff --git a/nofxi/internal/agent/i18n.go b/nofxi/internal/agent/i18n.go new file mode 100644 index 00000000..76167e7a --- /dev/null +++ b/nofxi/internal/agent/i18n.go @@ -0,0 +1,204 @@ +package agent + +// i18n message templates +var messages = map[string]map[string]string{ + "help": { + "zh": `🤖 *NOFXi — 你的 AI 交易 Agent* + +*交易:* +/buy BTC 0.01 — 做多(市价单) +/sell BTC 0.01 — 做空 +/close BTC — 平仓 +/positions — 查看持仓 +/balance — 查看余额 +/pnl — 盈亏记录 + +*分析:* +/analyze BTC — AI 市场分析 +/watch BTC — 监控价格 +/alert BTC above 100000 — 价格提醒 +/price BTC — 实时价格 + +*策略:* +/strategy start BTC 1h — 启动 AI 自动策略 +/strategy list — 查看运行中的策略 +/strategy stop — 停止策略 + +*系统:* +/status — Agent 状态 +/help — 帮助菜单 + +直接跟我说话就行,中英文都可以 💬`, + + "en": `🤖 *NOFXi — Your AI Trading Agent* + +*Trading:* +/buy BTC 0.01 — Open long (market order) +/sell BTC 0.01 — Open short +/close BTC — Close position +/positions — View open positions +/balance — Check balance +/pnl — P/L history + +*Analysis:* +/analyze BTC — AI market analysis +/watch BTC — Monitor price +/alert BTC above 100000 — Price alert +/price BTC — Real-time price + +*Strategy:* +/strategy start BTC 1h — Start AI auto strategy +/strategy list — View active strategies +/strategy stop — Stop strategy + +*System:* +/status — Agent status +/help — This menu + +Just talk to me in any language 💬`, + }, + + "no_trades": { + "zh": "📭 暂无交易记录。试试 `/buy BTC 0.01` 或 `/analyze BTC` 开始吧。", + "en": "📭 No trades yet. Start with `/buy BTC 0.01` or `/analyze BTC`.", + }, + "no_positions": { + "zh": "📭 当前没有持仓。", + "en": "📭 No open positions.", + }, + "recent_trades": { + "zh": "📋 *最近交易*\n\n", + "en": "📋 *Recent Trades*\n\n", + }, + "total_pnl": { + "zh": "\n💰 总盈亏: $%.2f", + "en": "\n💰 Total P/L: $%.2f", + }, + "open_positions": { + "zh": "📊 *当前持仓*\n\n", + "en": "📊 *Open Positions*\n\n", + }, + "total_unrealized": { + "zh": "💰 *未实现总盈亏: $%.2f*", + "en": "💰 *Total Unrealized P/L: $%.2f*", + }, + "account_balance": { + "zh": "💰 *账户余额*\n\n", + "en": "💰 *Account Balance*\n\n", + }, + "balance_total": { + "zh": " 总额: $%.2f\n", + "en": " Total: $%.2f\n", + }, + "balance_available": { + "zh": " 可用: $%.2f\n", + "en": " Available: $%.2f\n", + }, + "balance_in_position": { + "zh": " 持仓占用: $%.2f\n\n", + "en": " In Position: $%.2f\n\n", + }, + "no_exchange": { + "zh": "⚠️ 还没有配置交易所。请在 config.yaml 的 exchanges 中添加交易所 API Key。", + "en": "⚠️ No exchange configured. Add exchange API keys in config.yaml.", + }, + "trade_usage": { + "zh": "❓ 用法: `/buy BTC 0.01` 或 `/sell ETH 0.5 3x`", + "en": "❓ Usage: `/buy BTC 0.01` or `/sell ETH 0.5 3x`", + }, + "invalid_quantity": { + "zh": "❓ 无效数量: %s", + "en": "❓ Invalid quantity: %s", + }, + "specify_quantity": { + "zh": "❓ 请指定数量: `/buy BTC 0.01`", + "en": "❓ Please specify quantity: `/buy BTC 0.01`", + }, + "confirm_trade": { + "zh": "⚡ *确认交易*\n\n• 操作: %s\n• 交易对: %s\n• 数量: %.6f\n• 杠杆: %dx\n• 交易所: %s\n\n回复 '确认' 执行交易。", + "en": "⚡ *Confirm Trade*\n\n• Action: %s\n• Symbol: %s\n• Quantity: %.6f\n• Leverage: %dx\n• Exchange: %s\n\nReply 'yes' to execute.", + }, + "trade_executed": { + "zh": "✅ *交易已执行!*\n\n• %s %s\n• 数量: %.6f\n• 杠杆: %dx\n• 交易所: %s\n• 结果: %v", + "en": "✅ *Trade Executed!*\n\n• %s %s\n• Qty: %.6f\n• Leverage: %dx\n• Exchange: %s\n• Result: %v", + }, + "no_pending": { + "zh": "没有待确认的交易", + "en": "no pending trade", + }, + "analysis_signal": { + "zh": "🔍 *%s/USDT 分析*\n\n信号: %s\n置信度: %.0f%%\n\n%s", + "en": "🔍 *%s/USDT Analysis*\n\nSignal: %s\nConfidence: %.0f%%\n\n%s", + }, + "stop_loss": { + "zh": "\n\n🛑 止损: $%.2f", + "en": "\n\n🛑 Stop Loss: $%.2f", + }, + "take_profit": { + "zh": "\n🎯 止盈: $%.2f", + "en": "\n🎯 Take Profit: $%.2f", + }, + "settings": { + "zh": "⚙️ *设置*\n\n• 语言: %s\n• 模型: %s\n• 提供商: %s\n• 交易所: %d 个已配置", + "en": "⚙️ *Settings*\n\n• Language: %s\n• Model: %s\n• Provider: %s\n• Exchanges: %d configured", + }, + "status_title": { + "zh": "📊 *NOFXi 状态*\n\n• Agent: %s\n• 模型: %s\n• 提供商: %s\n• 记忆: ✅ 在线\n• 执行: %s\n• 监控: %d 个交易对\n• 时间: %s", + "en": "📊 *NOFXi Status*\n\n• Agent: %s\n• Model: %s\n• Provider: %s\n• Memory: ✅ Online\n• Execution: %s\n• Watching: %d symbols\n• Time: %s", + }, + "bridge_connected": { + "zh": "✅ 已连接", + "en": "✅ Connected", + }, + "bridge_disconnected": { + "zh": "❌ 未连接", + "en": "❌ Not connected", + }, + "ai_timeout": { + "zh": "⏱️ AI 响应超时,请稍后再试。", + "en": "⏱️ AI response timed out, please try again.", + }, + "system_prompt": { + "zh": `你是 NOFXi,一个基于 NOFX 构建的 AI 交易 Agent。 + +你的能力: +- 市场分析和交易建议 +- 实时持仓和余额监控 +- 交易执行(开仓/平仓) +- 价格提醒和行情监控 +- 风险管理建议 + +支持多家交易所:Binance、OKX、Bybit、Bitget、KuCoin、Gate、Hyperliquid 等。 + +简洁、自信、专注交易。使用交易相关的 emoji。 +用中文回复。 +当前时间: %s`, + "en": `You are NOFXi, an AI trading agent built on NOFX. + +Your capabilities: +- Market analysis and trading recommendations +- Real-time position and balance monitoring +- Trade execution (open/close positions) +- Price alerts and market monitoring +- Risk management advice + +Supports multiple exchanges: Binance, OKX, Bybit, Bitget, KuCoin, Gate, Hyperliquid, etc. + +Be concise, confident, and action-oriented. Use trading emojis. +Respond in English. +Current time: %s`, + }, +} + +// msg returns the localized message for the given key and language. +func msg(lang, key string) string { + if m, ok := messages[key]; ok { + if s, ok := m[lang]; ok { + return s + } + if s, ok := m["en"]; ok { + return s + } + } + return key +} diff --git a/nofxi/internal/interaction/web.go b/nofxi/internal/interaction/web.go index 17b1ca5c..b95349dc 100644 --- a/nofxi/internal/interaction/web.go +++ b/nofxi/internal/interaction/web.go @@ -36,6 +36,7 @@ func NewWebServer(port int, handler MessageHandler, webDir string, logger *slog. type chatRequest struct { UserID int64 `json:"user_id"` Message string `json:"message"` + Lang string `json:"lang"` // "zh" or "en" } // chatResponse is the API response body. @@ -78,7 +79,12 @@ func (w *WebServer) Start(ctx context.Context) error { req.UserID = 1 // Default user for API access } - resp, err := w.handler(r.Context(), req.UserID, req.Message) + // Pass language preference via message prefix (agent will extract it) + message := req.Message + if req.Lang != "" { + message = "[lang:" + req.Lang + "] " + message + } + resp, err := w.handler(r.Context(), req.UserID, message) if err != nil { writeJSON(rw, http.StatusInternalServerError, chatAPIResponse{Error: err.Error()}) return diff --git a/nofxi/web/index.html b/nofxi/web/index.html index 4bdf3a4e..114eefa5 100644 --- a/nofxi/web/index.html +++ b/nofxi/web/index.html @@ -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})}); + 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 d=await r.json(); addM('bot',d.response||d.error||'No response'); }catch(e){addM('bot','⚠️ Error: '+e.message)}