mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 09:24:36 +08:00
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:
@@ -1,12 +1,4 @@
|
|||||||
// Package agent implements the NOFXi Agent Core.
|
// 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
|
package agent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -25,314 +17,235 @@ import (
|
|||||||
|
|
||||||
// Agent is the NOFXi agent core.
|
// Agent is the NOFXi agent core.
|
||||||
type Agent struct {
|
type Agent struct {
|
||||||
config *Config
|
config *Config
|
||||||
memory *memory.Store
|
memory *memory.Store
|
||||||
thinker thinking.Engine
|
thinker thinking.Engine
|
||||||
bridge *execution.Bridge
|
bridge *execution.Bridge
|
||||||
monitor *perception.MarketMonitor
|
monitor *perception.MarketMonitor
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
NotifyFunc func(userID int64, text string) error
|
||||||
// NotifyFunc sends proactive notifications to users (set by interaction layer)
|
|
||||||
NotifyFunc func(userID int64, text string) error
|
|
||||||
|
|
||||||
// Strategy runner
|
|
||||||
strategyRunner *StrategyRunner
|
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 {
|
func New(cfg *Config, mem *memory.Store, thinker thinking.Engine, logger *slog.Logger) *Agent {
|
||||||
a := &Agent{
|
a := &Agent{config: cfg, memory: mem, thinker: thinker, logger: logger}
|
||||||
config: cfg,
|
|
||||||
memory: mem,
|
|
||||||
thinker: thinker,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
a.strategyRunner = NewStrategyRunner(a, logger)
|
a.strategyRunner = NewStrategyRunner(a, logger)
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetBridge attaches the execution bridge.
|
func (a *Agent) SetBridge(b *execution.Bridge) { a.bridge = b }
|
||||||
func (a *Agent) SetBridge(bridge *execution.Bridge) {
|
func (a *Agent) SetMonitor(m *perception.MarketMonitor) { a.monitor = m }
|
||||||
a.bridge = bridge
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMonitor attaches the market monitor and wires up alert notifications.
|
func (a *Agent) getLang(userID int64) string {
|
||||||
func (a *Agent) SetMonitor(monitor *perception.MarketMonitor) {
|
l, _ := a.memory.GetPreference(userID, "lang")
|
||||||
a.monitor = monitor
|
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.
|
// HandleMessage processes a user message and returns a response.
|
||||||
func (a *Agent) HandleMessage(ctx context.Context, userID int64, text string) (string, error) {
|
func (a *Agent) HandleMessage(ctx context.Context, userID int64, text string) (string, error) {
|
||||||
a.logger.Info("incoming message", "user_id", userID, "text", text)
|
// Extract lang prefix from web UI
|
||||||
|
if strings.HasPrefix(text, "[lang:") {
|
||||||
// Save user message to memory
|
if end := strings.Index(text, "] "); end > 0 {
|
||||||
if err := a.memory.SaveMessage(userID, "user", text); err != nil {
|
lang := text[6:end]
|
||||||
a.logger.Error("save user message", "error", err)
|
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)
|
intent := Route(text)
|
||||||
a.logger.Info("routed intent", "type", intent.Type, "params", intent.Params)
|
a.logger.Info("routed intent", "type", intent.Type, "params", intent.Params)
|
||||||
|
|
||||||
var response string
|
L := a.getLang(userID)
|
||||||
|
var resp string
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
switch intent.Type {
|
switch intent.Type {
|
||||||
case IntentHelp:
|
case IntentHelp:
|
||||||
response = a.handleHelp()
|
resp = msg(L, "help")
|
||||||
case IntentStatus:
|
case IntentStatus:
|
||||||
response = a.handleStatus()
|
resp = a.handleStatus(L)
|
||||||
case IntentQuery:
|
case IntentQuery:
|
||||||
response, err = a.handleQuery(ctx, intent)
|
resp, err = a.handleQuery(ctx, L, intent)
|
||||||
case IntentAnalyze:
|
case IntentAnalyze:
|
||||||
response, err = a.handleAnalyze(ctx, intent)
|
resp, err = a.handleAnalyze(ctx, L, intent)
|
||||||
case IntentTrade:
|
case IntentTrade:
|
||||||
response, err = a.handleTrade(ctx, userID, intent)
|
resp, err = a.handleTrade(ctx, userID, L, intent)
|
||||||
case IntentWatch:
|
case IntentWatch:
|
||||||
response = a.HandleWatchCommand(intent.Raw)
|
resp = a.HandleWatchCommand(intent.Raw)
|
||||||
case IntentStrategy:
|
case IntentStrategy:
|
||||||
response = a.handleStrategyCommand(intent.Raw)
|
resp = a.handleStrategyCommand(intent.Raw)
|
||||||
case IntentSettings:
|
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:
|
default:
|
||||||
response, err = a.handleChat(ctx, userID, text)
|
resp, err = a.handleChat(ctx, userID, L, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("handle message", "intent", intent.Type, "error", err)
|
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
|
a.memory.SaveMessage(userID, "assistant", resp)
|
||||||
if err := a.memory.SaveMessage(userID, "assistant", response); err != nil {
|
return resp, nil
|
||||||
a.logger.Error("save assistant message", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) handleHelp() string {
|
func (a *Agent) handleStatus(L string) string {
|
||||||
return `🤖 *NOFXi — Your AI Trading Agent*
|
wc := 0
|
||||||
|
|
||||||
*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
|
|
||||||
if a.monitor != nil {
|
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 {
|
if a.bridge != nil {
|
||||||
bridgeStatus = "✅ Connected"
|
bs = msg(L, "bridge_connected")
|
||||||
}
|
}
|
||||||
|
return fmt.Sprintf(msg(L, "status_title"),
|
||||||
return fmt.Sprintf(`📊 *NOFXi Status*
|
a.config.Agent.Name, a.config.LLM.Model, a.config.LLM.Provider,
|
||||||
|
bs, wc, time.Now().Format("2006-01-02 15:04:05"))
|
||||||
• 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"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
raw := strings.ToLower(intent.Raw)
|
||||||
|
|
||||||
// Try live positions from exchange first
|
|
||||||
if a.bridge != nil && (strings.Contains(raw, "position") || strings.Contains(raw, "持仓")) {
|
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, "余额")) {
|
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)
|
trades, err := a.memory.GetRecentTrades(10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("get trades: %w", err)
|
return "", fmt.Errorf("get trades: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(trades) == 0 {
|
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
|
var sb strings.Builder
|
||||||
sb.WriteString("📋 *Recent Trades*\n\n")
|
sb.WriteString(msg(L, "recent_trades"))
|
||||||
totalPnL := 0.0
|
total := 0.0
|
||||||
for _, t := range trades {
|
for _, t := range trades {
|
||||||
emoji := "🟢"
|
e := "🟢"
|
||||||
if t.PnL < 0 {
|
if t.PnL < 0 {
|
||||||
emoji = "🔴"
|
e = "🔴"
|
||||||
}
|
}
|
||||||
sb.WriteString(fmt.Sprintf("%s %s %s %s — $%.2f (P/L: $%.2f)\n",
|
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))
|
e, t.Side, t.Symbol, t.Exchange, t.Price*t.Quantity, t.PnL))
|
||||||
totalPnL += 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
|
return sb.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) queryPositions() (string, error) {
|
func (a *Agent) queryPositions(L string) (string, error) {
|
||||||
var allPositions []execution.Position
|
var all []execution.Position
|
||||||
for _, ex := range a.config.Exchanges {
|
for _, ex := range a.config.Exchanges {
|
||||||
positions, err := a.bridge.GetPositions(ex.Name)
|
pos, err := a.bridge.GetPositions(ex.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("get positions", "exchange", ex.Name, "error", err)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
allPositions = append(allPositions, positions...)
|
all = append(all, pos...)
|
||||||
}
|
}
|
||||||
|
if len(all) == 0 {
|
||||||
if len(allPositions) == 0 {
|
return msg(L, "no_positions"), nil
|
||||||
return "📭 No open positions.", nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("📊 *Open Positions*\n\n")
|
sb.WriteString(msg(L, "open_positions"))
|
||||||
totalPnL := 0.0
|
total := 0.0
|
||||||
for _, p := range allPositions {
|
for _, p := range all {
|
||||||
emoji := "🟢"
|
e := "🟢"
|
||||||
if p.PnL < 0 {
|
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("%s *%s* %s\n Size: %.4f | Entry: $%.2f\n Mark: $%.2f | P/L: $%.2f\n",
|
||||||
sb.WriteString(fmt.Sprintf(" Size: %.4f | Entry: $%.2f\n", p.Size, p.EntryPrice))
|
e, p.Symbol, strings.ToUpper(p.Side), p.Size, p.EntryPrice, p.MarkPrice, p.PnL))
|
||||||
sb.WriteString(fmt.Sprintf(" Mark: $%.2f | P/L: $%.2f\n", p.MarkPrice, p.PnL))
|
|
||||||
if p.Leverage > 0 {
|
if p.Leverage > 0 {
|
||||||
sb.WriteString(fmt.Sprintf(" Leverage: %.0fx | Exchange: %s\n", p.Leverage, p.Exchange))
|
sb.WriteString(fmt.Sprintf(" Leverage: %.0fx | Exchange: %s\n", p.Leverage, p.Exchange))
|
||||||
}
|
}
|
||||||
sb.WriteString("\n")
|
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
|
return sb.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) queryBalance() (string, error) {
|
func (a *Agent) queryBalance(L string) (string, error) {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("💰 *Account Balance*\n\n")
|
sb.WriteString(msg(L, "account_balance"))
|
||||||
|
|
||||||
for _, ex := range a.config.Exchanges {
|
for _, ex := range a.config.Exchanges {
|
||||||
bal, err := a.bridge.GetBalance(ex.Name)
|
bal, err := a.bridge.GetBalance(ex.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("get balance", "exchange", ex.Name, "error", err)
|
|
||||||
sb.WriteString(fmt.Sprintf("• %s: ⚠️ Error\n", ex.Name))
|
sb.WriteString(fmt.Sprintf("• %s: ⚠️ Error\n", ex.Name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sb.WriteString(fmt.Sprintf("*%s*\n", strings.Title(ex.Name)))
|
sb.WriteString(fmt.Sprintf("*%s*\n", ex.Name))
|
||||||
sb.WriteString(fmt.Sprintf(" Total: $%.2f\n", bal.Total))
|
sb.WriteString(fmt.Sprintf(msg(L, "balance_total"), bal.Total))
|
||||||
sb.WriteString(fmt.Sprintf(" Available: $%.2f\n", bal.Available))
|
sb.WriteString(fmt.Sprintf(msg(L, "balance_available"), bal.Available))
|
||||||
sb.WriteString(fmt.Sprintf(" In Position: $%.2f\n\n", bal.InPosition))
|
sb.WriteString(fmt.Sprintf(msg(L, "balance_in_position"), bal.InPosition))
|
||||||
}
|
}
|
||||||
return sb.String(), nil
|
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"
|
symbol := "BTC"
|
||||||
if detail, ok := intent.Params["detail"]; ok && detail != "" {
|
if d, ok := intent.Params["detail"]; ok && d != "" {
|
||||||
symbol = strings.ToUpper(strings.TrimSpace(detail))
|
symbol = strings.ToUpper(strings.TrimSpace(d))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add live price if available
|
|
||||||
priceInfo := ""
|
priceInfo := ""
|
||||||
if a.monitor != nil {
|
if a.monitor != nil {
|
||||||
if snap, ok := a.monitor.GetSnapshot(symbol + "USDT"); ok && snap.LastPrice > 0 {
|
if snap, ok := a.monitor.GetSnapshot(symbol + "USDT"); ok && snap.LastPrice > 0 {
|
||||||
priceInfo = fmt.Sprintf("\nCurrent price: $%.2f", snap.LastPrice)
|
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(
|
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.",
|
||||||
"Analyze %s/USDT for trading. %s\n"+
|
symbol, priceInfo, map[string]string{"zh": "Chinese", "en": "English"}[L])
|
||||||
"Consider: trend, support/resistance, momentum indicators, volume, sentiment.\n"+
|
|
||||||
"Give specific entry/exit levels and stop loss. Be concise.",
|
|
||||||
symbol, priceInfo)
|
|
||||||
|
|
||||||
analysis, err := a.thinker.Analyze(ctx, prompt)
|
analysis, err := a.thinker.Analyze(ctx, prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("AI analyze: %w", err)
|
return "", fmt.Errorf("AI analyze: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
emoji := map[string]string{
|
emojiMap := map[string]string{"buy": "🟢 BUY", "sell": "🔴 SELL", "hold": "🟡 HOLD", "wait": "⏳ WAIT"}
|
||||||
"buy": "🟢 BUY",
|
action := emojiMap[analysis.Action]
|
||||||
"sell": "🔴 SELL",
|
|
||||||
"hold": "🟡 HOLD",
|
|
||||||
"wait": "⏳ WAIT",
|
|
||||||
}
|
|
||||||
|
|
||||||
action := emoji[analysis.Action]
|
|
||||||
if action == "" {
|
if action == "" {
|
||||||
action = "🤔 " + analysis.Action
|
action = "🤔 " + analysis.Action
|
||||||
}
|
}
|
||||||
|
|
||||||
result := fmt.Sprintf("🔍 *%s/USDT Analysis*\n\nSignal: %s\nConfidence: %.0f%%\n\n%s",
|
result := fmt.Sprintf(msg(L, "analysis_signal"), symbol, action, analysis.Confidence*100, analysis.Reasoning)
|
||||||
symbol, action, analysis.Confidence*100, analysis.Reasoning)
|
|
||||||
|
|
||||||
if analysis.StopLoss > 0 {
|
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 {
|
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
|
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"])
|
action := strings.ToLower(intent.Params["action"])
|
||||||
detail := intent.Params["detail"]
|
detail := intent.Params["detail"]
|
||||||
|
|
||||||
if a.bridge == nil {
|
if a.bridge == nil || len(a.config.Exchanges) == 0 {
|
||||||
return fmt.Sprintf("⚡ *Trade: %s %s*\n\n🔧 No exchange connected. Configure exchanges in config.yaml first.",
|
return msg(L, "no_exchange"), nil
|
||||||
strings.ToUpper(action), detail), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse: "BTC 0.01" or "BTCUSDT 0.01 2x"
|
|
||||||
parts := strings.Fields(detail)
|
parts := strings.Fields(detail)
|
||||||
if len(parts) < 1 {
|
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])
|
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 {
|
if len(parts) >= 2 {
|
||||||
q, err := strconv.ParseFloat(parts[1], 64)
|
q, err := strconv.ParseFloat(parts[1], 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("❓ Invalid quantity: %s", parts[1]), nil
|
return fmt.Sprintf(msg(L, "invalid_quantity"), parts[1]), nil
|
||||||
}
|
}
|
||||||
quantity = q
|
quantity = q
|
||||||
}
|
}
|
||||||
if len(parts) >= 3 {
|
if len(parts) >= 3 {
|
||||||
levStr := strings.TrimSuffix(strings.ToLower(parts[2]), "x")
|
if l, err := strconv.Atoi(strings.TrimSuffix(strings.ToLower(parts[2]), "x")); err == nil {
|
||||||
l, err := strconv.Atoi(levStr)
|
|
||||||
if err == nil {
|
|
||||||
leverage = l
|
leverage = l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if quantity <= 0 && (action == "buy" || action == "sell" || action == "long" || action == "short") {
|
if quantity <= 0 {
|
||||||
return "❓ Please specify quantity: `/buy BTC 0.01`", nil
|
return msg(L, "specify_quantity"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map action to execution side
|
|
||||||
var side string
|
var side string
|
||||||
switch action {
|
switch action {
|
||||||
case "buy", "long", "open_long":
|
case "buy", "long", "open_long", "做多":
|
||||||
side = "LONG"
|
side = "LONG"
|
||||||
case "sell", "short", "open_short":
|
case "sell", "short", "open_short", "做空":
|
||||||
side = "SHORT"
|
side = "SHORT"
|
||||||
case "close":
|
case "close", "平仓":
|
||||||
side = "CLOSE_LONG" // TODO: detect which side to close
|
side = "CLOSE_LONG"
|
||||||
default:
|
default:
|
||||||
side = strings.ToUpper(action)
|
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
|
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",
|
a.memory.SetPreference(userID, "pending_trade",
|
||||||
fmt.Sprintf("%s|%s|%f|%d|%s", side, symbol, quantity, leverage, exchange))
|
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.
|
// ExecutePendingTrade executes a confirmed trade.
|
||||||
func (a *Agent) ExecutePendingTrade(ctx context.Context, userID int64) (string, error) {
|
func (a *Agent) ExecutePendingTrade(ctx context.Context, userID int64, L string) (string, error) {
|
||||||
pending, err := a.memory.GetPreference(userID, "pending_trade")
|
pending, err := a.memory.GetPreference(userID, "pending_trade")
|
||||||
if err != nil || pending == "" {
|
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", "")
|
a.memory.SetPreference(userID, "pending_trade", "")
|
||||||
|
|
||||||
parts := strings.Split(pending, "|")
|
parts := strings.Split(pending, "|")
|
||||||
if len(parts) != 5 {
|
if len(parts) != 5 {
|
||||||
return "", fmt.Errorf("invalid pending trade data")
|
return "", fmt.Errorf("invalid pending trade")
|
||||||
}
|
}
|
||||||
|
|
||||||
side := parts[0]
|
side, symbol := parts[0], parts[1]
|
||||||
symbol := parts[1]
|
|
||||||
quantity, _ := strconv.ParseFloat(parts[2], 64)
|
quantity, _ := strconv.ParseFloat(parts[2], 64)
|
||||||
leverage, _ := strconv.Atoi(parts[3])
|
leverage, _ := strconv.Atoi(parts[3])
|
||||||
exchange := parts[4]
|
exchange := parts[4]
|
||||||
@@ -423,27 +314,13 @@ func (a *Agent) ExecutePendingTrade(ctx context.Context, userID int64) (string,
|
|||||||
return "", fmt.Errorf("execute trade: %w", err)
|
return "", fmt.Errorf("execute trade: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save trade to memory
|
tr := &memory.TradeRecord{Exchange: exchange, Symbol: symbol, Side: strings.ToLower(side), Type: "market", Quantity: quantity, Status: "open"}
|
||||||
tradeRecord := &memory.TradeRecord{
|
if p, ok := result["avgPrice"].(float64); ok {
|
||||||
Exchange: exchange,
|
tr.Price = p
|
||||||
Symbol: symbol,
|
|
||||||
Side: strings.ToLower(side),
|
|
||||||
Type: "market",
|
|
||||||
Quantity: quantity,
|
|
||||||
Status: "open",
|
|
||||||
}
|
}
|
||||||
if price, ok := result["avgPrice"].(float64); ok {
|
a.memory.SaveTrade(tr)
|
||||||
tradeRecord.Price = price
|
|
||||||
}
|
|
||||||
a.memory.SaveTrade(tradeRecord)
|
|
||||||
|
|
||||||
return fmt.Sprintf("✅ *Trade Executed!*\n\n"+
|
return fmt.Sprintf(msg(L, "trade_executed"), side, symbol, quantity, leverage, exchange, result), nil
|
||||||
"• %s %s\n"+
|
|
||||||
"• Qty: %.6f\n"+
|
|
||||||
"• Leverage: %dx\n"+
|
|
||||||
"• Exchange: %s\n"+
|
|
||||||
"• Result: %v",
|
|
||||||
side, symbol, quantity, leverage, exchange, result), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) handleStrategyCommand(text string) string {
|
func (a *Agent) handleStrategyCommand(text string) string {
|
||||||
@@ -451,43 +328,37 @@ func (a *Agent) handleStrategyCommand(text string) string {
|
|||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
return a.strategyRunner.FormatStrategyList()
|
return a.strategyRunner.FormatStrategyList()
|
||||||
}
|
}
|
||||||
|
switch strings.ToLower(parts[1]) {
|
||||||
subcmd := strings.ToLower(parts[1])
|
|
||||||
switch subcmd {
|
|
||||||
case "list":
|
case "list":
|
||||||
return a.strategyRunner.FormatStrategyList()
|
return a.strategyRunner.FormatStrategyList()
|
||||||
case "start":
|
case "start":
|
||||||
if len(parts) < 3 {
|
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])
|
sym := strings.ToUpper(parts[2])
|
||||||
if !strings.HasSuffix(symbol, "USDT") {
|
if !strings.HasSuffix(sym, "USDT") {
|
||||||
symbol += "USDT"
|
sym += "USDT"
|
||||||
}
|
}
|
||||||
interval := 1 * time.Hour
|
iv := 1 * time.Hour
|
||||||
if len(parts) >= 4 {
|
if len(parts) >= 4 {
|
||||||
switch parts[3] {
|
switch parts[3] {
|
||||||
case "15m":
|
case "15m":
|
||||||
interval = 15 * time.Minute
|
iv = 15 * time.Minute
|
||||||
case "30m":
|
case "30m":
|
||||||
interval = 30 * time.Minute
|
iv = 30 * time.Minute
|
||||||
case "1h":
|
|
||||||
interval = 1 * time.Hour
|
|
||||||
case "4h":
|
case "4h":
|
||||||
interval = 4 * time.Hour
|
iv = 4 * time.Hour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exchange := "binance"
|
ex := "binance"
|
||||||
if len(parts) >= 5 {
|
if len(parts) >= 5 {
|
||||||
exchange = parts[4]
|
ex = parts[4]
|
||||||
}
|
}
|
||||||
name := fmt.Sprintf("AI-%s", symbol)
|
id, err := a.strategyRunner.StartStrategy("AI-"+sym, sym, ex, iv)
|
||||||
id, err := a.strategyRunner.StartStrategy(name, symbol, exchange, interval)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("⚠️ %v", err)
|
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`",
|
return fmt.Sprintf("🚀 Strategy started!\n\n• ID: `%s`\n• Symbol: %s\n• Interval: %s\n• Exchange: %s", id, sym, iv, ex)
|
||||||
id, symbol, interval, exchange, id)
|
|
||||||
case "stop":
|
case "stop":
|
||||||
if len(parts) < 3 {
|
if len(parts) < 3 {
|
||||||
return "Usage: `/strategy stop <id>`"
|
return "Usage: `/strategy stop <id>`"
|
||||||
@@ -500,67 +371,26 @@ func (a *Agent) handleStrategyCommand(text string) string {
|
|||||||
a.strategyRunner.StopAll()
|
a.strategyRunner.StopAll()
|
||||||
return "✅ All strategies stopped."
|
return "✅ All strategies stopped."
|
||||||
default:
|
default:
|
||||||
return "Unknown subcommand. Use: `/strategy list|start|stop|stopall`"
|
return "Use: `/strategy list|start|stop|stopall`"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) handleSettings(intent Intent) string {
|
func (a *Agent) handleChat(ctx context.Context, userID int64, L string, text string) (string, error) {
|
||||||
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
|
|
||||||
lower := strings.ToLower(text)
|
lower := strings.ToLower(text)
|
||||||
if lower == "yes" || lower == "y" || lower == "确认" || lower == "是" {
|
if lower == "yes" || lower == "y" || lower == "确认" || lower == "是" {
|
||||||
pending, _ := a.memory.GetPreference(userID, "pending_trade")
|
if p, _ := a.memory.GetPreference(userID, "pending_trade"); p != "" {
|
||||||
if pending != "" {
|
return a.ExecutePendingTrade(ctx, userID, L)
|
||||||
return a.ExecutePendingTrade(ctx, userID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get conversation history for context
|
history, _ := a.memory.GetRecentMessages(userID, 20)
|
||||||
history, err := a.memory.GetRecentMessages(userID, 20)
|
|
||||||
if err != nil {
|
sysPrompt := fmt.Sprintf(msg(L, "system_prompt"), time.Now().Format("2006-01-02 15:04:05"))
|
||||||
a.logger.Error("get history", "error", err)
|
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
|
return a.thinker.Chat(ctx, msgs)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
204
nofxi/internal/agent/i18n.go
Normal file
204
nofxi/internal/agent/i18n.go
Normal 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
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ func NewWebServer(port int, handler MessageHandler, webDir string, logger *slog.
|
|||||||
type chatRequest struct {
|
type chatRequest struct {
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
Lang string `json:"lang"` // "zh" or "en"
|
||||||
}
|
}
|
||||||
|
|
||||||
// chatResponse is the API response body.
|
// 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
|
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 {
|
if err != nil {
|
||||||
writeJSON(rw, http.StatusInternalServerError, chatAPIResponse{Error: err.Error()})
|
writeJSON(rw, http.StatusInternalServerError, chatAPIResponse{Error: err.Error()})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ async function send(text){
|
|||||||
addM('user',text);
|
addM('user',text);
|
||||||
typEl.classList.add('on');sBtnEl.disabled=true;inpEl.focus();
|
typEl.classList.add('on');sBtnEl.disabled=true;inpEl.focus();
|
||||||
try{
|
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();
|
const d=await r.json();
|
||||||
addM('bot',d.response||d.error||'No response');
|
addM('bot',d.response||d.error||'No response');
|
||||||
}catch(e){addM('bot','⚠️ Error: '+e.message)}
|
}catch(e){addM('bot','⚠️ Error: '+e.message)}
|
||||||
|
|||||||
Reference in New Issue
Block a user