diff --git a/start.sh b/start.sh index 8ce534b3..5ebbfb72 100755 --- a/start.sh +++ b/start.sh @@ -207,33 +207,90 @@ check_database() { 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 # ------------------------------------------------------------------------ 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 if [ ! -d "data" ]; then - print_info "创建数据目录..." install -m 700 -d data fi + # Interactive setup for first-time users + setup_telegram + + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + print_info "正在启动服务..." + if [ "$1" == "--build" ]; then - print_info "重新构建镜像..." $COMPOSE_CMD up -d --build else - print_info "启动容器..." $COMPOSE_CMD up -d fi - print_success "服务已启动!" - print_info "Web 界面: http://localhost:${NOFX_FRONTEND_PORT}" - print_info "API 端点: http://localhost:${NOFX_BACKEND_PORT}" - print_info "" - print_info "查看日志: ./start.sh logs" - print_info "停止服务: ./start.sh stop" + echo "" + echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ ✅ 启动成功!接下来: ║${NC}" + echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}" + echo "" + 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 "" } # ------------------------------------------------------------------------ diff --git a/telegram/agent/prompt.go b/telegram/agent/prompt.go index 56629b59..fb89e10a 100644 --- a/telegram/agent/prompt.go +++ b/telegram/agent/prompt.go @@ -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 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 diff --git a/telegram/bot.go b/telegram/bot.go index c26298a7..848250f1 100644 --- a/telegram/bot.go +++ b/telegram/bot.go @@ -1,6 +1,11 @@ package telegram import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" "nofx/api" "nofx/config" "nofx/logger" @@ -8,6 +13,7 @@ import ( "nofx/store" "nofx/telegram/agent" "os" + "strings" "sync" "time" @@ -105,26 +111,24 @@ func runBot(token string, cfg *config.Config, st *store.Store) bool { chatID := update.Message.Chat.ID text := update.Message.Text - // Handle /start: auto-bind or welcome + // Handle /start: auto-bind or welcome with setup guide if text == "/start" { if allowedChatID == 0 { - // First user to /start becomes the bound admin username := update.Message.From.UserName if err := st.TelegramConfig().BindUser(chatID, "@"+username); err != nil { logger.Errorf("Failed to bind Telegram user: %v", err) - sendMsg(bot, chatID, "Binding failed, please check server logs.") + sendMsg(bot, chatID, "绑定失败,请查看服务器日志。") continue } allowedChatID = chatID logger.Infof("Telegram bound to @%s (chatID: %d)", username, chatID) - sendMsg(bot, chatID, "Bound successfully! "+welcomeMessage()) - } else if chatID == allowedChatID { - // Already bound, same user: reset session and show welcome - agents.Reset(chatID) - sendMsg(bot, chatID, welcomeMessage()) + } else if chatID != allowedChatID { + sendMsg(bot, chatID, "该机器人已被其他用户绑定。") + continue } 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 } @@ -136,15 +140,27 @@ func runBot(token string, cfg *config.Config, st *store.Store) bool { // Access control if allowedChatID != 0 && chatID != allowedChatID { - sendMsg(bot, chatID, "Unauthorized access.") + sendMsg(bot, chatID, "无权限访问。") continue } if allowedChatID == 0 { - sendMsg(bot, chatID, "Please send /start to bind your account first.") + sendMsg(bot, chatID, "请先发送 /start 绑定账号。") + continue + } + if text == "" { continue } - if text == "" { + // Direct setup commands — handled without LLM so they work even before + // an AI model is configured. Format: "配置 " + 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 } @@ -270,50 +286,158 @@ func clientForProvider(provider string) mcp.AIClient { } } -func welcomeMessage() string { - return `*NOFX Trading Assistant Connected!* +// buildSetupGuide checks the current configuration state and returns a contextual +// 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* -- Show current positions -- Show account balance +*第一步:配置 AI 模型* -*Control* -- Start trader -- Stop trader +选择你有账号的服务商,发送以下格式的消息: -*Configure* -- Create a BTC strategy with 8% stop loss -- Configure Binance exchange API -- Add DeepSeek AI model -- Update strategy prompt +` + "```" + ` +配置 deepseek 你的API-Key +配置 openai 你的API-Key +配置 claude 你的API-Key +配置 qwen 你的API-Key +配置 kimi 你的API-Key +配置 grok 你的API-Key +配置 gemini 你的API-Key +` + "```" + ` -Send /help for detailed help -Send /start to reset session` +*推荐*:DeepSeek 价格最低,效果很好 +获取 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 "配置 " 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 "格式:配置 <服务商> \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 { - 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" -- "Configure Binance exchange, API Key is xxx, Secret is xxx" -- "Add DeepSeek model, Key is xxx" -- "Update strategy prompt for my main strategy to: you are a conservative trader..." +*创建策略* +- "帮我创建 BTC 趋势策略并跑起来" +- "创建保守型策略,BTC ETH,止损 8%" -*Other commands:* -- /start - Reset current session -- /help - Show this help +*配置*(不需要 AI,直接发) +- "配置 deepseek sk-xxxx" +- "帮我配置 OKX 交易所,Key 是 xxx" -You can use natural language — no need to memorize specific command formats.` +*命令* +/start - 重置对话 / 查看配置状态 +/help - 显示此帮助 + +支持中文和英文,直接说你想做什么就行。` }