docs/plans/2026-03-06-telegram-bot.md
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: 在 NOFX 单进程内内置 Telegram Bot,用户通过自然语言(LLM 解析意图)在 Telegram 配置策略、交易所、大模型、交易员、查询持仓、控制交易。
Architecture: 新增 telegram/ 包,单一 Facade 层(service/nofx.go)作为唯一接触 NOFX 内部的边界,借鉴 openclaw compaction 模式实现多轮对话记忆压缩,main.go 仅增加 3 行。
Tech Stack: Go, github.com/go-telegram-bot-api/telegram-bot-api/v5(已在 go.mod), nofx/mcp(复用现有 LLM 客户端)
这份文档里的代码块只能当伪代码参考,不能直接照抄。当前仓库真实接口和文档示例存在多处偏差,首轮实现必须以编译通过的仓库接口为准。
manager.TraderManager 没有 StartTrader / StopTrader 方法。
LoadUserTradersFromStore() -> GetTrader() -> go trader.Run() -> store.Trader().UpdateStatus(userID, traderID, true)GetTrader() -> 检查 GetStatus()["is_running"] -> Stop() -> UpdateStatus(..., false)store 方法签名与文档示例不一致,必须按真实接口实现:
store.Trader().List(userID) 返回 []*store.Traderstore.Trader() 没有 Get(traderID),常用的是 GetFullConfig(userID, traderID)store.Strategy().Get(userID, id string),Strategy.ID 是 string,不是 uintstore.AIModel().Create(...) 返回 error,不是 *store.AIModelstore.Exchange().Create(...) 返回 (string, error),不是 *store.Exchangestore.Exchange() 读单条配置用 GetByID(userID, id)store.Equity() 没有 Latest,现有方法是 GetLatest(traderID, limit)store.Position() 没有 ListByTradermcp.New() 在当前仓库中不存在。
mcp.NewDeepSeekClient()、mcp.NewClient(...),或新增一个显式 helper。策略创建不能直接拼一个“猜测字段”的 JSON。
store.StrategyConfigstore.GetDefaultStrategyConfig("zh") 起步,修改需要的字段,再 json.MarshalStrategy.ID 需要像现有 API 一样使用 uuid.New().String()“修改策略 Prompt” 不能按文档示例那样直接改 Strategy.CustomPrompt。
store.Strategy 没有这个顶层字段strategy.Config -> ParseConfig() -> 更新 StrategyConfig.CustomPrompt 或相关 prompt section -> 序列化回 strategy.Config -> Update(strategy)/start 的“完全重置”与当前伪代码冲突。
Memory.Reset() 只清空短期历史,不清空长期摘要/start 要“重置会话”,就必须新增 ClearAll() 或重建 Memory不要在 Telegram 回复里默认启用 Markdown parse mode。
不要在日志、回复、错误信息中回显敏感字段。
api_keysecret_keypassphrase首个可交付版本只做“最小可用闭环”,不要一口气把所有写操作做满:
必做:
/start 重置会话list traders / query positions / query equitystart trader / stop trader第二阶段再做:
config_strategyconfig_exchangeconfig_modelconfig_traderupdate_promptcontrol_close 先不要做,除非先找到仓库里现成且安全的平仓入口。
go build ./telegram/...go build ./...handler/ 不允许直接碰 store/ 或 manager/telegram/service/nofx.go 进入telegram/
├── bot.go # 新建:Bot 启动、消息收发路由
├── session/
│ ├── session.go # 新建:会话状态(当前意图、进度)
│ └── memory.go # 新建:对话记忆 + 自动压缩
├── intent/
│ └── parser.go # 新建:LLM 意图解析
├── service/
│ └── nofx.go # 新建:Facade(唯一接触 store/manager 的地方)
└── handler/
└── handler.go # 新建:业务路由,只调 service/ 和 intent/
config/config.go # 修改:加 TelegramBotToken, TelegramAdminChatID
main.go # 修改:加 3 行启动 Telegram Bot
Files:
config/config.goStep 1: 在 Config struct 末尾加两个字段
// Telegram Bot configuration
TelegramBotToken string // TELEGRAM_BOT_TOKEN
TelegramAdminChatID int64 // TELEGRAM_ADMIN_CHAT_ID (only this user can operate)
Step 2: 在 Init() 函数的解析段加读取逻辑
找到 Init() 函数中 os.Getenv 的模式,加:
cfg.TelegramBotToken = os.Getenv("TELEGRAM_BOT_TOKEN")
if chatIDStr := os.Getenv("TELEGRAM_ADMIN_CHAT_ID"); chatIDStr != "" {
if id, err := strconv.ParseInt(chatIDStr, 10, 64); err == nil {
cfg.TelegramAdminChatID = id
}
}
监工补充: Init() 函数里当前一直在填充局部变量 cfg,最后才赋值给 global,这里不能提前写 global.TelegramBotToken
Step 3: 构建验证
cd /Users/yida/gopro/open-nofx && go build ./...
Expected: 无错误
Step 4: Commit
git add config/config.go
git commit -m "feat(telegram): add TelegramBotToken and TelegramAdminChatID to config"
Files:
telegram/service/nofx.go这是唯一接触 NOFX 内部(store、manager)的文件。handler 不直接碰 store/manager。
Step 1: 创建文件
package service
import (
"fmt"
"nofx/manager"
"nofx/store"
)
// NofxService is the single facade between Telegram bot and NOFX internals.
// All store/manager access MUST go through this layer.
type NofxService struct {
store *store.Store
manager *manager.TraderManager
userID string // fixed user ID for single-user mode: "default"
}
func New(st *store.Store, tm *manager.TraderManager) *NofxService {
return &NofxService{store: st, manager: tm, userID: "default"}
}
// --- Trader ---
func (s *NofxService) ListTraders() ([]store.Trader, error) {
return s.store.Trader().List(s.userID)
}
func (s *NofxService) StartTrader(traderID string) error {
t, err := s.store.Trader().Get(traderID)
if err != nil {
return fmt.Errorf("trader not found: %w", err)
}
return s.manager.StartTrader(t, s.store)
}
func (s *NofxService) StopTrader(traderID string) error {
return s.manager.StopTrader(traderID)
}
// --- Strategy ---
func (s *NofxService) ListStrategies() ([]store.Strategy, error) {
return s.store.Strategy().List(s.userID)
}
func (s *NofxService) CreateStrategy(name string, configJSON string) (*store.Strategy, error) {
strategy := &store.Strategy{
UserID: s.userID,
Name: name,
Config: configJSON,
}
if err := s.store.Strategy().Create(strategy); err != nil {
return nil, err
}
return strategy, nil
}
func (s *NofxService) UpdateStrategyPrompt(strategyID uint, prompt string) error {
strategy, err := s.store.Strategy().Get(strategyID)
if err != nil {
return err
}
strategy.CustomPrompt = prompt
return s.store.Strategy().Update(strategy)
}
// --- AI Model ---
func (s *NofxService) ListModels() ([]store.AIModel, error) {
return s.store.AIModel().List(s.userID)
}
func (s *NofxService) CreateModel(provider, apiKey, model string) (*store.AIModel, error) {
m := &store.AIModel{
UserID: s.userID,
Provider: provider,
APIKey: apiKey,
Model: model,
}
if err := s.store.AIModel().Create(m); err != nil {
return nil, err
}
return m, nil
}
// --- Exchange ---
func (s *NofxService) ListExchanges() ([]store.Exchange, error) {
return s.store.Exchange().List(s.userID)
}
func (s *NofxService) CreateExchange(exchangeType, apiKey, secretKey string) (*store.Exchange, error) {
ex := &store.Exchange{
UserID: s.userID,
ExchangeType: exchangeType,
APIKey: apiKey,
SecretKey: secretKey,
}
if err := s.store.Exchange().Create(ex); err != nil {
return nil, err
}
return ex, nil
}
// --- Positions / Query ---
func (s *NofxService) GetPositions(traderID string) ([]store.TraderPosition, error) {
return s.store.Position().ListByTrader(traderID)
}
func (s *NofxService) GetEquitySummary(traderID string) (*store.EquitySnapshot, error) {
return s.store.Equity().Latest(traderID)
}
Step 2: 注意事项
store 的方法名称(List、Get、Create、Update)需要根据实际 store 接口调整。运行 go build ./telegram/... 后根据编译错误逐一对齐方法名。
监工补充:这一节不能照抄上面的示例实现,至少要修正以下事实
ListTraders() / ListStrategies() / ListModels() / ListExchanges() 的返回值都应与真实 store 一致,当前仓库大多是指针切片StartTrader() / StopTrader() 不能调用不存在的 manager 方法,必须镜像 api/server.go 的启动/停止流程CreateStrategy() 不能假设 Strategy.ID 是整数;请复用现有 API 的 uuid.New().String() 方案CreateModel() / CreateExchange() 不能假设 store 会返回新建对象;真实接口要么返回 error,要么返回 (id, error)GetPositions() / GetEquitySummary() 需要在 service 内封装真实查询逻辑,不能调用仓库中不存在的 ListByTrader() / Latest()Step 3: Build 验证
cd /Users/yida/gopro/open-nofx && go build ./telegram/...
Expected: 只可能有 store 方法名不匹配的错误,逐一修正即可。
Step 4: Commit
git add telegram/service/nofx.go
git commit -m "feat(telegram): add NofxService facade layer"
Files:
telegram/session/memory.go借鉴 openclaw compaction 模式:token 超阈值 → LLM 静默压缩 → 写入长期记忆 → 清空短期历史。
Step 1: 创建文件
package session
import (
"fmt"
"nofx/mcp"
"strings"
)
const (
// When short-term history exceeds this token estimate, trigger compaction
compactionThresholdTokens = 3000
// Rough estimate: 1 token ≈ 4 chars (Chinese ~2 chars/token)
charsPerToken = 3
)
// Message represents a single conversation turn
type Message struct {
Role string // "user" or "assistant"
Content string
}
// Memory manages conversation history with automatic compaction.
// Inspired by openclaw's compaction pattern.
type Memory struct {
LongTerm string // Durable summary (survives compaction)
ShortTerm []Message // Recent conversation (cleared on compaction)
llm mcp.AIClient
}
func NewMemory(llm mcp.AIClient) *Memory {
return &Memory{llm: llm}
}
// Add appends a message and triggers compaction if needed
func (m *Memory) Add(role, content string) {
m.ShortTerm = append(m.ShortTerm, Message{Role: role, Content: content})
if m.estimateTokens() > compactionThresholdTokens {
m.compact()
}
}
// BuildContext returns context string for LLM intent parsing
func (m *Memory) BuildContext() string {
var sb strings.Builder
if m.LongTerm != "" {
sb.WriteString("【历史摘要】\n")
sb.WriteString(m.LongTerm)
sb.WriteString("\n\n")
}
if len(m.ShortTerm) > 0 {
sb.WriteString("【近期对话】\n")
for _, msg := range m.ShortTerm {
sb.WriteString(fmt.Sprintf("%s: %s\n", msg.Role, msg.Content))
}
}
return sb.String()
}
// Reset clears session (called on /start or new session)
func (m *Memory) Reset() {
m.ShortTerm = []Message{}
// LongTerm is preserved intentionally
}
func (m *Memory) estimateTokens() int {
total := len(m.LongTerm)
for _, msg := range m.ShortTerm {
total += len(msg.Content)
}
return total / charsPerToken
}
// compact summarizes short-term history into long-term memory (silent, user doesn't see this)
func (m *Memory) compact() {
if m.llm == nil || len(m.ShortTerm) == 0 {
return
}
history := m.BuildContext()
systemPrompt := `你是一个对话摘要助手。将以下交易配置对话压缩为简洁摘要。
必须保留:
- 用户正在配置什么(策略/交易所/大模型/交易员)
- 已确认的参数(交易对、杠杆、止损比例、指标等)
- 待确认或缺失的参数
- 用户表达的偏好和要求
输出格式:纯文本摘要,不超过200字。`
summary, err := m.llm.CallWithMessages(systemPrompt, history)
if err != nil {
// Compaction failed: keep short-term as-is, don't lose data
return
}
// Write summary to long-term, clear short-term
if m.LongTerm != "" {
m.LongTerm = m.LongTerm + "\n" + summary
} else {
m.LongTerm = summary
}
m.ShortTerm = []Message{}
}
Step 2: Build 验证
cd /Users/yida/gopro/open-nofx && go build ./telegram/...
Step 3: Commit
git add telegram/session/memory.go
git commit -m "feat(telegram): add conversation memory with openclaw-style compaction"
Files:
telegram/session/session.goStep 1: 创建文件
package session
import (
"nofx/mcp"
"sync"
"time"
)
// Intent represents what the user is currently trying to do
type Intent string
const (
IntentNone Intent = ""
IntentConfigStrategy Intent = "config_strategy"
IntentConfigExchange Intent = "config_exchange"
IntentConfigModel Intent = "config_model"
IntentConfigTrader Intent = "config_trader"
IntentQueryPositions Intent = "query_positions"
IntentControlTrader Intent = "control_trader"
IntentUpdatePrompt Intent = "update_prompt"
)
// Session holds state for a single Telegram conversation
type Session struct {
ChatID int64
Intent Intent
Params map[string]string // collected parameters so far
Memory *Memory
UpdatedAt time.Time
}
// Manager manages all active sessions (one per chat ID)
type Manager struct {
mu sync.RWMutex
sessions map[int64]*Session
llm mcp.AIClient
}
func NewManager(llm mcp.AIClient) *Manager {
return &Manager{
sessions: make(map[int64]*Session),
llm: llm,
}
}
// Get returns or creates a session for the given chat ID
func (m *Manager) Get(chatID int64) *Session {
m.mu.Lock()
defer m.mu.Unlock()
s, ok := m.sessions[chatID]
if !ok {
s = &Session{
ChatID: chatID,
Intent: IntentNone,
Params: make(map[string]string),
Memory: NewMemory(m.llm),
UpdatedAt: time.Now(),
}
m.sessions[chatID] = s
}
s.UpdatedAt = time.Now()
return s
}
// Reset clears session intent and params (keeps memory)
func (s *Session) Reset() {
s.Intent = IntentNone
s.Params = make(map[string]string)
}
// ResetFull clears everything including memory (on /start command)
func (s *Session) ResetFull() {
s.Reset()
s.Memory.Reset()
}
监工补充:这里的伪代码与注释不一致
Memory.Reset() 只清空短期历史,不会清空 LongTerm/start 的产品语义是“完全重置”,这里必须改成真正清空长期摘要,或者直接新建一个 MemoryStep 2: Build 验证
cd /Users/yida/gopro/open-nofx && go build ./telegram/...
Step 3: Commit
git add telegram/session/session.go
git commit -m "feat(telegram): add session state manager"
Files:
telegram/intent/parser.go复用 nofx/mcp 的现有 LLM 客户端,不引入新依赖。
Step 1: 创建文件
package intent
import (
"encoding/json"
"nofx/mcp"
"strings"
)
// ParsedIntent is the structured output from LLM intent parsing
type ParsedIntent struct {
Action string `json:"action"` // e.g. "config_strategy", "query_positions"
Params map[string]string `json:"params"` // extracted parameters
Missing []string `json:"missing"` // params still needed
Reply string `json:"reply"` // what bot should say to user
}
const systemPrompt = `你是 NOFX 交易系统的对话助手。分析用户消息,提取交易配置意图和参数。
支持的操作(action):
- config_strategy: 创建/修改策略(需要:name, coins, indicators, max_position_pct, stop_loss_pct)
- config_exchange: 配置交易所(需要:exchange_type, api_key, secret_key)
- config_model: 配置大模型(需要:provider, api_key, model)
- config_trader: 配置交易员(需要:name, model_id, exchange_id, strategy_id)
- query_positions: 查询持仓(需要:trader_id 或 "all")
- query_equity: 查询账户余额/盈亏
- control_start: 启动交易员(需要:trader_id 或 trader_name)
- control_stop: 停止交易员(需要:trader_id 或 trader_name)
- control_close: 紧急平仓(需要:trader_id, symbol)
- update_prompt: 修改策略 Prompt(需要:strategy_id 或 strategy_name, prompt)
- unknown: 无法识别
输出严格 JSON 格式:
{
"action": "action_name",
"params": {"key": "value"},
"missing": ["param1", "param2"],
"reply": "对用户的回复(询问缺失参数或确认操作)"
}
安全要求:API Key 等敏感信息原样保留在 params 中,不要截断或修改。`
// Parser uses LLM to parse user message into structured intent
type Parser struct {
llm mcp.AIClient
}
func NewParser(llm mcp.AIClient) *Parser {
return &Parser{llm: llm}
}
// Parse sends user message + conversation context to LLM, returns structured intent
func (p *Parser) Parse(userMessage, conversationContext string) (*ParsedIntent, error) {
userPrompt := userMessage
if conversationContext != "" {
userPrompt = conversationContext + "\n\n【当前消息】\n" + userMessage
}
resp, err := p.llm.CallWithMessages(systemPrompt, userPrompt)
if err != nil {
return nil, err
}
// Extract JSON from response (LLM may wrap in markdown code block)
jsonStr := extractJSON(resp)
var result ParsedIntent
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
// Fallback: return unknown intent with raw response as reply
return &ParsedIntent{
Action: "unknown",
Reply: "抱歉,我没有理解你的意思。请描述你想做什么,例如:「帮我创建一个 BTC 策略」",
}, nil
}
return &result, nil
}
func extractJSON(s string) string {
// Strip markdown code block if present
s = strings.TrimSpace(s)
if idx := strings.Index(s, "```json"); idx >= 0 {
s = s[idx+7:]
} else if idx := strings.Index(s, "```"); idx >= 0 {
s = s[idx+3:]
}
if idx := strings.LastIndex(s, "```"); idx >= 0 {
s = s[:idx]
}
// Find first { to last }
start := strings.Index(s, "{")
end := strings.LastIndex(s, "}")
if start >= 0 && end > start {
return s[start : end+1]
}
return s
}
Step 2: Build 验证
cd /Users/yida/gopro/open-nofx && go build ./telegram/...
Step 3: Commit
git add telegram/intent/parser.go
git commit -m "feat(telegram): add LLM intent parser"
Files:
telegram/handler/handler.gohandler 只调 service/ 和 intent/,不直接碰 store/manager。
Step 1: 创建文件
package handler
import (
"fmt"
"nofx/telegram/intent"
"nofx/telegram/service"
"nofx/telegram/session"
"strings"
)
// Handler dispatches parsed intents to the right operation
type Handler struct {
svc *service.NofxService
parser *intent.Parser
sessions *session.Manager
}
func New(svc *service.NofxService, parser *intent.Parser, sessions *session.Manager) *Handler {
return &Handler{svc: svc, parser: parser, sessions: sessions}
}
// Handle processes a user message and returns the bot reply
func (h *Handler) Handle(chatID int64, userMessage string) string {
sess := h.sessions.Get(chatID)
// Record user message in memory
sess.Memory.Add("user", userMessage)
// Build conversation context for LLM
ctx := sess.Memory.BuildContext()
// Parse intent via LLM
parsed, err := h.parser.Parse(userMessage, ctx)
if err != nil {
return "❌ 解析失败,请重试"
}
// Merge newly extracted params into session
for k, v := range parsed.Params {
sess.Params[k] = v
}
// If there are missing params, ask user
if len(parsed.Missing) > 0 {
sess.Intent = session.Intent(parsed.Action)
reply := parsed.Reply
sess.Memory.Add("assistant", reply)
return reply
}
// Execute the action
reply := h.execute(sess, parsed)
sess.Memory.Add("assistant", reply)
sess.Reset() // clear intent after successful execution
return reply
}
func (h *Handler) execute(sess *session.Session, parsed *intent.ParsedIntent) string {
params := sess.Params
switch parsed.Action {
case "config_strategy":
return h.createStrategy(params)
case "config_exchange":
return h.createExchange(params)
case "config_model":
return h.createModel(params)
case "query_positions":
return h.queryPositions(params)
case "query_equity":
return h.queryEquity(params)
case "control_start":
return h.startTrader(params)
case "control_stop":
return h.stopTrader(params)
case "update_prompt":
return h.updatePrompt(params)
default:
return parsed.Reply
}
}
func (h *Handler) createStrategy(params map[string]string) string {
name := params["name"]
if name == "" {
name = "我的策略"
}
// Build a minimal strategy config JSON from params
// Full StrategyConfig is complex; we start with essential fields
configJSON := buildStrategyConfigJSON(params)
strategy, err := h.svc.CreateStrategy(name, configJSON)
if err != nil {
return fmt.Sprintf("❌ 创建策略失败: %v", err)
}
return fmt.Sprintf("✅ 策略「%s」已创建(ID: %d)\n\n配置摘要:\n%s", strategy.Name, strategy.ID, formatParams(params))
}
func (h *Handler) createExchange(params map[string]string) string {
exType := params["exchange_type"]
apiKey := params["api_key"]
secretKey := params["secret_key"]
ex, err := h.svc.CreateExchange(exType, apiKey, secretKey)
if err != nil {
return fmt.Sprintf("❌ 配置交易所失败: %v", err)
}
return fmt.Sprintf("✅ %s 交易所已配置(ID: %d)", ex.ExchangeType, ex.ID)
}
func (h *Handler) createModel(params map[string]string) string {
provider := params["provider"]
apiKey := params["api_key"]
model := params["model"]
m, err := h.svc.CreateModel(provider, apiKey, model)
if err != nil {
return fmt.Sprintf("❌ 配置大模型失败: %v", err)
}
return fmt.Sprintf("✅ %s (%s) 已配置(ID: %d)", m.Provider, m.Model, m.ID)
}
func (h *Handler) queryPositions(params map[string]string) string {
traderID := params["trader_id"]
if traderID == "" {
traders, err := h.svc.ListTraders()
if err != nil || len(traders) == 0 {
return "❌ 没有找到交易员"
}
traderID = traders[0].ID
}
positions, err := h.svc.GetPositions(traderID)
if err != nil {
return fmt.Sprintf("❌ 查询持仓失败: %v", err)
}
if len(positions) == 0 {
return "📭 当前无持仓"
}
var sb strings.Builder
sb.WriteString("📊 当前持仓:\n")
for _, p := range positions {
sb.WriteString(fmt.Sprintf("• %s %s | 入场: %.4f | 未实现P&L: %.2f USDT\n",
p.Symbol, p.Side, p.EntryPrice, p.UnrealizedPnl))
}
return sb.String()
}
func (h *Handler) queryEquity(params map[string]string) string {
traders, err := h.svc.ListTraders()
if err != nil || len(traders) == 0 {
return "❌ 没有找到交易员"
}
traderID := params["trader_id"]
if traderID == "" {
traderID = traders[0].ID
}
eq, err := h.svc.GetEquitySummary(traderID)
if err != nil {
return fmt.Sprintf("❌ 查询余额失败: %v", err)
}
return fmt.Sprintf("💰 账户余额:%.2f USDT", eq.TotalBalance)
}
func (h *Handler) startTrader(params map[string]string) string {
traderID := params["trader_id"]
if err := h.svc.StartTrader(traderID); err != nil {
return fmt.Sprintf("❌ 启动失败: %v", err)
}
return "✅ 交易员已启动"
}
func (h *Handler) stopTrader(params map[string]string) string {
traderID := params["trader_id"]
if err := h.svc.StopTrader(traderID); err != nil {
return fmt.Sprintf("❌ 停止失败: %v", err)
}
return "✅ 交易员已停止"
}
func (h *Handler) updatePrompt(params map[string]string) string {
// strategy_id must be numeric; convert from params
strategyIDStr := params["strategy_id"]
var strategyID uint
fmt.Sscanf(strategyIDStr, "%d", &strategyID)
prompt := params["prompt"]
if err := h.svc.UpdateStrategyPrompt(strategyID, prompt); err != nil {
return fmt.Sprintf("❌ 更新 Prompt 失败: %v", err)
}
return "✅ 策略 Prompt 已更新"
}
// buildStrategyConfigJSON builds a minimal valid StrategyConfig JSON from params
func buildStrategyConfigJSON(params map[string]string) string {
coins := params["coins"]
if coins == "" {
coins = "BTC"
}
stopLoss := params["stop_loss_pct"]
if stopLoss == "" {
stopLoss = "5"
}
maxPos := params["max_position_pct"]
if maxPos == "" {
maxPos = "20"
}
indicators := params["indicators"]
return fmt.Sprintf(`{
"strategy_type": "ai_trading",
"coin_source": {"source_type": "static", "static_coins": [%q]},
"indicators": {"enable_rsi": %v, "enable_macd": %v},
"risk_control": {"stop_loss_pct": %s, "max_position_pct": %s}
}`,
coins,
strings.Contains(indicators, "RSI"),
strings.Contains(indicators, "MACD"),
stopLoss,
maxPos,
)
}
func formatParams(params map[string]string) string {
var sb strings.Builder
for k, v := range params {
if k == "api_key" || k == "secret_key" {
v = "***"
}
sb.WriteString(fmt.Sprintf(" %s: %s\n", k, v))
}
return sb.String()
}
监工补充:这里至少有 6 个会直接出错或行为错误的点
当前写法会把“当前消息”重复注入 LLM 上下文。
sess.Memory.Add("user", userMessage) 已经把本轮消息写进历史parser.Parse(userMessage, ctx) 又会把 userMessage 拼到 conversationContext 后面Parse() 不再重复追加当前消息store.TraderPosition 没有 UnrealizedPnl 字段。
store.EquitySnapshot 没有 TotalBalance 字段,真实字段是 TotalEquity
strategy.ID 不是 %d,AIModel 也没有示例中的 Model 字段
buildStrategyConfigJSON() 示例不符合当前仓库真实 StrategyConfig
risk_control.stop_loss_pctrisk_control.max_position_pct
这些都不是当前结构里的真实字段名store.GetDefaultStrategyConfig("zh") 组装updatePrompt() 不能直接调用“按数值 strategyID 更新顶层 prompt”的假接口
Strategy.Config 里的 CustomPrompt 或 prompt sectionsTrader().UpdateCustomPrompt(...)Step 2: Build 验证
cd /Users/yida/gopro/open-nofx && go build ./telegram/...
Step 3: Commit
git add telegram/handler/handler.go
git commit -m "feat(telegram): add intent handler with 6 feature areas"
Files:
telegram/bot.goStep 1: 创建文件
package telegram
import (
"nofx/config"
"nofx/logger"
"nofx/manager"
"nofx/mcp"
"nofx/store"
"nofx/telegram/handler"
"nofx/telegram/intent"
"nofx/telegram/service"
"nofx/telegram/session"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
// Start initializes and runs the Telegram bot.
// Called from main.go as a goroutine.
func Start(cfg *config.Config, st *store.Store, tm *manager.TraderManager) {
if cfg.TelegramBotToken == "" {
logger.Info("📵 Telegram bot not configured (TELEGRAM_BOT_TOKEN not set), skipping")
return
}
bot, err := tgbotapi.NewBotAPI(cfg.TelegramBotToken)
if err != nil {
logger.Errorf("❌ Failed to start Telegram bot: %v", err)
return
}
logger.Infof("🤖 Telegram bot started: @%s", bot.Self.UserName)
// Build the LLM client for intent parsing (use DeepSeek by default)
llmClient := mcp.New()
// Configure with whatever key is available in env (intent parsing is lightweight)
// The service layer will use store to get user-configured models for actual trading
svc := service.New(st, tm)
parser := intent.NewParser(llmClient)
sessions := session.NewManager(llmClient)
h := handler.New(svc, parser, sessions)
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil {
continue
}
chatID := update.Message.Chat.ID
// Access control: only allow configured admin chat ID
if cfg.TelegramAdminChatID != 0 && chatID != cfg.TelegramAdminChatID {
msg := tgbotapi.NewMessage(chatID, "⛔ 未授权访问")
bot.Send(msg)
continue
}
text := update.Message.Text
if text == "" {
continue
}
// Handle /start command
if text == "/start" {
sessions.Get(chatID).ResetFull()
reply := tgbotapi.NewMessage(chatID, welcomeMessage())
bot.Send(reply)
continue
}
// Process message
reply := h.Handle(chatID, text)
msg := tgbotapi.NewMessage(chatID, reply)
msg.ParseMode = "Markdown"
bot.Send(msg)
}
}
func welcomeMessage() string {
return `👋 欢迎使用 NOFX 交易助手!
你可以用自然语言配置和管理你的交易系统:
📋 *配置功能*
• 「帮我创建一个 BTC 策略,RSI+MACD,止损 8%」
• 「配置 Binance 交易所」
• 「添加 DeepSeek 大模型」
• 「创建一个交易员」
📊 *查询功能*
• 「查看当前持仓」
• 「查看账户余额」
⚙️ *控制功能*
• 「启动交易员」
• 「停止交易员」
• 「修改策略 Prompt」
输入 /start 重置会话`
}
监工补充:本节伪代码需要先修正两个问题
mcp.New() 在当前仓库里不存在,必须改成真实可用的构造器msg.ParseMode = "Markdown" 首版不要开,先用纯文本,避免用户内容触发格式错误或意外转义Step 2: Build 验证
cd /Users/yida/gopro/open-nofx && go build ./telegram/...
Step 3: Commit
git add telegram/bot.go
git commit -m "feat(telegram): add Telegram bot entry point with access control"
Files:
main.goStep 1: 加 import
在 main.go 的 import 块加:
"nofx/telegram"
Step 2: 在 API Server 启动之后加 3 行
找到这段代码:
// Start API server
server := api.NewServer(...)
go func() { ... }()
在其后加:
// Start Telegram bot (if configured)
go telegram.Start(cfg, st, traderManager)
logger.Info("🤖 Telegram bot goroutine started")
Step 3: 完整构建
cd /Users/yida/gopro/open-nofx && go build -o nofx .
Expected: 成功编译,无错误
Step 4: Commit
git add main.go
git commit -m "feat(telegram): wire Telegram bot into main startup (3 lines)"
Files:
.env.example 或 .env(若存在)Step 1: 在 .env.example 末尾加
# Telegram Bot Configuration
# Get token from @BotFather on Telegram
TELEGRAM_BOT_TOKEN=
# Get your chat ID from @userinfobot on Telegram
TELEGRAM_ADMIN_CHAT_ID=
Step 2: Commit
git add .env.example
git commit -m "docs: add Telegram bot configuration to .env.example"
Step 1: 配置环境变量
export TELEGRAM_BOT_TOKEN=你的bot_token
export TELEGRAM_ADMIN_CHAT_ID=你的chat_id
Step 2: 启动 NOFX
cd /Users/yida/gopro/open-nofx && ./nofx
Expected 日志:
✅ Configuration loaded
🤖 Telegram bot started: @your_bot_name
✅ System started successfully
Step 3: 测试对话流程
在 Telegram 发送:
/start → 收到欢迎消息查看当前持仓 → 返回持仓信息或「无持仓」帮我创建一个 BTC 策略,RSI+MACD,止损 8% → Bot 追问策略名叫"主力BTC" → 策略创建成功Step 4: 验证访问控制
用其他账号发送消息 → 收到「⛔ 未授权访问」
service/nofx.go 是唯一接触 store/manager 的文件,handler 不能绕过它mcp.New()store / manager 接口与本文示例存在偏差,实现时必须以源码为准go build ./telegram/... 成功go build ./... 成功/start 后会话状态确实被清空,且重置语义与代码一致