Files
nofx/agent/skill_management_handlers.go
lky-spec 3ca95b294d feat: port NOFXi agent module onto latest dev base (#1485)
* feat: integrate NOFXi agent into dev

* Enhance NOFXi agent workflow and diagnostics
2026-04-21 23:47:55 +08:00

932 lines
35 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}