mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 00:36:56 +08:00
feat(solo): beginner-friendly onboarding — smart setup guide + direct config commands
start.sh:
- Interactive Telegram Bot Token prompt on first run
- Token format validation (must match 12345:ABC... pattern)
- Friendly step-by-step startup instructions after launch
telegram/bot.go:
- /start now shows context-aware setup guide based on actual config state:
- No AI model → explains how to configure, lists all providers
- AI model OK but no exchange → guides to configure exchange via chat
- All configured → full capabilities welcome message
- New: direct setup commands ('配置 deepseek sk-xxx') bypass LLM entirely
so AI model can be configured even before any model exists (bootstrap fix)
- All messages now in Chinese (匹配用户语言)
telegram/agent/prompt.go:
- Added first-time setup detection section
- Agent told to never ask user to visit web UI — everything via chat
This commit is contained in:
77
start.sh
77
start.sh
@@ -207,33 +207,90 @@ check_database() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# First-time Setup: Telegram Bot Token
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
setup_telegram() {
|
||||||
|
if is_env_configured "TELEGRAM_BOT_TOKEN"; then
|
||||||
|
local token=$(grep "^TELEGRAM_BOT_TOKEN=" .env | cut -d'=' -f2- | tr -d '"'"'")
|
||||||
|
print_success "Telegram Bot Token 已配置: ${token:0:10}..."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo -e "${CYAN} 🤖 Telegram Bot 配置(必须)${NC}"
|
||||||
|
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
echo ""
|
||||||
|
echo " 还没有 Bot Token?按以下步骤获取:"
|
||||||
|
echo ""
|
||||||
|
echo " 1. 打开 Telegram,搜索 @BotFather"
|
||||||
|
echo " 2. 发送 /newbot"
|
||||||
|
echo " 3. 输入机器人名字(随意,如 MyTradingBot)"
|
||||||
|
echo " 4. 输入用户名(必须以 bot 结尾,如 my_trading_bot)"
|
||||||
|
echo " 5. BotFather 会给你一串 Token,格式如:"
|
||||||
|
echo " 1234567890:AABBCCDDEEFFaabbccddeeff..."
|
||||||
|
echo ""
|
||||||
|
while true; do
|
||||||
|
read -p " 请粘贴 Bot Token: " bot_token
|
||||||
|
bot_token=$(echo "$bot_token" | tr -d ' ')
|
||||||
|
if [ -z "$bot_token" ]; then
|
||||||
|
print_warning "Token 不能为空,请重新输入"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ ! "$bot_token" =~ ^[0-9]+:.+ ]]; then
|
||||||
|
print_warning "Token 格式不对(应为 数字:字母, 如 123456:ABC...)请重试"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
break
|
||||||
|
done
|
||||||
|
set_env_var "TELEGRAM_BOT_TOKEN" "$bot_token"
|
||||||
|
print_success "Bot Token 已保存 ✅"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
# Service Management: Start
|
# Service Management: Start
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
start() {
|
start() {
|
||||||
print_info "正在启动 NOFX AI Trading System..."
|
echo ""
|
||||||
|
echo -e "${CYAN}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${CYAN}║ 🚀 NOFX 个人 AI 交易机器人 启动向导 ║${NC}"
|
||||||
|
echo -e "${CYAN}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
read_env_vars
|
read_env_vars
|
||||||
|
|
||||||
if [ ! -d "data" ]; then
|
if [ ! -d "data" ]; then
|
||||||
print_info "创建数据目录..."
|
|
||||||
install -m 700 -d data
|
install -m 700 -d data
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Interactive setup for first-time users
|
||||||
|
setup_telegram
|
||||||
|
|
||||||
|
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||||
|
print_info "正在启动服务..."
|
||||||
|
|
||||||
if [ "$1" == "--build" ]; then
|
if [ "$1" == "--build" ]; then
|
||||||
print_info "重新构建镜像..."
|
|
||||||
$COMPOSE_CMD up -d --build
|
$COMPOSE_CMD up -d --build
|
||||||
else
|
else
|
||||||
print_info "启动容器..."
|
|
||||||
$COMPOSE_CMD up -d
|
$COMPOSE_CMD up -d
|
||||||
fi
|
fi
|
||||||
|
|
||||||
print_success "服务已启动!"
|
echo ""
|
||||||
print_info "Web 界面: http://localhost:${NOFX_FRONTEND_PORT}"
|
echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
print_info "API 端点: http://localhost:${NOFX_BACKEND_PORT}"
|
echo -e "${GREEN}║ ✅ 启动成功!接下来: ║${NC}"
|
||||||
print_info ""
|
echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
print_info "查看日志: ./start.sh logs"
|
echo ""
|
||||||
print_info "停止服务: ./start.sh stop"
|
echo " 1. 打开 Telegram,找到你的机器人"
|
||||||
|
echo " 2. 发送 /start 绑定账号"
|
||||||
|
echo " 3. 机器人会引导你完成 AI 模型和交易所配置"
|
||||||
|
echo " 4. 配置完成后,直接发消息让机器人帮你交易"
|
||||||
|
echo ""
|
||||||
|
echo -e " Web 管理界面: ${BLUE}http://localhost:${NOFX_FRONTEND_PORT}${NC}(可选)"
|
||||||
|
echo -e " 查看日志: ${YELLOW}./start.sh logs${NC}"
|
||||||
|
echo -e " 停止服务: ${YELLOW}./start.sh stop${NC}"
|
||||||
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ Use the api_request tool to call the NOFX REST API:
|
|||||||
4. When user provides enough info, act immediately — no confirmation needed
|
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
|
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)
|
## Verification Rule (CRITICAL)
|
||||||
After ANY PUT or POST that creates or modifies a resource:
|
After ANY PUT or POST that creates or modifies a resource:
|
||||||
1. Immediately GET the resource to read actual saved values
|
1. Immediately GET the resource to read actual saved values
|
||||||
|
|||||||
214
telegram/bot.go
214
telegram/bot.go
@@ -1,6 +1,11 @@
|
|||||||
package telegram
|
package telegram
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"nofx/api"
|
"nofx/api"
|
||||||
"nofx/config"
|
"nofx/config"
|
||||||
"nofx/logger"
|
"nofx/logger"
|
||||||
@@ -8,6 +13,7 @@ import (
|
|||||||
"nofx/store"
|
"nofx/store"
|
||||||
"nofx/telegram/agent"
|
"nofx/telegram/agent"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -105,26 +111,24 @@ func runBot(token string, cfg *config.Config, st *store.Store) bool {
|
|||||||
chatID := update.Message.Chat.ID
|
chatID := update.Message.Chat.ID
|
||||||
text := update.Message.Text
|
text := update.Message.Text
|
||||||
|
|
||||||
// Handle /start: auto-bind or welcome
|
// Handle /start: auto-bind or welcome with setup guide
|
||||||
if text == "/start" {
|
if text == "/start" {
|
||||||
if allowedChatID == 0 {
|
if allowedChatID == 0 {
|
||||||
// First user to /start becomes the bound admin
|
|
||||||
username := update.Message.From.UserName
|
username := update.Message.From.UserName
|
||||||
if err := st.TelegramConfig().BindUser(chatID, "@"+username); err != nil {
|
if err := st.TelegramConfig().BindUser(chatID, "@"+username); err != nil {
|
||||||
logger.Errorf("Failed to bind Telegram user: %v", err)
|
logger.Errorf("Failed to bind Telegram user: %v", err)
|
||||||
sendMsg(bot, chatID, "Binding failed, please check server logs.")
|
sendMsg(bot, chatID, "绑定失败,请查看服务器日志。")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
allowedChatID = chatID
|
allowedChatID = chatID
|
||||||
logger.Infof("Telegram bound to @%s (chatID: %d)", username, chatID)
|
logger.Infof("Telegram bound to @%s (chatID: %d)", username, chatID)
|
||||||
sendMsg(bot, chatID, "Bound successfully! "+welcomeMessage())
|
} else if chatID != allowedChatID {
|
||||||
} else if chatID == allowedChatID {
|
sendMsg(bot, chatID, "该机器人已被其他用户绑定。")
|
||||||
// Already bound, same user: reset session and show welcome
|
continue
|
||||||
agents.Reset(chatID)
|
|
||||||
sendMsg(bot, chatID, welcomeMessage())
|
|
||||||
} else {
|
} else {
|
||||||
sendMsg(bot, chatID, "This bot is already bound to another user.")
|
agents.Reset(chatID)
|
||||||
}
|
}
|
||||||
|
sendMsg(bot, chatID, buildSetupGuide(st, botUserID, cfg.APIServerPort, botToken))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,15 +140,27 @@ func runBot(token string, cfg *config.Config, st *store.Store) bool {
|
|||||||
|
|
||||||
// Access control
|
// Access control
|
||||||
if allowedChatID != 0 && chatID != allowedChatID {
|
if allowedChatID != 0 && chatID != allowedChatID {
|
||||||
sendMsg(bot, chatID, "Unauthorized access.")
|
sendMsg(bot, chatID, "无权限访问。")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if allowedChatID == 0 {
|
if allowedChatID == 0 {
|
||||||
sendMsg(bot, chatID, "Please send /start to bind your account first.")
|
sendMsg(bot, chatID, "请先发送 /start 绑定账号。")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if text == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if text == "" {
|
// Direct setup commands — handled without LLM so they work even before
|
||||||
|
// an AI model is configured. Format: "配置 <provider> <api-key>"
|
||||||
|
if reply, handled := tryHandleSetupCommand(text, cfg.APIServerPort, botToken, st, botUserID); handled {
|
||||||
|
sendMsg(bot, chatID, reply)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if AI model is configured before entering agent loop.
|
||||||
|
if newLLMClient(st, botUserID) == nil {
|
||||||
|
sendMsg(bot, chatID, buildSetupGuide(st, botUserID, cfg.APIServerPort, botToken))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,50 +286,158 @@ func clientForProvider(provider string) mcp.AIClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func welcomeMessage() string {
|
// buildSetupGuide checks the current configuration state and returns a contextual
|
||||||
return `*NOFX Trading Assistant Connected!*
|
// onboarding message. Called on every /start so the user always knows what to do next.
|
||||||
|
func buildSetupGuide(st *store.Store, userID string, apiPort int, botToken string) string {
|
||||||
|
// Step 1: AI model configured?
|
||||||
|
if _, err := st.AIModel().GetDefault(userID); err != nil {
|
||||||
|
return `🤖 *NOFX 个人 AI 交易助手*
|
||||||
|
|
||||||
You can manage your trading system with natural language:
|
欢迎!在开始交易前,需要先配置 AI 模型。
|
||||||
|
|
||||||
*Query*
|
*第一步:配置 AI 模型*
|
||||||
- Show current positions
|
|
||||||
- Show account balance
|
|
||||||
|
|
||||||
*Control*
|
选择你有账号的服务商,发送以下格式的消息:
|
||||||
- Start trader
|
|
||||||
- Stop trader
|
|
||||||
|
|
||||||
*Configure*
|
` + "```" + `
|
||||||
- Create a BTC strategy with 8% stop loss
|
配置 deepseek 你的API-Key
|
||||||
- Configure Binance exchange API
|
配置 openai 你的API-Key
|
||||||
- Add DeepSeek AI model
|
配置 claude 你的API-Key
|
||||||
- Update strategy prompt
|
配置 qwen 你的API-Key
|
||||||
|
配置 kimi 你的API-Key
|
||||||
|
配置 grok 你的API-Key
|
||||||
|
配置 gemini 你的API-Key
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
Send /help for detailed help
|
*推荐*:DeepSeek 价格最低,效果很好
|
||||||
Send /start to reset session`
|
获取 Key:https://platform.deepseek.com/api_keys`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Exchange configured?
|
||||||
|
exchanges, _ := st.Exchange().List(userID)
|
||||||
|
hasEnabled := false
|
||||||
|
for _, e := range exchanges {
|
||||||
|
if e.Enabled {
|
||||||
|
hasEnabled = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasEnabled {
|
||||||
|
return `✅ AI 模型已配置!
|
||||||
|
|
||||||
|
*第二步:配置交易所*
|
||||||
|
|
||||||
|
直接发消息告诉我交易所信息,例如:
|
||||||
|
|
||||||
|
_"帮我配置 OKX 交易所,API Key 是 xxx,Secret 是 xxx,Passphrase 是 xxx"_
|
||||||
|
|
||||||
|
_"帮我配置 Binance,API Key 是 xxx,Secret Key 是 xxx"_
|
||||||
|
|
||||||
|
_"帮我配置 Bybit,API Key 是 xxx,Secret Key 是 xxx"_
|
||||||
|
|
||||||
|
交易所 API Key 去交易所官网 → 账户设置 → API 管理 → 新建 Key(需要开启合约交易权限)`
|
||||||
|
}
|
||||||
|
|
||||||
|
// All configured — show full capabilities
|
||||||
|
return `✅ *NOFX 交易助手已就绪*
|
||||||
|
|
||||||
|
直接用自然语言告诉我你要做什么:
|
||||||
|
|
||||||
|
*查询*
|
||||||
|
_"查看我的持仓"_、_"查看账户余额"_
|
||||||
|
|
||||||
|
*创建并启动交易*
|
||||||
|
_"帮我创建一个 BTC 趋势策略并跑起来"_
|
||||||
|
_"创建保守型策略,只交易 BTC 和 ETH"_
|
||||||
|
|
||||||
|
*控制*
|
||||||
|
_"启动交易员"_、_"暂停交易员"_
|
||||||
|
|
||||||
|
*查看数据*
|
||||||
|
_"查看今天的交易记录"_、_"查看盈亏统计"_
|
||||||
|
|
||||||
|
发送 /start 重置对话 | /help 查看更多`
|
||||||
|
}
|
||||||
|
|
||||||
|
// tryHandleSetupCommand handles "配置 <provider> <api-key>" commands directly
|
||||||
|
// without going through the LLM. This allows AI model setup even before any
|
||||||
|
// model is configured (bootstrapping problem).
|
||||||
|
func tryHandleSetupCommand(text string, apiPort int, botToken string, st *store.Store, userID string) (string, bool) {
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
if !strings.HasPrefix(text, "配置 ") && !strings.HasPrefix(strings.ToLower(text), "setup ") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Fields(text)
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return "格式:配置 <服务商> <API-Key>\n例如:配置 deepseek sk-xxxxxxxxx", true
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := strings.ToLower(parts[1])
|
||||||
|
apiKey := parts[2]
|
||||||
|
|
||||||
|
validProviders := map[string]bool{
|
||||||
|
"openai": true, "deepseek": true, "claude": true,
|
||||||
|
"qwen": true, "kimi": true, "grok": true, "gemini": true,
|
||||||
|
}
|
||||||
|
if !validProviders[provider] {
|
||||||
|
return fmt.Sprintf("不支持的服务商:%s\n支持:openai / deepseek / claude / qwen / kimi / grok / gemini", provider), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call PUT /api/models directly without LLM.
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"models": map[string]any{
|
||||||
|
provider: map[string]any{
|
||||||
|
"enabled": true,
|
||||||
|
"api_key": apiKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
req, err := http.NewRequest("PUT", fmt.Sprintf("http://127.0.0.1:%d/api/models", apiPort), bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return "配置请求失败,请稍后重试", true
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+botToken)
|
||||||
|
|
||||||
|
resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "无法连接到服务,请确认服务正常运行", true
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
respBody, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return fmt.Sprintf("配置失败(%d):%s", resp.StatusCode, string(respBody)), true
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("Bot: setup command configured provider=%s", provider)
|
||||||
|
return fmt.Sprintf("✅ %s 配置成功!\n\n发送 /start 查看下一步", provider), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func helpMessage() string {
|
func helpMessage() string {
|
||||||
return `*NOFX Trading Assistant Guide*
|
return `*NOFX 使用指南*
|
||||||
|
|
||||||
*Query examples:*
|
*查询*
|
||||||
- "Show current positions"
|
- "查看我的持仓"
|
||||||
- "Show account balance"
|
- "查看账户余额"
|
||||||
- "List my traders"
|
- "列出我的交易员"
|
||||||
|
|
||||||
*Control examples:*
|
*交易控制*
|
||||||
- "Start trader"
|
- "启动交易员"
|
||||||
- "Stop trader [name]"
|
- "暂停 xxx 交易员"
|
||||||
|
|
||||||
*Configure examples:*
|
*创建策略*
|
||||||
- "Create a BTC strategy with RSI+MACD, 8% stop loss, 20% max position"
|
- "帮我创建 BTC 趋势策略并跑起来"
|
||||||
- "Configure Binance exchange, API Key is xxx, Secret is xxx"
|
- "创建保守型策略,BTC ETH,止损 8%"
|
||||||
- "Add DeepSeek model, Key is xxx"
|
|
||||||
- "Update strategy prompt for my main strategy to: you are a conservative trader..."
|
|
||||||
|
|
||||||
*Other commands:*
|
*配置*(不需要 AI,直接发)
|
||||||
- /start - Reset current session
|
- "配置 deepseek sk-xxxx"
|
||||||
- /help - Show this help
|
- "帮我配置 OKX 交易所,Key 是 xxx"
|
||||||
|
|
||||||
You can use natural language — no need to memorize specific command formats.`
|
*命令*
|
||||||
|
/start - 重置对话 / 查看配置状态
|
||||||
|
/help - 显示此帮助
|
||||||
|
|
||||||
|
支持中文和英文,直接说你想做什么就行。`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user