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
This commit is contained in:
shinchan-zhai
2026-03-22 22:32:38 +08:00
parent 7cd6a6f7b3
commit 4996db81d4
4 changed files with 355 additions and 315 deletions

View File

@@ -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:*
• "帮我做多 ETH2x 杠杆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 <id>`"
@@ -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)
}

View File

@@ -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 <id> — 停止策略
*系统:*
/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 <id> — 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
}

View File

@@ -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

View File

@@ -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)}