mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 16:56:56 +08:00
Enhance NOFXi agent workflow and diagnostics
This commit is contained in:
@@ -4,7 +4,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"nofx/store"
|
||||
)
|
||||
|
||||
var urlPattern = regexp.MustCompile(`https://[^\s"'<>]+`)
|
||||
@@ -15,7 +18,7 @@ func detectTraderManagementIntent(text string) bool {
|
||||
return false
|
||||
}
|
||||
return containsAny(lower, []string{"交易员", "trader", "agent"}) &&
|
||||
containsAny(lower, []string{"修改", "编辑", "更新", "删除", "启动", "停止", "查看", "查询", "列出", "rename", "update", "delete", "start", "stop", "list", "show"})
|
||||
containsAny(lower, []string{"修改", "编辑", "更新", "改", "改一下", "删除", "删了", "启动", "停止", "查看", "查询", "列出", "rename", "update", "delete", "start", "stop", "list", "show"})
|
||||
}
|
||||
|
||||
func detectExchangeManagementIntent(text string) bool {
|
||||
@@ -24,7 +27,7 @@ func detectExchangeManagementIntent(text string) bool {
|
||||
return false
|
||||
}
|
||||
return containsAny(lower, []string{"交易所", "exchange", "okx", "binance", "bybit", "gate", "kucoin", "hyperliquid"}) &&
|
||||
containsAny(lower, []string{"创建", "新建", "修改", "编辑", "更新", "删除", "查询", "查看", "列出", "create", "update", "delete", "list", "show"})
|
||||
containsAny(lower, []string{"创建", "新建", "修改", "编辑", "更新", "改", "改一下", "删除", "删了", "查询", "查看", "列出", "启用", "禁用", "改名", "rename", "create", "update", "delete", "list", "show", "enable", "disable"})
|
||||
}
|
||||
|
||||
func detectModelManagementIntent(text string) bool {
|
||||
@@ -33,7 +36,7 @@ func detectModelManagementIntent(text string) bool {
|
||||
return false
|
||||
}
|
||||
return containsAny(lower, []string{"模型", "model", "provider", "deepseek", "openai", "claude", "gemini", "qwen", "kimi", "grok", "minimax"}) &&
|
||||
containsAny(lower, []string{"创建", "新建", "修改", "编辑", "更新", "删除", "查询", "查看", "列出", "create", "update", "delete", "list", "show"})
|
||||
containsAny(lower, []string{"创建", "新建", "修改", "编辑", "更新", "改", "改一下", "删除", "删了", "查询", "查看", "列出", "启用", "禁用", "改名", "rename", "create", "update", "delete", "list", "show", "enable", "disable"})
|
||||
}
|
||||
|
||||
func detectStrategyManagementIntent(text string) bool {
|
||||
@@ -41,8 +44,11 @@ func detectStrategyManagementIntent(text string) bool {
|
||||
if lower == "" {
|
||||
return false
|
||||
}
|
||||
if wantsDefaultStrategyConfig(text) {
|
||||
return true
|
||||
}
|
||||
return containsAny(lower, []string{"策略", "strategy"}) &&
|
||||
containsAny(lower, []string{"创建", "新建", "修改", "编辑", "更新", "删除", "查询", "查看", "列出", "激活", "复制", "create", "update", "delete", "list", "show", "activate", "duplicate"})
|
||||
containsAny(lower, []string{"创建", "新建", "修改", "编辑", "更新", "改", "改一下", "改成", "改为", "删除", "删了", "查询", "查看", "列出", "激活", "复制", "参数", "配置", "详情", "详细", "prompt", "提示词", "什么样", "怎么样", "create", "update", "delete", "list", "show", "activate", "duplicate", "detail", "details", "config", "configuration", "parameter", "prompt", "what kind"})
|
||||
}
|
||||
|
||||
func detectTraderDiagnosisSkill(text string) bool {
|
||||
@@ -62,8 +68,9 @@ func detectManagementAction(text string, domain string) string {
|
||||
if lower == "" {
|
||||
return ""
|
||||
}
|
||||
hasUpdateVerb := containsAny(lower, []string{"修改", "编辑", "更新", "改", "rename", "update", "切换", "换成", "换到"})
|
||||
switch {
|
||||
case containsAny(lower, []string{"删除", "删掉", "remove", "delete"}):
|
||||
case containsAny(lower, []string{"删除", "删掉", "删了", "remove", "delete"}):
|
||||
return "delete"
|
||||
case containsAny(lower, []string{"启动", "开始", "run", "start"}) && domain == "trader":
|
||||
return "start"
|
||||
@@ -73,10 +80,32 @@ func detectManagementAction(text string, domain string) string {
|
||||
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"
|
||||
return "query_list"
|
||||
case containsAny(lower, []string{"创建", "新建", "加一个", "create", "new"}):
|
||||
return "create"
|
||||
default:
|
||||
@@ -152,6 +181,21 @@ func fieldValue(session skillSession, key string) string {
|
||||
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
|
||||
@@ -173,6 +217,18 @@ func (a *Agent) handleTraderManagementSkill(storeUserID string, userID int64, la
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -186,7 +242,13 @@ func (a *Agent) handleExchangeManagementSkill(storeUserID string, userID int64,
|
||||
}
|
||||
options := a.loadExchangeOptions(storeUserID)
|
||||
switch action {
|
||||
case "query":
|
||||
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
|
||||
@@ -205,7 +267,13 @@ func (a *Agent) handleModelManagementSkill(storeUserID string, userID int64, lan
|
||||
}
|
||||
options := a.loadEnabledModelOptions(storeUserID)
|
||||
switch action {
|
||||
case "query":
|
||||
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
|
||||
@@ -219,12 +287,24 @@ func (a *Agent) handleStrategyManagementSkill(storeUserID string, userID int64,
|
||||
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":
|
||||
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
|
||||
@@ -233,6 +313,350 @@ func (a *Agent) handleStrategyManagementSkill(storeUserID string, userID int64,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -252,6 +676,9 @@ func (a *Agent) handleExchangeCreateSkill(storeUserID string, userID int64, lang
|
||||
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" {
|
||||
@@ -267,6 +694,7 @@ func (a *Agent) handleExchangeCreateSkill(storeUserID string, userID int64, lang
|
||||
}
|
||||
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。"
|
||||
@@ -277,6 +705,7 @@ func (a *Agent) handleExchangeCreateSkill(storeUserID string, userID int64, lang
|
||||
if accountName == "" {
|
||||
accountName = "Default"
|
||||
}
|
||||
setSkillDAGStep(&session, "execute_create")
|
||||
args := map[string]any{
|
||||
"action": "create",
|
||||
"exchange_type": exType,
|
||||
@@ -302,6 +731,9 @@ func (a *Agent) handleModelCreateSkill(storeUserID string, userID int64, lang, t
|
||||
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" {
|
||||
@@ -320,17 +752,19 @@ func (a *Agent) handleModelCreateSkill(storeUserID string, userID int64, lang, t
|
||||
}
|
||||
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"),
|
||||
"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)
|
||||
@@ -353,6 +787,9 @@ func (a *Agent) handleStrategyCreateSkill(storeUserID string, userID int64, lang
|
||||
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" {
|
||||
@@ -371,12 +808,14 @@ func (a *Agent) handleStrategyCreateSkill(storeUserID string, userID int64, lang
|
||||
}
|
||||
}
|
||||
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))
|
||||
@@ -408,22 +847,65 @@ func (a *Agent) handleSimpleEntitySkill(storeUserID string, userID int64, lang,
|
||||
if session.Name != skillName || session.Action != action {
|
||||
return "", false
|
||||
}
|
||||
session.TargetRef = resolveTargetFromText(text, options, session.TargetRef)
|
||||
if session.TargetRef == nil && action != "query" {
|
||||
a.saveSkillSession(userID, session)
|
||||
label := formatOptionList("可选对象:", options)
|
||||
if lang == "zh" {
|
||||
reply := "我还需要你明确要操作的是哪一个对象。"
|
||||
|
||||
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
|
||||
}
|
||||
reply := "I still need you to specify which object to operate on."
|
||||
if label != "" {
|
||||
reply += "\n" + label
|
||||
}
|
||||
return reply, true
|
||||
}
|
||||
|
||||
switch skillName {
|
||||
|
||||
Reference in New Issue
Block a user