mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
3ca95b294d
* feat: integrate NOFXi agent into dev * Enhance NOFXi agent workflow and diagnostics
932 lines
35 KiB
Go
932 lines
35 KiB
Go
package agent
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"regexp"
|
||
"sort"
|
||
"strings"
|
||
|
||
"nofx/store"
|
||
)
|
||
|
||
var urlPattern = regexp.MustCompile(`https://[^\s"'<>]+`)
|
||
|
||
func detectTraderManagementIntent(text string) bool {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if lower == "" {
|
||
return false
|
||
}
|
||
return containsAny(lower, []string{"交易员", "trader", "agent"}) &&
|
||
containsAny(lower, []string{"修改", "编辑", "更新", "改", "改一下", "删除", "删了", "启动", "停止", "查看", "查询", "列出", "rename", "update", "delete", "start", "stop", "list", "show"})
|
||
}
|
||
|
||
func detectExchangeManagementIntent(text string) bool {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if lower == "" {
|
||
return false
|
||
}
|
||
return containsAny(lower, []string{"交易所", "exchange", "okx", "binance", "bybit", "gate", "kucoin", "hyperliquid"}) &&
|
||
containsAny(lower, []string{"创建", "新建", "修改", "编辑", "更新", "改", "改一下", "删除", "删了", "查询", "查看", "列出", "启用", "禁用", "改名", "rename", "create", "update", "delete", "list", "show", "enable", "disable"})
|
||
}
|
||
|
||
func detectModelManagementIntent(text string) bool {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if lower == "" {
|
||
return false
|
||
}
|
||
return containsAny(lower, []string{"模型", "model", "provider", "deepseek", "openai", "claude", "gemini", "qwen", "kimi", "grok", "minimax"}) &&
|
||
containsAny(lower, []string{"创建", "新建", "修改", "编辑", "更新", "改", "改一下", "删除", "删了", "查询", "查看", "列出", "启用", "禁用", "改名", "rename", "create", "update", "delete", "list", "show", "enable", "disable"})
|
||
}
|
||
|
||
func detectStrategyManagementIntent(text string) bool {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if lower == "" {
|
||
return false
|
||
}
|
||
if wantsDefaultStrategyConfig(text) {
|
||
return true
|
||
}
|
||
return containsAny(lower, []string{"策略", "strategy"}) &&
|
||
containsAny(lower, []string{"创建", "新建", "修改", "编辑", "更新", "改", "改一下", "改成", "改为", "删除", "删了", "查询", "查看", "列出", "激活", "复制", "参数", "配置", "详情", "详细", "prompt", "提示词", "什么样", "怎么样", "create", "update", "delete", "list", "show", "activate", "duplicate", "detail", "details", "config", "configuration", "parameter", "prompt", "what kind"})
|
||
}
|
||
|
||
func detectTraderDiagnosisSkill(text string) bool {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
return containsAny(lower, []string{"交易员", "trader"}) &&
|
||
containsAny(lower, []string{"启动失败", "不交易", "没开仓", "无法启动", "异常", "失败", "diagnose", "error", "not trading"})
|
||
}
|
||
|
||
func detectStrategyDiagnosisSkill(text string) bool {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
return containsAny(lower, []string{"策略", "strategy", "prompt"}) &&
|
||
containsAny(lower, []string{"不生效", "没生效", "异常", "失败", "不一致", "失效", "diagnose", "error"})
|
||
}
|
||
|
||
func detectManagementAction(text string, domain string) string {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if lower == "" {
|
||
return ""
|
||
}
|
||
hasUpdateVerb := containsAny(lower, []string{"修改", "编辑", "更新", "改", "rename", "update", "切换", "换成", "换到"})
|
||
switch {
|
||
case containsAny(lower, []string{"删除", "删掉", "删了", "remove", "delete"}):
|
||
return "delete"
|
||
case containsAny(lower, []string{"启动", "开始", "run", "start"}) && domain == "trader":
|
||
return "start"
|
||
case containsAny(lower, []string{"停止", "停掉", "stop", "pause"}) && domain == "trader":
|
||
return "stop"
|
||
case containsAny(lower, []string{"激活", "activate"}) && domain == "strategy":
|
||
return "activate"
|
||
case containsAny(lower, []string{"复制", "duplicate"}) && domain == "strategy":
|
||
return "duplicate"
|
||
case containsAny(lower, []string{"改名", "重命名", "rename"}):
|
||
return "update_name"
|
||
case domain == "trader" && containsAny(lower, []string{"换模型", "换交易所", "换策略", "绑定", "切换模型", "切换交易所", "切换策略"}):
|
||
return "update_bindings"
|
||
case (domain == "exchange" || domain == "model") && containsAny(lower, []string{"启用", "禁用", "enable", "disable"}):
|
||
return "update_status"
|
||
case domain == "model" && hasUpdateVerb && containsAny(lower, []string{"url", "endpoint", "地址", "接口"}):
|
||
return "update_endpoint"
|
||
case domain == "strategy" && hasUpdateVerb && containsAny(lower, []string{"prompt", "提示词"}):
|
||
return "update_prompt"
|
||
case domain == "strategy" && hasUpdateVerb && containsAny(lower, []string{
|
||
"参数", "配置", "config", "configuration", "parameter",
|
||
"最大持仓", "最小置信度", "最低置信度", "主周期", "多周期", "时间框架",
|
||
"btc/eth杠杆", "btc eth杠杆", "山寨币杠杆",
|
||
"核心指标", "ema", "macd", "rsi", "atr", "boll", "bollinger", "布林",
|
||
}):
|
||
return "update_config"
|
||
case containsAny(lower, []string{"修改", "编辑", "更新", "改", "rename", "update"}):
|
||
return "update"
|
||
case domain == "trader" && containsAny(lower, []string{"运行中的", "在跑", "running"}):
|
||
return "query_running"
|
||
case !containsAny(lower, []string{"创建", "新建", "create", "new"}) &&
|
||
containsAny(lower, []string{"详情", "详细", "prompt", "提示词", "什么样", "怎么样", "detail", "details", "what kind"}):
|
||
return "query_detail"
|
||
case containsAny(lower, []string{"查询", "查看", "列出", "list", "show", "有哪些"}):
|
||
return "query_list"
|
||
case containsAny(lower, []string{"创建", "新建", "加一个", "create", "new"}):
|
||
return "create"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func exchangeTypeFromText(text string) string {
|
||
lower := strings.ToLower(text)
|
||
candidates := []string{"binance", "okx", "bybit", "gate", "kucoin", "hyperliquid", "aster", "lighter"}
|
||
for _, candidate := range candidates {
|
||
if strings.Contains(lower, candidate) {
|
||
return candidate
|
||
}
|
||
}
|
||
switch {
|
||
case strings.Contains(text, "币安"):
|
||
return "binance"
|
||
case strings.Contains(text, "欧易"):
|
||
return "okx"
|
||
case strings.Contains(text, "库币"):
|
||
return "kucoin"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func providerFromText(text string) string {
|
||
lower := strings.ToLower(text)
|
||
candidates := []string{"openai", "deepseek", "claude", "gemini", "qwen", "kimi", "grok", "minimax"}
|
||
for _, candidate := range candidates {
|
||
if strings.Contains(lower, candidate) {
|
||
return candidate
|
||
}
|
||
}
|
||
if strings.Contains(text, "通义") {
|
||
return "qwen"
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func extractURL(text string) string {
|
||
return strings.TrimSpace(urlPattern.FindString(text))
|
||
}
|
||
|
||
func extractPostKeywordName(text string, keywords []string) string {
|
||
trimmed := strings.TrimSpace(text)
|
||
for _, keyword := range keywords {
|
||
if idx := strings.Index(trimmed, keyword); idx >= 0 {
|
||
name := strings.TrimSpace(trimmed[idx+len(keyword):])
|
||
name = strings.Trim(name, "“”\"':: ")
|
||
if name != "" && len([]rune(name)) <= 50 {
|
||
return name
|
||
}
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func setField(session *skillSession, key, value string) {
|
||
ensureSkillFields(session)
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return
|
||
}
|
||
session.Fields[key] = value
|
||
}
|
||
|
||
func fieldValue(session skillSession, key string) string {
|
||
if session.Fields == nil {
|
||
return ""
|
||
}
|
||
return strings.TrimSpace(session.Fields[key])
|
||
}
|
||
|
||
func textMeansAllTargets(text string) bool {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if lower == "" {
|
||
return false
|
||
}
|
||
return containsAny(lower, []string{
|
||
"全部", "所有", "全都", "全部策略", "所有策略",
|
||
"all", "all strategies", "every strategy",
|
||
})
|
||
}
|
||
|
||
func supportsBulkTargetSelection(skillName, action string) bool {
|
||
return skillName == "strategy_management" && action == "delete"
|
||
}
|
||
|
||
func resolveTargetFromText(text string, options []traderSkillOption, existing *EntityReference) *EntityReference {
|
||
if existing != nil && (existing.ID != "" || existing.Name != "") {
|
||
return existing
|
||
}
|
||
if match := pickMentionedOption(text, options); match != nil {
|
||
return &EntityReference{ID: match.ID, Name: match.Name}
|
||
}
|
||
if choice := choosePreferredOption(options); choice != nil {
|
||
return &EntityReference{ID: choice.ID, Name: choice.Name}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (a *Agent) handleTraderManagementSkill(storeUserID string, userID int64, lang, text string, session skillSession) (string, bool) {
|
||
action := detectManagementAction(text, "trader")
|
||
if session.Name == "trader_management" && session.Action != "" {
|
||
action = session.Action
|
||
}
|
||
if action == "" || action == "create" {
|
||
return "", false
|
||
}
|
||
if action == "query_running" {
|
||
answer := formatReadFastPathResponse(lang, "list_traders", a.toolListTraders(storeUserID))
|
||
return applyTraderQueryFilter(lang, answer, a.toolListTraders(storeUserID), "running_only"), true
|
||
}
|
||
if action == "query_detail" {
|
||
options := a.loadTraderOptions(storeUserID)
|
||
target := resolveTargetFromText(text, options, session.TargetRef)
|
||
if detail, ok := a.describeTrader(storeUserID, lang, target); ok {
|
||
return detail, true
|
||
}
|
||
return formatReadFastPathResponse(lang, "list_traders", a.toolListTraders(storeUserID)), true
|
||
}
|
||
return a.handleSimpleEntitySkill(storeUserID, userID, lang, text, session, "trader_management", action, a.loadTraderOptions(storeUserID))
|
||
}
|
||
|
||
func (a *Agent) handleExchangeManagementSkill(storeUserID string, userID int64, lang, text string, session skillSession) (string, bool) {
|
||
action := detectManagementAction(text, "exchange")
|
||
if session.Name == "exchange_management" && session.Action != "" {
|
||
action = session.Action
|
||
}
|
||
if action == "" {
|
||
return "", false
|
||
}
|
||
options := a.loadExchangeOptions(storeUserID)
|
||
switch action {
|
||
case "query_list":
|
||
return formatReadFastPathResponse(lang, "get_exchange_configs", a.toolGetExchangeConfigs(storeUserID)), true
|
||
case "query_detail":
|
||
target := resolveTargetFromText(text, options, session.TargetRef)
|
||
if detail, ok := a.describeExchange(storeUserID, lang, target); ok {
|
||
return detail, true
|
||
}
|
||
return formatReadFastPathResponse(lang, "get_exchange_configs", a.toolGetExchangeConfigs(storeUserID)), true
|
||
case "create":
|
||
return a.handleExchangeCreateSkill(storeUserID, userID, lang, text, session), true
|
||
default:
|
||
return a.handleSimpleEntitySkill(storeUserID, userID, lang, text, session, "exchange_management", action, options)
|
||
}
|
||
}
|
||
|
||
func (a *Agent) handleModelManagementSkill(storeUserID string, userID int64, lang, text string, session skillSession) (string, bool) {
|
||
action := detectManagementAction(text, "model")
|
||
if session.Name == "model_management" && session.Action != "" {
|
||
action = session.Action
|
||
}
|
||
if action == "" {
|
||
return "", false
|
||
}
|
||
options := a.loadEnabledModelOptions(storeUserID)
|
||
switch action {
|
||
case "query_list":
|
||
return formatReadFastPathResponse(lang, "get_model_configs", a.toolGetModelConfigs(storeUserID)), true
|
||
case "query_detail":
|
||
target := resolveTargetFromText(text, options, session.TargetRef)
|
||
if detail, ok := a.describeModel(storeUserID, lang, target); ok {
|
||
return detail, true
|
||
}
|
||
return formatReadFastPathResponse(lang, "get_model_configs", a.toolGetModelConfigs(storeUserID)), true
|
||
case "create":
|
||
return a.handleModelCreateSkill(storeUserID, userID, lang, text, session), true
|
||
default:
|
||
return a.handleSimpleEntitySkill(storeUserID, userID, lang, text, session, "model_management", action, options)
|
||
}
|
||
}
|
||
|
||
func (a *Agent) handleStrategyManagementSkill(storeUserID string, userID int64, lang, text string, session skillSession) (string, bool) {
|
||
action := detectManagementAction(text, "strategy")
|
||
if session.Name == "strategy_management" && session.Action != "" {
|
||
action = session.Action
|
||
}
|
||
if action == "" && wantsStrategyDetails(text) {
|
||
action = "query_detail"
|
||
}
|
||
if action == "" {
|
||
return "", false
|
||
}
|
||
options := a.loadStrategyOptions(storeUserID)
|
||
switch action {
|
||
case "query_detail":
|
||
if wantsDefaultStrategyConfig(text) {
|
||
return a.describeDefaultStrategyConfig(lang), true
|
||
}
|
||
target := resolveTargetFromText(text, options, session.TargetRef)
|
||
if detail, ok := a.describeStrategy(storeUserID, lang, target); ok {
|
||
return detail, true
|
||
}
|
||
return formatReadFastPathResponse(lang, "get_strategies", a.toolGetStrategies(storeUserID)), true
|
||
case "query_list":
|
||
return formatReadFastPathResponse(lang, "get_strategies", a.toolGetStrategies(storeUserID)), true
|
||
case "create":
|
||
return a.handleStrategyCreateSkill(storeUserID, userID, lang, text, session), true
|
||
default:
|
||
return a.handleSimpleEntitySkill(storeUserID, userID, lang, text, session, "strategy_management", action, options)
|
||
}
|
||
}
|
||
|
||
func wantsStrategyDetails(text string) bool {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if lower == "" {
|
||
return false
|
||
}
|
||
return containsAny(lower, []string{
|
||
"什么样", "怎么样", "详情", "详细", "参数", "配置", "prompt", "提示词",
|
||
"what kind", "details", "detail", "config", "configuration", "parameter", "prompt",
|
||
})
|
||
}
|
||
|
||
func wantsDefaultStrategyConfig(text string) bool {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if lower == "" {
|
||
return false
|
||
}
|
||
return containsAny(lower, []string{
|
||
"默认配置", "默认策略", "默认模板", "模板配置",
|
||
"default config", "default strategy", "default template",
|
||
})
|
||
}
|
||
|
||
func (a *Agent) describeStrategy(storeUserID, lang string, target *EntityReference) (string, bool) {
|
||
if a.store == nil {
|
||
return "", false
|
||
}
|
||
|
||
var strategy *store.Strategy
|
||
var err error
|
||
if target != nil && strings.TrimSpace(target.ID) != "" {
|
||
strategy, err = a.store.Strategy().Get(storeUserID, strings.TrimSpace(target.ID))
|
||
} else if target != nil && strings.TrimSpace(target.Name) != "" {
|
||
strategies, listErr := a.store.Strategy().List(storeUserID)
|
||
if listErr != nil {
|
||
return "", false
|
||
}
|
||
for _, item := range strategies {
|
||
if item != nil && strings.EqualFold(strings.TrimSpace(item.Name), strings.TrimSpace(target.Name)) {
|
||
strategy = item
|
||
break
|
||
}
|
||
}
|
||
} else {
|
||
strategies, listErr := a.store.Strategy().List(storeUserID)
|
||
if listErr != nil || len(strategies) != 1 {
|
||
return "", false
|
||
}
|
||
strategy = strategies[0]
|
||
}
|
||
if err != nil || strategy == nil {
|
||
return "", false
|
||
}
|
||
|
||
var cfg store.StrategyConfig
|
||
if strings.TrimSpace(strategy.Config) != "" {
|
||
_ = json.Unmarshal([]byte(strategy.Config), &cfg)
|
||
}
|
||
|
||
return formatStrategyDetailResponse(lang, strategy, cfg), true
|
||
}
|
||
|
||
func formatStrategyDetailResponse(lang string, strategy *store.Strategy, cfg store.StrategyConfig) string {
|
||
name := strings.TrimSpace(strategy.Name)
|
||
if name == "" {
|
||
name = strings.TrimSpace(strategy.ID)
|
||
}
|
||
|
||
sourceBits := make([]string, 0, 4)
|
||
if strings.TrimSpace(cfg.CoinSource.SourceType) != "" {
|
||
sourceBits = append(sourceBits, cfg.CoinSource.SourceType)
|
||
}
|
||
if cfg.CoinSource.UseAI500 {
|
||
sourceBits = append(sourceBits, fmt.Sprintf("AI500=%d", cfg.CoinSource.AI500Limit))
|
||
}
|
||
if cfg.CoinSource.UseOITop {
|
||
sourceBits = append(sourceBits, fmt.Sprintf("OITop=%d", cfg.CoinSource.OITopLimit))
|
||
}
|
||
if cfg.CoinSource.UseOILow {
|
||
sourceBits = append(sourceBits, fmt.Sprintf("OILow=%d", cfg.CoinSource.OILowLimit))
|
||
}
|
||
if len(cfg.CoinSource.StaticCoins) > 0 {
|
||
sourceBits = append(sourceBits, "static="+strings.Join(cfg.CoinSource.StaticCoins, ","))
|
||
}
|
||
|
||
timeframes := append([]string(nil), cfg.Indicators.Klines.SelectedTimeframes...)
|
||
if len(timeframes) == 0 {
|
||
timeframes = cleanStringList([]string{cfg.Indicators.Klines.PrimaryTimeframe, cfg.Indicators.Klines.LongerTimeframe})
|
||
}
|
||
|
||
indicatorBits := make([]string, 0, 8)
|
||
if cfg.Indicators.EnableRawKlines {
|
||
indicatorBits = append(indicatorBits, "raw_klines")
|
||
}
|
||
if cfg.Indicators.EnableVolume {
|
||
indicatorBits = append(indicatorBits, "volume")
|
||
}
|
||
if cfg.Indicators.EnableOI {
|
||
indicatorBits = append(indicatorBits, "oi")
|
||
}
|
||
if cfg.Indicators.EnableFundingRate {
|
||
indicatorBits = append(indicatorBits, "funding_rate")
|
||
}
|
||
if cfg.Indicators.EnableEMA {
|
||
indicatorBits = append(indicatorBits, "ema")
|
||
}
|
||
if cfg.Indicators.EnableMACD {
|
||
indicatorBits = append(indicatorBits, "macd")
|
||
}
|
||
if cfg.Indicators.EnableRSI {
|
||
indicatorBits = append(indicatorBits, "rsi")
|
||
}
|
||
if cfg.Indicators.EnableATR {
|
||
indicatorBits = append(indicatorBits, "atr")
|
||
}
|
||
if cfg.Indicators.EnableBOLL {
|
||
indicatorBits = append(indicatorBits, "boll")
|
||
}
|
||
sort.Strings(indicatorBits)
|
||
|
||
promptBits := make([]string, 0, 5)
|
||
if strings.TrimSpace(cfg.PromptSections.RoleDefinition) != "" {
|
||
promptBits = append(promptBits, "role_definition")
|
||
}
|
||
if strings.TrimSpace(cfg.PromptSections.TradingFrequency) != "" {
|
||
promptBits = append(promptBits, "trading_frequency")
|
||
}
|
||
if strings.TrimSpace(cfg.PromptSections.EntryStandards) != "" {
|
||
promptBits = append(promptBits, "entry_standards")
|
||
}
|
||
if strings.TrimSpace(cfg.PromptSections.DecisionProcess) != "" {
|
||
promptBits = append(promptBits, "decision_process")
|
||
}
|
||
|
||
customPrompt := strings.TrimSpace(cfg.CustomPrompt)
|
||
customPromptPreview := customPrompt
|
||
if len([]rune(customPromptPreview)) > 120 {
|
||
runes := []rune(customPromptPreview)
|
||
customPromptPreview = string(runes[:120]) + "..."
|
||
}
|
||
|
||
if lang == "zh" {
|
||
lines := []string{
|
||
fmt.Sprintf("策略“%s”概览:", name),
|
||
fmt.Sprintf("- 类型:%s", defaultIfEmpty(strings.TrimSpace(cfg.StrategyType), "ai_trading")),
|
||
fmt.Sprintf("- 语言:%s", defaultIfEmpty(strings.TrimSpace(cfg.Language), "zh")),
|
||
}
|
||
if strings.TrimSpace(strategy.Description) != "" {
|
||
lines = append(lines, fmt.Sprintf("- 描述:%s", strings.TrimSpace(strategy.Description)))
|
||
}
|
||
if len(sourceBits) > 0 {
|
||
lines = append(lines, "- 标的来源:"+strings.Join(sourceBits, " | "))
|
||
}
|
||
if len(timeframes) > 0 {
|
||
lines = append(lines, "- K线周期:"+strings.Join(timeframes, " / "))
|
||
}
|
||
lines = append(lines, fmt.Sprintf("- 仓位风险:最多持仓 %d,BTC/ETH 最大杠杆 %d,山寨最大杠杆 %d,最低置信度 %d",
|
||
cfg.RiskControl.MaxPositions, cfg.RiskControl.BTCETHMaxLeverage, cfg.RiskControl.AltcoinMaxLeverage, cfg.RiskControl.MinConfidence))
|
||
if len(indicatorBits) > 0 {
|
||
lines = append(lines, "- 已启用指标:"+strings.Join(indicatorBits, "、"))
|
||
}
|
||
if len(promptBits) > 0 {
|
||
lines = append(lines, "- Prompt 模块:"+strings.Join(promptBits, "、"))
|
||
}
|
||
if customPromptPreview != "" {
|
||
lines = append(lines, "- 自定义 Prompt:"+customPromptPreview)
|
||
} else {
|
||
lines = append(lines, "- 自定义 Prompt:当前为空,主要使用策略模板内置 prompt sections。")
|
||
}
|
||
lines = append(lines, "- 如果你要,我还可以继续展开这条策略的完整参数 JSON,或者逐段解释它的 prompt。")
|
||
return strings.Join(lines, "\n")
|
||
}
|
||
|
||
lines := []string{
|
||
fmt.Sprintf("Strategy %q overview:", name),
|
||
fmt.Sprintf("- Type: %s", defaultIfEmpty(strings.TrimSpace(cfg.StrategyType), "ai_trading")),
|
||
fmt.Sprintf("- Language: %s", defaultIfEmpty(strings.TrimSpace(cfg.Language), "en")),
|
||
}
|
||
if strings.TrimSpace(strategy.Description) != "" {
|
||
lines = append(lines, fmt.Sprintf("- Description: %s", strings.TrimSpace(strategy.Description)))
|
||
}
|
||
if len(sourceBits) > 0 {
|
||
lines = append(lines, "- Coin source: "+strings.Join(sourceBits, " | "))
|
||
}
|
||
if len(timeframes) > 0 {
|
||
lines = append(lines, "- Timeframes: "+strings.Join(timeframes, " / "))
|
||
}
|
||
lines = append(lines, fmt.Sprintf("- Risk: max positions %d, BTC/ETH max leverage %d, alt max leverage %d, min confidence %d",
|
||
cfg.RiskControl.MaxPositions, cfg.RiskControl.BTCETHMaxLeverage, cfg.RiskControl.AltcoinMaxLeverage, cfg.RiskControl.MinConfidence))
|
||
if len(indicatorBits) > 0 {
|
||
lines = append(lines, "- Enabled indicators: "+strings.Join(indicatorBits, ", "))
|
||
}
|
||
if len(promptBits) > 0 {
|
||
lines = append(lines, "- Prompt modules: "+strings.Join(promptBits, ", "))
|
||
}
|
||
if customPromptPreview != "" {
|
||
lines = append(lines, "- Custom prompt: "+customPromptPreview)
|
||
} else {
|
||
lines = append(lines, "- Custom prompt: empty right now; it mainly uses the built-in prompt sections from the strategy template.")
|
||
}
|
||
lines = append(lines, "- I can also expand the full strategy config JSON or walk through the prompt section by section.")
|
||
return strings.Join(lines, "\n")
|
||
}
|
||
|
||
func (a *Agent) describeDefaultStrategyConfig(lang string) string {
|
||
if lang != "zh" {
|
||
lang = "en"
|
||
}
|
||
cfg := store.GetDefaultStrategyConfig(lang)
|
||
name := "Default Strategy Template"
|
||
description := "System default strategy configuration template"
|
||
if lang == "zh" {
|
||
name = "默认策略模板"
|
||
description = "系统默认策略配置模板"
|
||
}
|
||
return formatStrategyDetailResponse(lang, &store.Strategy{
|
||
ID: "default_strategy_template",
|
||
Name: name,
|
||
Description: description,
|
||
}, cfg)
|
||
}
|
||
|
||
func (a *Agent) describeTrader(storeUserID, lang string, target *EntityReference) (string, bool) {
|
||
raw := a.toolListTraders(storeUserID)
|
||
var payload struct {
|
||
Traders []safeTraderToolConfig `json:"traders"`
|
||
}
|
||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||
return "", false
|
||
}
|
||
trader := findTraderByReference(payload.Traders, target)
|
||
if trader == nil {
|
||
if len(payload.Traders) != 1 {
|
||
return "", false
|
||
}
|
||
trader = &payload.Traders[0]
|
||
}
|
||
if lang == "zh" {
|
||
status := "未运行"
|
||
if trader.IsRunning {
|
||
status = "运行中"
|
||
}
|
||
return fmt.Sprintf("交易员“%s”详情:\n- 状态:%s\n- 模型:%s\n- 交易所:%s\n- 策略:%s\n- 扫描间隔:%d 分钟\n- 初始余额:%.2f",
|
||
trader.Name, status, trader.AIModelID, trader.ExchangeID, defaultIfEmpty(trader.StrategyID, "未绑定"), trader.ScanIntervalMinutes, trader.InitialBalance), true
|
||
}
|
||
status := "stopped"
|
||
if trader.IsRunning {
|
||
status = "running"
|
||
}
|
||
return fmt.Sprintf("Trader %q details:\n- Status: %s\n- Model: %s\n- Exchange: %s\n- Strategy: %s\n- Scan interval: %d minutes\n- Initial balance: %.2f",
|
||
trader.Name, status, trader.AIModelID, trader.ExchangeID, defaultIfEmpty(trader.StrategyID, "none"), trader.ScanIntervalMinutes, trader.InitialBalance), true
|
||
}
|
||
|
||
func (a *Agent) describeExchange(storeUserID, lang string, target *EntityReference) (string, bool) {
|
||
raw := a.toolGetExchangeConfigs(storeUserID)
|
||
var payload struct {
|
||
ExchangeConfigs []safeExchangeToolConfig `json:"exchange_configs"`
|
||
}
|
||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||
return "", false
|
||
}
|
||
exchange := findExchangeByReference(payload.ExchangeConfigs, target)
|
||
if exchange == nil {
|
||
if len(payload.ExchangeConfigs) != 1 {
|
||
return "", false
|
||
}
|
||
exchange = &payload.ExchangeConfigs[0]
|
||
}
|
||
if lang == "zh" {
|
||
return fmt.Sprintf("交易所配置“%s”详情:\n- 交易所:%s\n- 已启用:%t\n- API Key:%t\n- Secret:%t\n- Passphrase:%t\n- Testnet:%t",
|
||
defaultIfEmpty(exchange.AccountName, exchange.ID), exchange.ExchangeType, exchange.Enabled, exchange.HasAPIKey, exchange.HasSecretKey, exchange.HasPassphrase, exchange.Testnet), true
|
||
}
|
||
return fmt.Sprintf("Exchange config %q details:\n- Exchange: %s\n- Enabled: %t\n- API key present: %t\n- Secret present: %t\n- Passphrase present: %t\n- Testnet: %t",
|
||
defaultIfEmpty(exchange.AccountName, exchange.ID), exchange.ExchangeType, exchange.Enabled, exchange.HasAPIKey, exchange.HasSecretKey, exchange.HasPassphrase, exchange.Testnet), true
|
||
}
|
||
|
||
func (a *Agent) describeModel(storeUserID, lang string, target *EntityReference) (string, bool) {
|
||
raw := a.toolGetModelConfigs(storeUserID)
|
||
var payload struct {
|
||
ModelConfigs []safeModelToolConfig `json:"model_configs"`
|
||
}
|
||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||
return "", false
|
||
}
|
||
model := findModelByReference(payload.ModelConfigs, target)
|
||
if model == nil {
|
||
if len(payload.ModelConfigs) != 1 {
|
||
return "", false
|
||
}
|
||
model = &payload.ModelConfigs[0]
|
||
}
|
||
if lang == "zh" {
|
||
return fmt.Sprintf("模型配置“%s”详情:\n- Provider:%s\n- 已启用:%t\n- API Key:%t\n- URL:%s\n- Model Name:%s",
|
||
defaultIfEmpty(model.Name, model.ID), model.Provider, model.Enabled, model.HasAPIKey, defaultIfEmpty(model.CustomAPIURL, "未设置"), defaultIfEmpty(model.CustomModelName, "未设置")), true
|
||
}
|
||
return fmt.Sprintf("Model config %q details:\n- Provider: %s\n- Enabled: %t\n- API key present: %t\n- URL: %s\n- Model name: %s",
|
||
defaultIfEmpty(model.Name, model.ID), model.Provider, model.Enabled, model.HasAPIKey, defaultIfEmpty(model.CustomAPIURL, "not set"), defaultIfEmpty(model.CustomModelName, "not set")), true
|
||
}
|
||
|
||
func findTraderByReference(items []safeTraderToolConfig, target *EntityReference) *safeTraderToolConfig {
|
||
if target == nil {
|
||
return nil
|
||
}
|
||
for i := range items {
|
||
if strings.TrimSpace(target.ID) != "" && items[i].ID == strings.TrimSpace(target.ID) {
|
||
return &items[i]
|
||
}
|
||
if strings.TrimSpace(target.Name) != "" && strings.EqualFold(strings.TrimSpace(items[i].Name), strings.TrimSpace(target.Name)) {
|
||
return &items[i]
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func findExchangeByReference(items []safeExchangeToolConfig, target *EntityReference) *safeExchangeToolConfig {
|
||
if target == nil {
|
||
return nil
|
||
}
|
||
for i := range items {
|
||
name := defaultIfEmpty(items[i].AccountName, items[i].Name)
|
||
if strings.TrimSpace(target.ID) != "" && items[i].ID == strings.TrimSpace(target.ID) {
|
||
return &items[i]
|
||
}
|
||
if strings.TrimSpace(target.Name) != "" && strings.EqualFold(strings.TrimSpace(name), strings.TrimSpace(target.Name)) {
|
||
return &items[i]
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func findModelByReference(items []safeModelToolConfig, target *EntityReference) *safeModelToolConfig {
|
||
if target == nil {
|
||
return nil
|
||
}
|
||
for i := range items {
|
||
if strings.TrimSpace(target.ID) != "" && items[i].ID == strings.TrimSpace(target.ID) {
|
||
return &items[i]
|
||
}
|
||
if strings.TrimSpace(target.Name) != "" && strings.EqualFold(strings.TrimSpace(items[i].Name), strings.TrimSpace(target.Name)) {
|
||
return &items[i]
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (a *Agent) loadTraderOptions(storeUserID string) []traderSkillOption {
|
||
if a.store == nil {
|
||
return nil
|
||
}
|
||
traders, err := a.store.Trader().List(storeUserID)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
out := make([]traderSkillOption, 0, len(traders))
|
||
for _, trader := range traders {
|
||
out = append(out, traderSkillOption{ID: trader.ID, Name: trader.Name, Enabled: trader.IsRunning})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func (a *Agent) handleExchangeCreateSkill(storeUserID string, userID int64, lang, text string, session skillSession) string {
|
||
if session.Name == "" {
|
||
session = skillSession{Name: "exchange_management", Action: "create", Phase: "collecting"}
|
||
}
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "resolve_exchange_type")
|
||
}
|
||
if isCancelSkillReply(text) {
|
||
a.clearSkillSession(userID)
|
||
if lang == "zh" {
|
||
return "已取消当前创建交易所配置流程。"
|
||
}
|
||
return "Cancelled the current exchange creation flow."
|
||
}
|
||
if v := exchangeTypeFromText(text); fieldValue(session, "exchange_type") == "" && v != "" {
|
||
setField(&session, "exchange_type", v)
|
||
}
|
||
if v := extractTraderName(text); fieldValue(session, "account_name") == "" && v != "" {
|
||
setField(&session, "account_name", v)
|
||
}
|
||
exType := fieldValue(session, "exchange_type")
|
||
if actionRequiresSlot("exchange_management", "create", "exchange_type") && exType == "" {
|
||
setSkillDAGStep(&session, "resolve_exchange_type")
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "要创建交易所配置,我还需要:" + slotDisplayName("exchange_type", lang) + "。例如:OKX、Binance、Bybit。"
|
||
}
|
||
return "To create an exchange config, tell me which exchange to use, for example OKX, Binance, or Bybit."
|
||
}
|
||
accountName := fieldValue(session, "account_name")
|
||
if accountName == "" {
|
||
accountName = "Default"
|
||
}
|
||
setSkillDAGStep(&session, "execute_create")
|
||
args := map[string]any{
|
||
"action": "create",
|
||
"exchange_type": exType,
|
||
"account_name": accountName,
|
||
}
|
||
raw, _ := json.Marshal(args)
|
||
resp := a.toolManageExchangeConfig(storeUserID, string(raw))
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "创建交易所配置失败:" + errMsg
|
||
}
|
||
return "Failed to create exchange config: " + errMsg
|
||
}
|
||
a.clearSkillSession(userID)
|
||
if lang == "zh" {
|
||
return fmt.Sprintf("已创建交易所配置:%s(%s)。如需继续补 API Key、Secret 或 Passphrase,可以直接继续说。", accountName, exType)
|
||
}
|
||
return fmt.Sprintf("Created exchange config %s (%s). You can continue by adding API key, secret, or passphrase.", accountName, exType)
|
||
}
|
||
|
||
func (a *Agent) handleModelCreateSkill(storeUserID string, userID int64, lang, text string, session skillSession) string {
|
||
if session.Name == "" {
|
||
session = skillSession{Name: "model_management", Action: "create", Phase: "collecting"}
|
||
}
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "resolve_provider")
|
||
}
|
||
if isCancelSkillReply(text) {
|
||
a.clearSkillSession(userID)
|
||
if lang == "zh" {
|
||
return "已取消当前创建模型配置流程。"
|
||
}
|
||
return "Cancelled the current model creation flow."
|
||
}
|
||
if v := providerFromText(text); fieldValue(session, "provider") == "" && v != "" {
|
||
setField(&session, "provider", v)
|
||
}
|
||
if v := extractTraderName(text); fieldValue(session, "name") == "" && v != "" {
|
||
setField(&session, "name", v)
|
||
}
|
||
if v := extractURL(text); fieldValue(session, "custom_api_url") == "" && v != "" {
|
||
setField(&session, "custom_api_url", v)
|
||
}
|
||
provider := fieldValue(session, "provider")
|
||
if actionRequiresSlot("model_management", "create", "provider") && provider == "" {
|
||
setSkillDAGStep(&session, "resolve_provider")
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "要创建模型配置,我还需要:" + slotDisplayName("provider", lang) + ",例如:OpenAI、DeepSeek、Claude、Gemini。"
|
||
}
|
||
return "To create a model config, I need the provider first, for example OpenAI, DeepSeek, Claude, or Gemini."
|
||
}
|
||
setSkillDAGStep(&session, "execute_create")
|
||
args := map[string]any{
|
||
"action": "create",
|
||
"provider": provider,
|
||
"name": defaultIfEmpty(fieldValue(session, "name"), provider),
|
||
"custom_api_url": fieldValue(session, "custom_api_url"),
|
||
"custom_model_name": fieldValue(session, "custom_model_name"),
|
||
}
|
||
raw, _ := json.Marshal(args)
|
||
resp := a.toolManageModelConfig(storeUserID, string(raw))
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "创建模型配置失败:" + errMsg
|
||
}
|
||
return "Failed to create model config: " + errMsg
|
||
}
|
||
a.clearSkillSession(userID)
|
||
if lang == "zh" {
|
||
return fmt.Sprintf("已创建模型配置:%s。你后续还可以继续补 API Key、URL 或模型名。", provider)
|
||
}
|
||
return fmt.Sprintf("Created model config for %s. You can continue by adding API key, URL, or model name.", provider)
|
||
}
|
||
|
||
func (a *Agent) handleStrategyCreateSkill(storeUserID string, userID int64, lang, text string, session skillSession) string {
|
||
if session.Name == "" {
|
||
session = skillSession{Name: "strategy_management", Action: "create", Phase: "collecting"}
|
||
}
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "resolve_name")
|
||
}
|
||
if isCancelSkillReply(text) {
|
||
a.clearSkillSession(userID)
|
||
if lang == "zh" {
|
||
return "已取消当前创建策略流程。"
|
||
}
|
||
return "Cancelled the current strategy creation flow."
|
||
}
|
||
name := fieldValue(session, "name")
|
||
if name == "" {
|
||
name = extractTraderName(text)
|
||
if name == "" {
|
||
name = extractPostKeywordName(text, []string{"叫", "名为", "策略叫", "strategy called"})
|
||
}
|
||
if name != "" {
|
||
setField(&session, "name", name)
|
||
}
|
||
}
|
||
if actionRequiresSlot("strategy_management", "create", "name") && name == "" {
|
||
setSkillDAGStep(&session, "resolve_name")
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "要创建策略,我还需要:" + slotDisplayName("name", lang) + "。你可以直接说:创建一个叫“趋势策略A”的策略。"
|
||
}
|
||
return "To create a strategy, I need a strategy name. You can say: create a strategy called 'Trend A'."
|
||
}
|
||
setSkillDAGStep(&session, "execute_create")
|
||
args := map[string]any{"action": "create", "name": name, "lang": "zh"}
|
||
raw, _ := json.Marshal(args)
|
||
resp := a.toolManageStrategy(storeUserID, string(raw))
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "创建策略失败:" + errMsg
|
||
}
|
||
return "Failed to create strategy: " + errMsg
|
||
}
|
||
a.clearSkillSession(userID)
|
||
if lang == "zh" {
|
||
return fmt.Sprintf("已创建策略“%s”。默认配置已就绪,你后续可以继续让我帮你改细节。", name)
|
||
}
|
||
return fmt.Sprintf("Created strategy %q with the default configuration.", name)
|
||
}
|
||
|
||
func (a *Agent) handleSimpleEntitySkill(storeUserID string, userID int64, lang, text string, session skillSession, skillName, action string, options []traderSkillOption) (string, bool) {
|
||
if isCancelSkillReply(text) {
|
||
a.clearSkillSession(userID)
|
||
if lang == "zh" {
|
||
return "已取消当前流程。", true
|
||
}
|
||
return "Cancelled the current flow.", true
|
||
}
|
||
if session.Name == "" {
|
||
session = skillSession{Name: skillName, Action: action, Phase: "collecting"}
|
||
}
|
||
if session.Name != skillName || session.Action != action {
|
||
return "", false
|
||
}
|
||
|
||
if dag, ok := getSkillDAG(skillName, action); ok && len(dag.Steps) > 0 {
|
||
currentStep, _ := currentSkillDAGStep(session)
|
||
if currentStep.ID == "resolve_target" {
|
||
if supportsBulkTargetSelection(skillName, action) && textMeansAllTargets(text) {
|
||
setField(&session, "bulk_scope", "all")
|
||
advanceSkillDAGStep(&session, currentStep.ID)
|
||
} else {
|
||
session.TargetRef = resolveTargetFromText(text, options, session.TargetRef)
|
||
}
|
||
if session.TargetRef == nil {
|
||
if !(supportsBulkTargetSelection(skillName, action) && fieldValue(session, "bulk_scope") == "all") {
|
||
setSkillDAGStep(&session, "resolve_target")
|
||
a.saveSkillSession(userID, session)
|
||
label := "可选对象:"
|
||
if lang != "zh" {
|
||
label = "Available targets:"
|
||
}
|
||
optionList := formatOptionList(label, options)
|
||
if lang == "zh" {
|
||
reply := "当前这一步需要先确定目标对象。请告诉我你要操作哪一个。"
|
||
if optionList != "" {
|
||
reply += "\n" + optionList
|
||
}
|
||
return reply, true
|
||
}
|
||
reply := "This step needs a target object first. Tell me which one to operate on."
|
||
if optionList != "" {
|
||
reply += "\n" + optionList
|
||
}
|
||
return reply, true
|
||
}
|
||
}
|
||
if fieldValue(session, skillDAGStepField) == currentStep.ID {
|
||
advanceSkillDAGStep(&session, currentStep.ID)
|
||
}
|
||
}
|
||
} else {
|
||
if supportsBulkTargetSelection(skillName, action) && textMeansAllTargets(text) {
|
||
setField(&session, "bulk_scope", "all")
|
||
} else {
|
||
session.TargetRef = resolveTargetFromText(text, options, session.TargetRef)
|
||
}
|
||
if session.TargetRef == nil && fieldValue(session, "bulk_scope") != "all" && action != "query" && action != "query_list" && action != "query_detail" && action != "query_running" {
|
||
a.saveSkillSession(userID, session)
|
||
label := formatOptionList("可选对象:", options)
|
||
if lang == "zh" {
|
||
reply := "我还需要你明确要操作的是哪一个对象。"
|
||
if label != "" {
|
||
reply += "\n" + label
|
||
}
|
||
return reply, true
|
||
}
|
||
reply := "I still need you to specify which object to operate on."
|
||
if label != "" {
|
||
reply += "\n" + label
|
||
}
|
||
return reply, true
|
||
}
|
||
}
|
||
|
||
switch skillName {
|
||
case "trader_management":
|
||
return a.executeTraderManagementAction(storeUserID, userID, lang, text, session), true
|
||
case "exchange_management":
|
||
return a.executeExchangeManagementAction(storeUserID, userID, lang, text, session), true
|
||
case "model_management":
|
||
return a.executeModelManagementAction(storeUserID, userID, lang, text, session), true
|
||
case "strategy_management":
|
||
return a.executeStrategyManagementAction(storeUserID, userID, lang, text, session), true
|
||
default:
|
||
return "", false
|
||
}
|
||
}
|
||
|
||
func defaultIfEmpty(value, fallback string) string {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return strings.TrimSpace(fallback)
|
||
}
|
||
return value
|
||
}
|