feat: cream terminal redesign, English-only UI, autopilot launch fixes

- Redesign dashboard into a cream-paper + vermilion IBM Plex Mono terminal
  (live L2 order book, cost/liq map, WS K-line, signal matrix, orchestration
  topology, risk radar, execution log, current positions, equity curve)
- Convert all user-facing UI and backend strings/prompts from Chinese to
  English (multi-language retained, default English)
- Add /api/statistics/full endpoint + full-stats frontend wiring
- Fix Autopilot launch: reuse the existing trader instead of creating
  duplicates (eliminates repeat ~35s create cost and stale-trader 404s);
  launch sends 5m scan interval
- Fix unreadable toasts: cream theme with high-contrast text + per-type accent
- Silence background dashboard polls (getTraderConfig) to stop error-toast spam
This commit is contained in:
tinkle-community
2026-06-30 16:03:52 +08:00
parent eba28bcf0e
commit 110bf52908
149 changed files with 6835 additions and 3611 deletions

View File

@@ -273,7 +273,7 @@ func (a *Agent) Run(userMessage string, onChunk func(string)) string {
// Safety: max iterations reached.
logger.Warnf("Agent: max iterations (%d) reached for message: %q", maxIterations, userMessage)
reply := "Operation completed. Please check your account for the latest status. / 操作已完成,请检查您的账户查看最新状态。"
reply := "Operation completed. Please check your account for the latest status."
a.memory.Add("user", userMessage)
a.memory.Add("assistant", reply)
return reply

View File

@@ -213,7 +213,7 @@ func TestNarrationStructurallyImpossible(t *testing.T) {
// Simulate a (malformed) response that has both Content and ToolCalls.
malformed := &mcp.LLMResponse{
Content: "现在我将为您查询策略。", // narration — must NOT reach user
Content: "Now I will look up your strategies.", // narration — must NOT reach user
ToolCalls: []mcp.ToolCall{{
ID: "c1",
Type: "function",
@@ -226,15 +226,15 @@ func TestNarrationStructurallyImpossible(t *testing.T) {
llm := &mockLLM{responses: []*mcp.LLMResponse{
malformed,
textReply("你有1个策略BTC Trend"),
textReply("You have 1 strategy: BTC Trend."),
}}
a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt)
reply := a.Run("查询我的策略", nil)
reply := a.Run("look up my strategies", nil)
if strings.Contains(reply, "现在我将") {
if strings.Contains(reply, "Now I will") {
t.Fatalf("narration leaked into final reply: %q", reply)
}
if reply != "你有1个策略BTC Trend" {
if reply != "You have 1 strategy: BTC Trend." {
t.Fatalf("unexpected reply: %q", reply)
}
}
@@ -276,18 +276,18 @@ func TestOnChunkCalledWithFinalReply(t *testing.T) {
// Verifies: POST strategy → GET verify → final reply shows strategy info.
func TestCreateStrategyWorkflow(t *testing.T) {
srv, port := mockAPIServer(map[string]string{
"POST /api/strategies": `{"id":"s1","name":"BTC趋势"}`,
"GET /api/strategies/s1": `{"id":"s1","name":"BTC趋势","config":{"coin_source":{"source_type":"static","static_coins":["BTC/USDT"]},"leverage":5}}`,
"POST /api/strategies": `{"id":"s1","name":"BTC Trend"}`,
"GET /api/strategies/s1": `{"id":"s1","name":"BTC Trend","config":{"coin_source":{"source_type":"static","static_coins":["BTC/USDT"]},"leverage":5}}`,
})
defer srv.Close()
llm := &mockLLM{responses: []*mcp.LLMResponse{
toolCall("c1", "POST", "/api/strategies", `{"name":"BTC趋势","config":{}}`),
toolCall("c1", "POST", "/api/strategies", `{"name":"BTC Trend","config":{}}`),
toolCall("c2", "GET", "/api/strategies/s1", "{}"),
textReply("策略已创建BTC趋势币种 BTC/USDT杠杆 5x"),
textReply("Strategy created: BTC Trend, coin BTC/USDT, leverage 5x."),
}}
a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt)
reply := a.Run("帮我配置个btc趋势交易的策略", nil)
reply := a.Run("set up a BTC trend trading strategy for me", nil)
if llm.calls != 3 {
t.Fatalf("expected 3 LLM calls, got %d", llm.calls)
@@ -298,7 +298,7 @@ func TestCreateStrategyWorkflow(t *testing.T) {
}
// TestFullSetupWorkflow: create strategy → verify → create trader → start trader.
// This is the "帮我配置策略并跑起来" workflow.
// This is the "create strategy and start it" workflow.
func TestFullSetupWorkflow(t *testing.T) {
calls := map[string]int{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -306,11 +306,11 @@ func TestFullSetupWorkflow(t *testing.T) {
calls[key]++
switch key {
case "POST /api/strategies":
w.Write([]byte(`{"id":"s1","name":"BTC趋势"}`)) //nolint:errcheck
w.Write([]byte(`{"id":"s1","name":"BTC Trend"}`)) //nolint:errcheck
case "GET /api/strategies/s1":
w.Write([]byte(`{"id":"s1","name":"BTC趋势","config":{}}`)) //nolint:errcheck
w.Write([]byte(`{"id":"s1","name":"BTC Trend","config":{}}`)) //nolint:errcheck
case "POST /api/traders":
w.Write([]byte(`{"id":"tr1","name":"BTC趋势交易员"}`)) //nolint:errcheck
w.Write([]byte(`{"id":"tr1","name":"BTC Trend Trader"}`)) //nolint:errcheck
case "POST /api/traders/tr1/start":
w.Write([]byte(`{"ok":true}`)) //nolint:errcheck
default:
@@ -322,14 +322,14 @@ func TestFullSetupWorkflow(t *testing.T) {
fmt.Sscanf(srv.Listener.Addr().String(), "127.0.0.1:%d", &port)
llm := &mockLLM{responses: []*mcp.LLMResponse{
toolCall("c1", "POST", "/api/strategies", `{"name":"BTC趋势"}`),
toolCall("c1", "POST", "/api/strategies", `{"name":"BTC Trend"}`),
toolCall("c2", "GET", "/api/strategies/s1", "{}"),
toolCall("c3", "POST", "/api/traders", `{"name":"BTC趋势交易员","strategy_id":"s1"}`),
toolCall("c3", "POST", "/api/traders", `{"name":"BTC Trend Trader","strategy_id":"s1"}`),
toolCall("c4", "POST", "/api/traders/tr1/start", "{}"),
textReply("策略和交易员已创建并启动BTC趋势交易员正在运行。"),
textReply("Strategy and trader created and started! BTC Trend Trader is running."),
}}
a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt)
reply := a.Run("帮我配置个btc趋势交易的策略交易 跑起来", nil)
reply := a.Run("set up a BTC trend trading strategy and start it", nil)
if llm.calls != 5 {
t.Fatalf("expected 5 LLM calls, got %d", llm.calls)
@@ -370,15 +370,15 @@ func TestStartExistingTrader(t *testing.T) {
llm := &mockLLM{responses: []*mcp.LLMResponse{
toolCall("c1", "GET", "/api/my-traders", "{}"),
toolCall("c2", "POST", "/api/traders/tr1/start", "{}"),
textReply("交易员 BTC Trader 已启动。"),
textReply("Trader BTC Trader has been started."),
}}
a := New(port, "tok", "test-user", mockGetLLM(llm), testPrompt)
reply := a.Run("启动交易员", nil)
reply := a.Run("start the trader", nil)
if calls["POST /api/traders/tr1/start"] != 1 {
t.Errorf("expected trader to be started, got %d start calls", calls["POST /api/traders/tr1/start"])
}
if reply != "交易员 BTC Trader 已启动。" {
if reply != "Trader BTC Trader has been started." {
t.Fatalf("unexpected reply: %q", reply)
}
}

View File

@@ -45,7 +45,7 @@ func (m *Manager) Run(chatID int64, userMessage string, onChunk func(string)) st
case lane <- struct{}{}:
case <-time.After(60 * time.Second):
logger.Warnf("Agent: lane wait timeout for chat %d — previous message still processing", chatID)
return "Previous message is still being processed. Please wait a moment and try again. / 上一条消息仍在处理中,请稍等片刻后再试。"
return "Previous message is still being processed. Please wait a moment and try again."
}
defer func() { <-lane }()
return a.Run(userMessage, onChunk)

View File

@@ -36,7 +36,7 @@ The Account State block at the start of this conversation lists every resource w
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)
1. Reply in the same language the user used (match the user's language exactly)
2. Keep final replies concise — show results, not process
3. Ask for ALL missing required info in ONE message — never ask one field at a time
4. When user provides enough info, act immediately — no confirmation needed
@@ -77,7 +77,7 @@ Use this to:
- Only include "config" when user explicitly requests custom settings (specific coins, custom leverage, different timeframes).
- After POST: GET /api/strategies/:id to verify → show user: name, coin_source.source_type, key risk_control values
**"帮我配置策略并跑起来" / "create strategy and start" (full setup workflow)**:
**"create strategy and start" (full setup workflow)**:
Execute these steps IN ORDER with NO user confirmation between them:
1. POST /api/strategies — body: {"name":"<descriptive name>"} — no config needed, defaults are complete
2. GET /api/strategies/:id — verify strategy was saved

View File

@@ -355,12 +355,12 @@ func statusMsg(st *store.Store, userID string, apiPort int, lang string) string
missing := ""
if lang == "zh" {
if !hasModel {
missing += "\n❌ AI 模型 → 设置 → AI 模型 → 添加"
missing += "\n❌ AI Model → Settings → AI Models → Add"
}
if !hasExchange {
missing += "\n❌ 交易所 → 设置 → 交易所 → 添加"
missing += "\n❌ Exchange → Settings → Exchanges → Add"
}
return "⚙️ *需要完成初始配置*\n\n打开 Web 管理界面完成配置:\n→ " + webURL + "\n" + missing + "\n\n配置完成后发送 /start"
return "⚙️ *Setup required*\n\nOpen the web dashboard to complete setup:\n→ " + webURL + "\n" + missing + "\n\nSend /start when done."
}
if !hasModel {
missing += "\n❌ AI Model → Settings → AI Models → Add"
@@ -373,16 +373,16 @@ func statusMsg(st *store.Store, userID string, apiPort int, lang string) string
// All configured — show ready state.
if lang == "zh" {
return `✅ *NOFX 就绪,开始交易吧!*
return `✅ *NOFX is ready!*
直接告诉我你想做什么:
Just tell me what you want:
📊 "查看我的持仓"
💰 "账户余额多少"
🤖 "帮我创建 BTC 趋势策略并启动"
⏹ "停止所有交易员"
📊 "Show my positions"
💰 "What's my balance?"
🤖 "Create a BTC trend strategy and start it"
⏹ "Stop all traders"
/help 查看更多 · /lang 切换语言`
/help for more · /lang to change language`
}
return `✅ *NOFX is ready!*
@@ -399,14 +399,14 @@ Just tell me what you want:
// ── Language ──────────────────────────────────────────────────────────────────
func langMenuMsg() string {
return "🌐 *Choose your language*\n\n1 — English\n2 — 中文\n\nReply with 1 or 2"
return "🌐 *Choose your language*\n\n1 — English\n2 — Chinese\n\nReply with 1 or 2"
}
func parseLangChoice(text string) string {
switch strings.TrimSpace(text) {
case "1", "en", "EN", "English", "english":
return "en"
case "2", "zh", "ZH", "中文", "chinese", "Chinese":
case "2", "zh", "ZH", "chinese", "Chinese":
return "zh"
}
return ""
@@ -416,26 +416,26 @@ func parseLangChoice(text string) string {
func helpMsg(lang string) string {
if lang == "zh" {
return `*NOFX 使用指南*
return `*NOFX Help*
*查询*
• "查看我的持仓"
• "账户余额多少"
• "列出我的交易员"
*Query*
• "Show my positions"
• "What's my balance?"
• "List my traders"
*创建 & 启动*
• "帮我创建 BTC 趋势策略并跑起来"
• "保守型策略,只交易 BTC ETH"
*Create & start*
• "Create a BTC trend strategy and start it"
• "Conservative strategy, BTC and ETH only"
*控制*
• "启动交易员"
• "暂停交易员"
• "停止所有交易"
*Control*
• "Start trader"
• "Pause trader"
• "Stop all trading"
*命令*
/start — 刷新状态
/lang — 切换语言
/help — 帮助`
*Commands*
/start — refresh status
/lang — change language
/help — show this`
}
return `*NOFX Help*