mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-11 14:56:57 +08:00
refactor: single-user web-based setup — replace env config with Settings UI
Move from multi-user env-var config to single-user web-first architecture: - Add SetupPage for first-time initialization (replaces /register) - Add SettingsPage for AI models, exchanges, Telegram, and password management - Enrich all API route schemas with exact ID usage documentation - Add PUT /user/password endpoint for in-app password changes - Remove REGISTRATION_ENABLED, MAX_USERS, TELEGRAM_BOT_TOKEN from env config - Simplify LoginPage design, remove admin mode and registration links - Telegram bot now resolves user email for identity display - start.sh no longer runs interactive Telegram setup
This commit is contained in:
@@ -75,46 +75,107 @@ func GenerateBotToken(userID string) (string, error) {
|
||||
// and per-trader account summary + statistics) and returns it as a formatted string for
|
||||
// injection into the LLM context at the start of each conversation.
|
||||
func (a *Agent) buildAccountContext() string {
|
||||
type q struct {
|
||||
label string
|
||||
path string
|
||||
}
|
||||
queries := []q{
|
||||
{"AI Models", "/api/models"},
|
||||
{"Exchanges", "/api/exchanges"},
|
||||
{"Strategies", "/api/strategies"},
|
||||
{"Traders", "/api/my-traders"},
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("[Current Account State - Authenticated User ID: %s]\n\n", a.userID))
|
||||
sb.WriteString(fmt.Sprintf("[Current Account State — User: %s]\n\n", a.userID))
|
||||
|
||||
var tradersJSON string
|
||||
for _, query := range queries {
|
||||
result := a.apiTool.execute(&apiRequest{Method: "GET", Path: query.path})
|
||||
sb.WriteString(fmt.Sprintf("%s:\n%s\n\n", query.label, result))
|
||||
if query.path == "/api/my-traders" {
|
||||
tradersJSON = result
|
||||
}
|
||||
// ── AI Models ─────────────────────────────────────────────────────────────
|
||||
modelsRaw := a.apiTool.execute(&apiRequest{Method: "GET", Path: "/api/models"})
|
||||
sb.WriteString("## AI Models\n")
|
||||
sb.WriteString("⚠️ When creating a trader, use the EXACT \"id\" value below for \"ai_model_id\".\n")
|
||||
sb.WriteString(" DO NOT use the \"provider\" field — it is NOT a valid ai_model_id.\n\n")
|
||||
|
||||
var models []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(modelsRaw), &models); err == nil && len(models) > 0 {
|
||||
for _, m := range models {
|
||||
status := "disabled"
|
||||
if m.Enabled {
|
||||
status = "ENABLED"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" • ai_model_id=\"%s\" provider=%s name=%s [%s]\n", m.ID, m.Provider, m.Name, status))
|
||||
}
|
||||
} else {
|
||||
sb.WriteString(modelsRaw)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// ── Exchanges ─────────────────────────────────────────────────────────────
|
||||
exchangesRaw := a.apiTool.execute(&apiRequest{Method: "GET", Path: "/api/exchanges"})
|
||||
sb.WriteString("## Exchanges\n")
|
||||
sb.WriteString("⚠️ Use the EXACT \"id\" value below for \"exchange_id\" when creating a trader.\n\n")
|
||||
|
||||
var exchanges []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ExchangeType string `json:"exchange_type"`
|
||||
AccountName string `json:"account_name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(exchangesRaw), &exchanges); err == nil && len(exchanges) > 0 {
|
||||
for _, e := range exchanges {
|
||||
status := "disabled"
|
||||
if e.Enabled {
|
||||
status = "ENABLED"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" • exchange_id=\"%s\" type=%s account=%s [%s]\n", e.ID, e.ExchangeType, e.AccountName, status))
|
||||
}
|
||||
} else {
|
||||
sb.WriteString(exchangesRaw)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// ── Strategies ────────────────────────────────────────────────────────────
|
||||
strategiesRaw := a.apiTool.execute(&apiRequest{Method: "GET", Path: "/api/strategies"})
|
||||
sb.WriteString("## Strategies\n")
|
||||
|
||||
var strategies []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(strategiesRaw), &strategies); err == nil && len(strategies) > 0 {
|
||||
for _, s := range strategies {
|
||||
sb.WriteString(fmt.Sprintf(" • strategy_id=\"%s\" name=%s\n", s.ID, s.Name))
|
||||
}
|
||||
} else {
|
||||
sb.WriteString(strategiesRaw)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// ── Traders ───────────────────────────────────────────────────────────────
|
||||
tradersRaw := a.apiTool.execute(&apiRequest{Method: "GET", Path: "/api/my-traders"})
|
||||
sb.WriteString("## Traders\n")
|
||||
|
||||
// For each running trader, fetch real-time account balance and trading statistics.
|
||||
var traders []struct {
|
||||
TraderID string `json:"trader_id"`
|
||||
Name string `json:"trader_name"`
|
||||
IsRunning bool `json:"is_running"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(tradersJSON), &traders); err == nil {
|
||||
if err := json.Unmarshal([]byte(tradersRaw), &traders); err == nil && len(traders) > 0 {
|
||||
for _, t := range traders {
|
||||
if !t.IsRunning {
|
||||
continue
|
||||
status := "stopped"
|
||||
if t.IsRunning {
|
||||
status = "RUNNING"
|
||||
}
|
||||
acct := a.apiTool.execute(&apiRequest{Method: "GET", Path: "/api/account?trader_id=" + t.TraderID})
|
||||
sb.WriteString(fmt.Sprintf("Account [%s] (trader_id=%s):\n%s\n\n", t.Name, t.TraderID, acct))
|
||||
|
||||
stats := a.apiTool.execute(&apiRequest{Method: "GET", Path: "/api/statistics?trader_id=" + t.TraderID})
|
||||
sb.WriteString(fmt.Sprintf("Statistics [%s] (trader_id=%s):\n%s\n\n", t.Name, t.TraderID, stats))
|
||||
sb.WriteString(fmt.Sprintf(" • trader_id=\"%s\" name=%s [%s]\n", t.TraderID, t.Name, status))
|
||||
}
|
||||
} else {
|
||||
sb.WriteString(tradersRaw)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// ── Per-trader live data (running traders only) ────────────────────────────
|
||||
for _, t := range traders {
|
||||
if !t.IsRunning {
|
||||
continue
|
||||
}
|
||||
acct := a.apiTool.execute(&apiRequest{Method: "GET", Path: "/api/account?trader_id=" + t.TraderID})
|
||||
sb.WriteString(fmt.Sprintf("Account [%s]:\n%s\n\n", t.Name, acct))
|
||||
stats := a.apiTool.execute(&apiRequest{Method: "GET", Path: "/api/statistics?trader_id=" + t.TraderID})
|
||||
sb.WriteString(fmt.Sprintf("Statistics [%s]:\n%s\n\n", t.Name, stats))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
|
||||
@@ -21,8 +21,9 @@ type Manager struct {
|
||||
}
|
||||
|
||||
// NewManager creates a Manager. Call api.GetAPIDocs() before this and pass the result as apiDocs.
|
||||
// userID is the database user ID the bot authenticates as (used in system prompt context).
|
||||
func NewManager(apiPort int, botToken, userID string, getLLM func() mcp.AIClient, apiDocs string) *Manager {
|
||||
// userEmail is the registered email shown to the user when they ask "who am I".
|
||||
// userID is the internal DB UUID used for API authentication.
|
||||
func NewManager(apiPort int, botToken, userEmail, userID string, getLLM func() mcp.AIClient, apiDocs string) *Manager {
|
||||
return &Manager{
|
||||
agents: make(map[int64]*Agent),
|
||||
lanes: make(map[int64]chan struct{}),
|
||||
@@ -30,7 +31,7 @@ func NewManager(apiPort int, botToken, userID string, getLLM func() mcp.AIClient
|
||||
botToken: botToken,
|
||||
userID: userID,
|
||||
getLLM: getLLM,
|
||||
systemPrompt: BuildAgentPrompt(apiDocs, userID),
|
||||
systemPrompt: BuildAgentPrompt(apiDocs, userEmail, userID),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,16 @@ import "fmt"
|
||||
|
||||
// BuildAgentPrompt constructs the full system prompt with live API documentation injected.
|
||||
// apiDocs is the output of api.GetAPIDocs() — reflects all currently registered routes with full schemas.
|
||||
// userID is the actual database user ID the bot authenticates as.
|
||||
func BuildAgentPrompt(apiDocs, userID string) string {
|
||||
// userEmail is the registered email of the bound user (shown when user asks "who am I").
|
||||
// userID is the internal DB UUID used for API authentication only.
|
||||
func BuildAgentPrompt(apiDocs, userEmail, userID string) string {
|
||||
return fmt.Sprintf(`You are the NOFX quantitative trading system AI assistant.
|
||||
|
||||
## Your Identity
|
||||
- You are authenticated as user ID: %s
|
||||
- You are operating as: %s
|
||||
- Internal user ID (for API calls only): %s
|
||||
- When asked "which user / account / email" — answer with the email address above
|
||||
- All API calls are made on behalf of this user
|
||||
- When asked "which user / username / email" — answer with this user ID directly, no API call needed
|
||||
|
||||
## Tool: api_request
|
||||
Use the api_request tool to call the NOFX REST API:
|
||||
@@ -23,6 +25,16 @@ Use the api_request tool to call the NOFX REST API:
|
||||
|
||||
%s
|
||||
|
||||
## CRITICAL: Exact ID Rule (read this before every API call)
|
||||
API fields like "ai_model_id", "exchange_id", "strategy_id", "trader_id" require the EXACT "id" value
|
||||
from the corresponding API response. NEVER use "provider", "type", or any other field as a substitute.
|
||||
|
||||
Wrong: {"ai_model_id": "deepseek"} ← "deepseek" is the provider, NOT the id
|
||||
Correct: {"ai_model_id": "abc123_deepseek"} ← full "id" from GET /api/models
|
||||
|
||||
The Account State block at the start of this conversation lists every resource with its exact id.
|
||||
Read the id field from there and copy it verbatim — do not abbreviate, shorten, or guess.
|
||||
|
||||
## Behavior Rules
|
||||
1. Reply in the same language the user used (中文→中文, English→English)
|
||||
2. Keep final replies concise — show results, not process
|
||||
@@ -30,12 +42,6 @@ Use the api_request tool to call the NOFX REST API:
|
||||
4. When user provides enough info, act immediately — no confirmation needed
|
||||
5. Be decisive — infer intent from context, use schema to fill in smart defaults
|
||||
|
||||
## First-time Setup Detection
|
||||
Check Account State at conversation start:
|
||||
- If AI Models shows all disabled/unconfigured AND Exchanges empty → tell user to send /start for setup guide
|
||||
- If Exchanges empty but models OK → guide user to configure exchange: ask for exchange type + API credentials in ONE message
|
||||
- Never ask user to visit the web UI — everything can be done via chat
|
||||
|
||||
## Verification Rule (CRITICAL)
|
||||
After ANY PUT or POST that creates or modifies a resource:
|
||||
1. Immediately GET the resource to read actual saved values
|
||||
@@ -45,7 +51,7 @@ After ANY PUT or POST that creates or modifies a resource:
|
||||
|
||||
## Error Handling
|
||||
- 400: explain what was wrong, ask user to correct
|
||||
- 404: resource doesn't exist, check IDs
|
||||
- 404: resource doesn't exist — you may have used the wrong ID format; check the Account State for the exact id
|
||||
- "AI model not enabled": tell user to enable the model first via PUT /api/models
|
||||
- "Exchange not enabled": tell user to enable the exchange first
|
||||
- 5xx: server error, ask user to try again
|
||||
@@ -65,10 +71,6 @@ Use this to:
|
||||
|
||||
## Common Workflows
|
||||
|
||||
**Configure model**: Ask only for api_key. Set enabled:true, send empty strings for URL/model (backend applies provider defaults).
|
||||
|
||||
**Configure exchange**: Ask for all required fields in ONE message (see schema). Always set enabled:true.
|
||||
|
||||
**Create strategy** (independent from traders):
|
||||
- Never GET trader info just to create a strategy.
|
||||
- POST {"name":"<descriptive name>"} — config is OPTIONAL. Backend applies complete working defaults automatically (ai500 top coins, all indicators, standard risk control). Strategy is immediately usable.
|
||||
@@ -91,5 +93,5 @@ Execute these steps IN ORDER with NO user confirmation between them:
|
||||
|
||||
**Start/stop existing trader**: From Account State, if only one trader, act directly. If multiple, list and ask.
|
||||
|
||||
**Query data**: Use trader_id from Account State, then query /api/positions?trader_id=xxx or /api/account?trader_id=xxx etc.`, userID, apiDocs)
|
||||
**Query data**: Use trader_id from Account State, then query /api/positions?trader_id=xxx or /api/account?trader_id=xxx etc.`, userEmail, userID, apiDocs)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user