mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-09 22:10:57 +08:00
3124 lines
101 KiB
Go
3124 lines
101 KiB
Go
package agent
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"nofx/store"
|
||
)
|
||
|
||
var (
|
||
firstIntegerPattern = regexp.MustCompile(`\d+`)
|
||
firstFloatPattern = regexp.MustCompile(`\d+(?:\.\d+)?`)
|
||
timeframeTokenRE = regexp.MustCompile(`(?i)\b\d{1,2}[mhdw]\b`)
|
||
coinSymbolTokenRE = regexp.MustCompile(`(?i)^(?:xyz:)?[a-z0-9._-]{2,20}(?:usdt|usd|-usdc)?$`)
|
||
quotedContentRE = regexp.MustCompile(`[“"]([^“”"]{1,200})[”"]`)
|
||
)
|
||
|
||
const (
|
||
strategyPendingUpdateConfigField = "_pending_strategy_update_config"
|
||
strategyPendingUpdateWarnings = "_pending_strategy_update_warnings"
|
||
strategyPendingUpdateZhMsg = "_pending_strategy_update_zh_msg"
|
||
strategyPendingUpdateEnMsg = "_pending_strategy_update_en_msg"
|
||
)
|
||
|
||
func generatedDraftRequiresConfirmation(session skillSession) bool {
|
||
return fieldValue(session, "_requires_generated_confirmation") == "true"
|
||
}
|
||
|
||
func clearGeneratedDraftConfirmation(session *skillSession, keys ...string) {
|
||
if session == nil || session.Fields == nil {
|
||
return
|
||
}
|
||
delete(session.Fields, "_requires_generated_confirmation")
|
||
for _, key := range keys {
|
||
if strings.TrimSpace(key) != "" {
|
||
delete(session.Fields, key)
|
||
}
|
||
}
|
||
}
|
||
|
||
func parseStandaloneInteger(text string) (int, bool) {
|
||
return 0, false
|
||
}
|
||
|
||
func parseStandaloneFloat(text string) (float64, bool) {
|
||
return 0, false
|
||
}
|
||
|
||
func parseEnabledValue(text string) (bool, bool) {
|
||
return false, false
|
||
}
|
||
|
||
func parseFlagValue(text string, keywords []string) (bool, bool) {
|
||
return false, false
|
||
}
|
||
|
||
func extractCredentialValue(text string, keywords []string) string {
|
||
return ""
|
||
}
|
||
|
||
func parseScanIntervalMinutes(text string) (int, bool) {
|
||
return 0, false
|
||
}
|
||
|
||
type entityFieldPatch struct {
|
||
Field string
|
||
Value string
|
||
}
|
||
|
||
func entityFieldPatchesFromCatalog(catalog []entityFieldMeta, values map[string]string) []entityFieldPatch {
|
||
patches := make([]entityFieldPatch, 0, len(values))
|
||
for _, meta := range catalog {
|
||
value := strings.TrimSpace(values[meta.Key])
|
||
if value == "" {
|
||
continue
|
||
}
|
||
patches = append(patches, entityFieldPatch{Field: meta.Key, Value: value})
|
||
}
|
||
return patches
|
||
}
|
||
|
||
func fieldMetaByKey(catalog []entityFieldMeta, key string) (entityFieldMeta, bool) {
|
||
for _, meta := range catalog {
|
||
if meta.Key == key {
|
||
return meta, true
|
||
}
|
||
}
|
||
return entityFieldMeta{}, false
|
||
}
|
||
|
||
func parseFieldValueFromMeta(text string, meta entityFieldMeta) (string, bool) {
|
||
return "", false
|
||
}
|
||
|
||
func detectCatalogField(text string, catalog []entityFieldMeta) string {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if lower == "" {
|
||
return ""
|
||
}
|
||
if strings.Contains(lower, "api key index") || strings.Contains(lower, "lighter api key index") {
|
||
for _, meta := range catalog {
|
||
if meta.Key == "lighter_api_key_index" {
|
||
return meta.Key
|
||
}
|
||
}
|
||
}
|
||
bestKey := ""
|
||
bestLen := -1
|
||
for _, meta := range catalog {
|
||
for _, keyword := range meta.Keywords {
|
||
normalized := strings.ToLower(strings.TrimSpace(keyword))
|
||
if normalized == "" {
|
||
continue
|
||
}
|
||
if entityFieldExplicitlyMentioned(lower, []string{normalized}) && len([]rune(normalized)) > bestLen {
|
||
bestKey = meta.Key
|
||
bestLen = len([]rune(normalized))
|
||
}
|
||
}
|
||
}
|
||
return bestKey
|
||
}
|
||
|
||
func looksLikeBareFieldSelection(text string, keywords []string) bool {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if lower == "" {
|
||
return false
|
||
}
|
||
trimNoise := func(s string) string {
|
||
s = strings.TrimSpace(s)
|
||
for _, prefix := range []string{"改", "改一下", "改下", "修改", "更新", "设置", "设成", "设为", "change", "update", "set"} {
|
||
s = strings.TrimSpace(strings.TrimPrefix(s, prefix))
|
||
}
|
||
for _, suffix := range []string{"呢", "吧", "呀", "一下", "这个", "字段", "配置"} {
|
||
s = strings.TrimSpace(strings.TrimSuffix(s, suffix))
|
||
}
|
||
return strings.Trim(s, "“”\"'::,,。.;;")
|
||
}
|
||
normalizedText := trimNoise(lower)
|
||
for _, keyword := range keywords {
|
||
normalizedKeyword := trimNoise(strings.ToLower(keyword))
|
||
if normalizedKeyword != "" && normalizedText == normalizedKeyword {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func displayCatalogFieldName(field, lang string) string {
|
||
switch field {
|
||
case "name":
|
||
if lang == "zh" {
|
||
return "名称"
|
||
}
|
||
return "name"
|
||
case "ai_model_id":
|
||
if lang == "zh" {
|
||
return "模型"
|
||
}
|
||
return "model"
|
||
case "exchange_id":
|
||
if lang == "zh" {
|
||
return "交易所"
|
||
}
|
||
return "exchange"
|
||
case "strategy_id":
|
||
if lang == "zh" {
|
||
return "策略"
|
||
}
|
||
return "strategy"
|
||
case "initial_balance":
|
||
if lang == "zh" {
|
||
return "初始资金"
|
||
}
|
||
return "initial balance"
|
||
case "scan_interval_minutes":
|
||
if lang == "zh" {
|
||
return "扫描间隔"
|
||
}
|
||
return "scan interval"
|
||
case "is_cross_margin":
|
||
if lang == "zh" {
|
||
return "全仓模式"
|
||
}
|
||
return "cross margin"
|
||
case "show_in_competition":
|
||
if lang == "zh" {
|
||
return "竞技场显示"
|
||
}
|
||
return "show in competition"
|
||
case "enabled":
|
||
if lang == "zh" {
|
||
return "启用状态"
|
||
}
|
||
return "enabled state"
|
||
case "api_key":
|
||
return "API Key"
|
||
case "custom_api_url":
|
||
if lang == "zh" {
|
||
return "接口地址"
|
||
}
|
||
return "API URL"
|
||
case "custom_model_name":
|
||
if lang == "zh" {
|
||
return "模型名称"
|
||
}
|
||
return "model name"
|
||
case "account_name":
|
||
if lang == "zh" {
|
||
return "账户名"
|
||
}
|
||
return "account name"
|
||
case "exchange_type":
|
||
if lang == "zh" {
|
||
return "交易所类型"
|
||
}
|
||
return "exchange type"
|
||
case "secret_key":
|
||
return "Secret"
|
||
case "passphrase":
|
||
return "Passphrase"
|
||
case "testnet":
|
||
if lang == "zh" {
|
||
return "测试网"
|
||
}
|
||
return "testnet"
|
||
case "hyperliquid_wallet_addr":
|
||
if lang == "zh" {
|
||
return "Hyperliquid 钱包地址"
|
||
}
|
||
return "Hyperliquid wallet address"
|
||
case "hyperliquid_unified_account":
|
||
if lang == "zh" {
|
||
return "Hyperliquid Unified Account"
|
||
}
|
||
return "Hyperliquid unified account"
|
||
case "aster_user":
|
||
if lang == "zh" {
|
||
return "Aster User"
|
||
}
|
||
return "Aster user"
|
||
case "aster_signer":
|
||
if lang == "zh" {
|
||
return "Aster Signer"
|
||
}
|
||
return "Aster signer"
|
||
case "aster_private_key":
|
||
if lang == "zh" {
|
||
return "Aster 私钥"
|
||
}
|
||
return "Aster private key"
|
||
case "lighter_wallet_addr":
|
||
if lang == "zh" {
|
||
return "Lighter 钱包地址"
|
||
}
|
||
return "Lighter wallet address"
|
||
case "lighter_private_key":
|
||
if lang == "zh" {
|
||
return "Lighter 私钥"
|
||
}
|
||
return "Lighter private key"
|
||
case "lighter_api_key_private_key":
|
||
if lang == "zh" {
|
||
return "Lighter API Key 私钥"
|
||
}
|
||
return "Lighter API key private key"
|
||
case "lighter_api_key_index":
|
||
if lang == "zh" {
|
||
return "Lighter API Key Index"
|
||
}
|
||
return "Lighter API key index"
|
||
default:
|
||
if lang == "zh" {
|
||
return field
|
||
}
|
||
return field
|
||
}
|
||
}
|
||
|
||
func detectCatalogDomainFromText(text string) string {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
switch {
|
||
case containsAny(lower, []string{"策略", "strategy"}):
|
||
return "strategy_management"
|
||
case containsAny(lower, []string{"交易所", "exchange"}):
|
||
return "exchange_management"
|
||
case containsAny(lower, []string{"模型", "model"}):
|
||
return "model_management"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func (a *Agent) executeAtomicSkillWithSession(storeUserID string, userID int64, lang, text string, session skillSession) string {
|
||
if answer, ok := a.dispatchBridgedSkillSession(storeUserID, userID, lang, text, session); ok {
|
||
return answer
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func parseLooseTextValue(text string) string {
|
||
return ""
|
||
}
|
||
|
||
func parseModelFieldValue(text, field string) (string, bool) {
|
||
return "", false
|
||
}
|
||
|
||
func parseExchangeFieldValue(text, field string) (string, bool) {
|
||
return "", false
|
||
}
|
||
|
||
func (a *Agent) parseTraderFieldValue(storeUserID, text, field string) (string, bool) {
|
||
return "", false
|
||
}
|
||
|
||
func detectCatalogFieldPatches(text string, catalog []entityFieldMeta, overrides map[string]string) []entityFieldPatch {
|
||
return nil
|
||
}
|
||
|
||
func entityFieldExplicitlyMentioned(text string, keywords []string) bool {
|
||
if len(keywords) == 0 {
|
||
return false
|
||
}
|
||
return containsAny(strings.ToLower(text), keywords)
|
||
}
|
||
|
||
func hasTraderUpdatePatch(text string) bool {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if lower == "" {
|
||
return false
|
||
}
|
||
return len(detectTraderUpdatePatches(nil, "", text)) > 0
|
||
}
|
||
|
||
func hasModelUpdatePatch(text string) bool {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if lower == "" {
|
||
return false
|
||
}
|
||
return len(detectModelUpdatePatches(text)) > 0
|
||
}
|
||
|
||
func hasExchangeUpdatePatch(text string) bool {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if lower == "" {
|
||
return false
|
||
}
|
||
return len(detectExchangeUpdatePatches(text)) > 0
|
||
}
|
||
|
||
type traderUpdateArgs struct {
|
||
AIModelID string
|
||
ExchangeID string
|
||
StrategyID string
|
||
InitialBalance *float64
|
||
ScanIntervalMinutes *int
|
||
IsCrossMargin *bool
|
||
ShowInCompetition *bool
|
||
BTCETHLeverage *int
|
||
AltcoinLeverage *int
|
||
TradingSymbols string
|
||
CustomPrompt string
|
||
OverrideBasePrompt *bool
|
||
SystemPromptTemplate string
|
||
UseAI500 *bool
|
||
UseOITop *bool
|
||
}
|
||
|
||
func (a traderUpdateArgs) hasAny() bool {
|
||
return a.AIModelID != "" || a.ExchangeID != "" || a.StrategyID != "" || a.InitialBalance != nil ||
|
||
a.ScanIntervalMinutes != nil || a.IsCrossMargin != nil || a.ShowInCompetition != nil ||
|
||
a.BTCETHLeverage != nil || a.AltcoinLeverage != nil || a.TradingSymbols != "" ||
|
||
a.CustomPrompt != "" || a.OverrideBasePrompt != nil || a.SystemPromptTemplate != "" ||
|
||
a.UseAI500 != nil || a.UseOITop != nil
|
||
}
|
||
|
||
func parseStandaloneTraderUpdateArgs(text string) traderUpdateArgs {
|
||
return traderUpdateArgs{}
|
||
}
|
||
|
||
func mergeTraderUpdateArgs(base, patch traderUpdateArgs) traderUpdateArgs {
|
||
if patch.AIModelID != "" {
|
||
base.AIModelID = patch.AIModelID
|
||
}
|
||
if patch.ExchangeID != "" {
|
||
base.ExchangeID = patch.ExchangeID
|
||
}
|
||
if patch.StrategyID != "" {
|
||
base.StrategyID = patch.StrategyID
|
||
}
|
||
if patch.InitialBalance != nil {
|
||
base.InitialBalance = patch.InitialBalance
|
||
}
|
||
if patch.ScanIntervalMinutes != nil {
|
||
base.ScanIntervalMinutes = patch.ScanIntervalMinutes
|
||
}
|
||
if patch.IsCrossMargin != nil {
|
||
base.IsCrossMargin = patch.IsCrossMargin
|
||
}
|
||
if patch.ShowInCompetition != nil {
|
||
base.ShowInCompetition = patch.ShowInCompetition
|
||
}
|
||
if patch.BTCETHLeverage != nil {
|
||
base.BTCETHLeverage = patch.BTCETHLeverage
|
||
}
|
||
if patch.AltcoinLeverage != nil {
|
||
base.AltcoinLeverage = patch.AltcoinLeverage
|
||
}
|
||
if patch.TradingSymbols != "" {
|
||
base.TradingSymbols = patch.TradingSymbols
|
||
}
|
||
if patch.CustomPrompt != "" {
|
||
base.CustomPrompt = patch.CustomPrompt
|
||
}
|
||
if patch.OverrideBasePrompt != nil {
|
||
base.OverrideBasePrompt = patch.OverrideBasePrompt
|
||
}
|
||
if patch.SystemPromptTemplate != "" {
|
||
base.SystemPromptTemplate = patch.SystemPromptTemplate
|
||
}
|
||
if patch.UseAI500 != nil {
|
||
base.UseAI500 = patch.UseAI500
|
||
}
|
||
if patch.UseOITop != nil {
|
||
base.UseOITop = patch.UseOITop
|
||
}
|
||
return base
|
||
}
|
||
|
||
func buildTraderUpdateArgs(a *Agent, storeUserID string, text string) traderUpdateArgs {
|
||
return traderUpdateArgs{}
|
||
}
|
||
|
||
func detectTraderUpdatePatches(a *Agent, storeUserID, text string) []entityFieldPatch {
|
||
return nil
|
||
}
|
||
|
||
func applyTraderUpdateArgsToSession(session *skillSession, args traderUpdateArgs) {
|
||
if args.AIModelID != "" {
|
||
setField(session, "ai_model_id", args.AIModelID)
|
||
}
|
||
if args.ExchangeID != "" {
|
||
setField(session, "exchange_id", args.ExchangeID)
|
||
}
|
||
if args.StrategyID != "" {
|
||
setField(session, "strategy_id", args.StrategyID)
|
||
}
|
||
if args.InitialBalance != nil {
|
||
setField(session, "initial_balance", strconv.FormatFloat(*args.InitialBalance, 'f', -1, 64))
|
||
}
|
||
if args.ScanIntervalMinutes != nil {
|
||
setField(session, "scan_interval_minutes", strconv.Itoa(*args.ScanIntervalMinutes))
|
||
}
|
||
if args.IsCrossMargin != nil {
|
||
setField(session, "is_cross_margin", strconv.FormatBool(*args.IsCrossMargin))
|
||
}
|
||
if args.ShowInCompetition != nil {
|
||
setField(session, "show_in_competition", strconv.FormatBool(*args.ShowInCompetition))
|
||
}
|
||
if args.BTCETHLeverage != nil {
|
||
setField(session, "btc_eth_leverage", strconv.Itoa(*args.BTCETHLeverage))
|
||
}
|
||
if args.AltcoinLeverage != nil {
|
||
setField(session, "altcoin_leverage", strconv.Itoa(*args.AltcoinLeverage))
|
||
}
|
||
if args.TradingSymbols != "" {
|
||
setField(session, "trading_symbols", args.TradingSymbols)
|
||
}
|
||
if args.CustomPrompt != "" {
|
||
setField(session, "custom_prompt", args.CustomPrompt)
|
||
}
|
||
if args.OverrideBasePrompt != nil {
|
||
setField(session, "override_base_prompt", strconv.FormatBool(*args.OverrideBasePrompt))
|
||
}
|
||
if args.SystemPromptTemplate != "" {
|
||
setField(session, "system_prompt_template", args.SystemPromptTemplate)
|
||
}
|
||
if args.UseAI500 != nil {
|
||
setField(session, "use_ai500", strconv.FormatBool(*args.UseAI500))
|
||
}
|
||
if args.UseOITop != nil {
|
||
setField(session, "use_oi_top", strconv.FormatBool(*args.UseOITop))
|
||
}
|
||
}
|
||
|
||
func buildTraderUpdateArgsFromSession(session skillSession) traderUpdateArgs {
|
||
var args traderUpdateArgs
|
||
args.AIModelID = fieldValue(session, "ai_model_id")
|
||
args.ExchangeID = fieldValue(session, "exchange_id")
|
||
args.StrategyID = fieldValue(session, "strategy_id")
|
||
if value := fieldValue(session, "initial_balance"); value != "" {
|
||
if parsed, err := strconv.ParseFloat(value, 64); err == nil {
|
||
args.InitialBalance = &parsed
|
||
}
|
||
}
|
||
if value := fieldValue(session, "scan_interval_minutes"); value != "" {
|
||
if parsed, err := strconv.Atoi(value); err == nil {
|
||
args.ScanIntervalMinutes = &parsed
|
||
}
|
||
}
|
||
if value := fieldValue(session, "is_cross_margin"); value != "" {
|
||
parsed := value == "true"
|
||
args.IsCrossMargin = &parsed
|
||
}
|
||
if value := fieldValue(session, "show_in_competition"); value != "" {
|
||
parsed := value == "true"
|
||
args.ShowInCompetition = &parsed
|
||
}
|
||
if value := fieldValue(session, "btc_eth_leverage"); value != "" {
|
||
if parsed, err := strconv.Atoi(value); err == nil {
|
||
args.BTCETHLeverage = &parsed
|
||
}
|
||
}
|
||
if value := fieldValue(session, "altcoin_leverage"); value != "" {
|
||
if parsed, err := strconv.Atoi(value); err == nil {
|
||
args.AltcoinLeverage = &parsed
|
||
}
|
||
}
|
||
args.TradingSymbols = fieldValue(session, "trading_symbols")
|
||
args.CustomPrompt = fieldValue(session, "custom_prompt")
|
||
if value := fieldValue(session, "override_base_prompt"); value != "" {
|
||
parsed := value == "true"
|
||
args.OverrideBasePrompt = &parsed
|
||
}
|
||
args.SystemPromptTemplate = fieldValue(session, "system_prompt_template")
|
||
if value := fieldValue(session, "use_ai500"); value != "" {
|
||
parsed := value == "true"
|
||
args.UseAI500 = &parsed
|
||
}
|
||
if value := fieldValue(session, "use_oi_top"); value != "" {
|
||
parsed := value == "true"
|
||
args.UseOITop = &parsed
|
||
}
|
||
return args
|
||
}
|
||
|
||
type modelUpdatePatch struct {
|
||
Enabled *bool
|
||
APIKey string
|
||
CustomAPIURL string
|
||
CustomModelName string
|
||
}
|
||
|
||
func (p modelUpdatePatch) hasAny() bool {
|
||
return p.Enabled != nil || p.APIKey != "" || p.CustomAPIURL != "" || p.CustomModelName != ""
|
||
}
|
||
|
||
func buildModelUpdatePatch(text string) modelUpdatePatch {
|
||
return modelUpdatePatch{}
|
||
}
|
||
|
||
func detectModelUpdatePatches(text string) []entityFieldPatch {
|
||
return nil
|
||
}
|
||
|
||
func applyModelUpdatePatchToSession(session *skillSession, patch modelUpdatePatch) {
|
||
if patch.CustomAPIURL != "" {
|
||
setField(session, "custom_api_url", patch.CustomAPIURL)
|
||
}
|
||
if patch.Enabled != nil {
|
||
setField(session, "enabled", strconv.FormatBool(*patch.Enabled))
|
||
}
|
||
if patch.APIKey != "" {
|
||
setField(session, "api_key", patch.APIKey)
|
||
}
|
||
if patch.CustomModelName != "" {
|
||
setField(session, "custom_model_name", patch.CustomModelName)
|
||
}
|
||
}
|
||
|
||
func mergeModelUpdatePatch(base, patch modelUpdatePatch) modelUpdatePatch {
|
||
if patch.Enabled != nil {
|
||
base.Enabled = patch.Enabled
|
||
}
|
||
if patch.APIKey != "" {
|
||
base.APIKey = patch.APIKey
|
||
}
|
||
if patch.CustomAPIURL != "" {
|
||
base.CustomAPIURL = patch.CustomAPIURL
|
||
}
|
||
if patch.CustomModelName != "" {
|
||
base.CustomModelName = patch.CustomModelName
|
||
}
|
||
return base
|
||
}
|
||
|
||
func buildModelUpdatePatchFromSession(session skillSession) modelUpdatePatch {
|
||
var patch modelUpdatePatch
|
||
if value := fieldValue(session, "enabled"); value != "" {
|
||
parsed := value == "true"
|
||
patch.Enabled = &parsed
|
||
}
|
||
patch.APIKey = fieldValue(session, "api_key")
|
||
patch.CustomAPIURL = fieldValue(session, "custom_api_url")
|
||
patch.CustomModelName = fieldValue(session, "custom_model_name")
|
||
return patch
|
||
}
|
||
|
||
type exchangeUpdatePatch struct {
|
||
AccountName string
|
||
Enabled *bool
|
||
APIKey string
|
||
SecretKey string
|
||
Passphrase string
|
||
Testnet *bool
|
||
HyperliquidWalletAddr string
|
||
AsterUser string
|
||
AsterSigner string
|
||
AsterPrivateKey string
|
||
LighterWalletAddr string
|
||
LighterAPIKeyPrivateKey string
|
||
LighterAPIKeyIndex *int
|
||
}
|
||
|
||
func (p exchangeUpdatePatch) hasAny() bool {
|
||
return p.AccountName != "" || p.Enabled != nil || p.APIKey != "" || p.SecretKey != "" ||
|
||
p.Passphrase != "" || p.Testnet != nil || p.HyperliquidWalletAddr != "" || p.AsterUser != "" ||
|
||
p.AsterSigner != "" || p.AsterPrivateKey != "" || p.LighterWalletAddr != "" ||
|
||
p.LighterAPIKeyPrivateKey != "" || p.LighterAPIKeyIndex != nil
|
||
}
|
||
|
||
func buildExchangeUpdatePatch(text string) exchangeUpdatePatch {
|
||
return exchangeUpdatePatch{}
|
||
}
|
||
|
||
func detectExchangeUpdatePatches(text string) []entityFieldPatch {
|
||
return nil
|
||
}
|
||
|
||
func applyExchangeUpdatePatchToSession(session *skillSession, patch exchangeUpdatePatch) {
|
||
if patch.AccountName != "" {
|
||
setField(session, "account_name", patch.AccountName)
|
||
}
|
||
if patch.Enabled != nil {
|
||
setField(session, "enabled", strconv.FormatBool(*patch.Enabled))
|
||
}
|
||
if patch.APIKey != "" {
|
||
setField(session, "api_key", patch.APIKey)
|
||
}
|
||
if patch.SecretKey != "" {
|
||
setField(session, "secret_key", patch.SecretKey)
|
||
}
|
||
if patch.Passphrase != "" {
|
||
setField(session, "passphrase", patch.Passphrase)
|
||
}
|
||
if patch.Testnet != nil {
|
||
setField(session, "testnet", strconv.FormatBool(*patch.Testnet))
|
||
}
|
||
if patch.HyperliquidWalletAddr != "" {
|
||
setField(session, "hyperliquid_wallet_addr", patch.HyperliquidWalletAddr)
|
||
}
|
||
if patch.AsterUser != "" {
|
||
setField(session, "aster_user", patch.AsterUser)
|
||
}
|
||
if patch.AsterSigner != "" {
|
||
setField(session, "aster_signer", patch.AsterSigner)
|
||
}
|
||
if patch.AsterPrivateKey != "" {
|
||
setField(session, "aster_private_key", patch.AsterPrivateKey)
|
||
}
|
||
if patch.LighterWalletAddr != "" {
|
||
setField(session, "lighter_wallet_addr", patch.LighterWalletAddr)
|
||
}
|
||
if patch.LighterAPIKeyPrivateKey != "" {
|
||
setField(session, "lighter_api_key_private_key", patch.LighterAPIKeyPrivateKey)
|
||
}
|
||
if patch.LighterAPIKeyIndex != nil {
|
||
setField(session, "lighter_api_key_index", strconv.Itoa(*patch.LighterAPIKeyIndex))
|
||
}
|
||
}
|
||
|
||
func mergeExchangeUpdatePatch(base, patch exchangeUpdatePatch) exchangeUpdatePatch {
|
||
if patch.AccountName != "" {
|
||
base.AccountName = patch.AccountName
|
||
}
|
||
if patch.Enabled != nil {
|
||
base.Enabled = patch.Enabled
|
||
}
|
||
if patch.APIKey != "" {
|
||
base.APIKey = patch.APIKey
|
||
}
|
||
if patch.SecretKey != "" {
|
||
base.SecretKey = patch.SecretKey
|
||
}
|
||
if patch.Passphrase != "" {
|
||
base.Passphrase = patch.Passphrase
|
||
}
|
||
if patch.Testnet != nil {
|
||
base.Testnet = patch.Testnet
|
||
}
|
||
if patch.HyperliquidWalletAddr != "" {
|
||
base.HyperliquidWalletAddr = patch.HyperliquidWalletAddr
|
||
}
|
||
if patch.AsterUser != "" {
|
||
base.AsterUser = patch.AsterUser
|
||
}
|
||
if patch.AsterSigner != "" {
|
||
base.AsterSigner = patch.AsterSigner
|
||
}
|
||
if patch.AsterPrivateKey != "" {
|
||
base.AsterPrivateKey = patch.AsterPrivateKey
|
||
}
|
||
if patch.LighterWalletAddr != "" {
|
||
base.LighterWalletAddr = patch.LighterWalletAddr
|
||
}
|
||
if patch.LighterAPIKeyPrivateKey != "" {
|
||
base.LighterAPIKeyPrivateKey = patch.LighterAPIKeyPrivateKey
|
||
}
|
||
if patch.LighterAPIKeyIndex != nil {
|
||
base.LighterAPIKeyIndex = patch.LighterAPIKeyIndex
|
||
}
|
||
return base
|
||
}
|
||
|
||
func buildExchangeUpdatePatchFromSession(session skillSession) exchangeUpdatePatch {
|
||
var patch exchangeUpdatePatch
|
||
patch.AccountName = fieldValue(session, "account_name")
|
||
if value := fieldValue(session, "enabled"); value != "" {
|
||
parsed := value == "true"
|
||
patch.Enabled = &parsed
|
||
}
|
||
patch.APIKey = fieldValue(session, "api_key")
|
||
patch.SecretKey = fieldValue(session, "secret_key")
|
||
patch.Passphrase = fieldValue(session, "passphrase")
|
||
if value := fieldValue(session, "testnet"); value != "" {
|
||
parsed := value == "true"
|
||
patch.Testnet = &parsed
|
||
}
|
||
patch.HyperliquidWalletAddr = fieldValue(session, "hyperliquid_wallet_addr")
|
||
patch.AsterUser = fieldValue(session, "aster_user")
|
||
patch.AsterSigner = fieldValue(session, "aster_signer")
|
||
patch.AsterPrivateKey = fieldValue(session, "aster_private_key")
|
||
patch.LighterWalletAddr = fieldValue(session, "lighter_wallet_addr")
|
||
patch.LighterAPIKeyPrivateKey = fieldValue(session, "lighter_api_key_private_key")
|
||
if value := fieldValue(session, "lighter_api_key_index"); value != "" {
|
||
if parsed, err := strconv.Atoi(value); err == nil {
|
||
patch.LighterAPIKeyIndex = &parsed
|
||
}
|
||
}
|
||
return patch
|
||
}
|
||
|
||
func detectStrategyConfigField(text string) string {
|
||
return ""
|
||
}
|
||
|
||
func strategyConfigFieldDisplayName(field, lang string) string {
|
||
switch field {
|
||
case "name":
|
||
if lang == "zh" {
|
||
return "名称"
|
||
}
|
||
return "name"
|
||
case "strategy_type":
|
||
if lang == "zh" {
|
||
return "策略类型"
|
||
}
|
||
return "strategy type"
|
||
case "symbol":
|
||
if lang == "zh" {
|
||
return "交易对"
|
||
}
|
||
return "symbol"
|
||
case "grid_count":
|
||
if lang == "zh" {
|
||
return "网格数量"
|
||
}
|
||
return "grid count"
|
||
case "total_investment":
|
||
if lang == "zh" {
|
||
return "总投资"
|
||
}
|
||
return "total investment"
|
||
case "upper_price":
|
||
if lang == "zh" {
|
||
return "上沿价格"
|
||
}
|
||
return "upper price"
|
||
case "lower_price":
|
||
if lang == "zh" {
|
||
return "下沿价格"
|
||
}
|
||
return "lower price"
|
||
case "use_atr_bounds":
|
||
if lang == "zh" {
|
||
return "ATR 自动边界"
|
||
}
|
||
return "use ATR bounds"
|
||
case "atr_multiplier":
|
||
if lang == "zh" {
|
||
return "ATR 倍数"
|
||
}
|
||
return "ATR multiplier"
|
||
case "distribution":
|
||
if lang == "zh" {
|
||
return "分布方式"
|
||
}
|
||
return "distribution"
|
||
case "enable_direction_adjust":
|
||
if lang == "zh" {
|
||
return "方向自适应"
|
||
}
|
||
return "enable direction adjust"
|
||
case "direction_bias_ratio":
|
||
if lang == "zh" {
|
||
return "方向偏置比例"
|
||
}
|
||
return "direction bias ratio"
|
||
case "max_drawdown_pct":
|
||
if lang == "zh" {
|
||
return "最大回撤"
|
||
}
|
||
return "max drawdown pct"
|
||
case "stop_loss_pct":
|
||
if lang == "zh" {
|
||
return "止损比例"
|
||
}
|
||
return "stop loss pct"
|
||
case "daily_loss_limit_pct":
|
||
if lang == "zh" {
|
||
return "日亏损限制"
|
||
}
|
||
return "daily loss limit pct"
|
||
case "use_maker_only":
|
||
if lang == "zh" {
|
||
return "仅 Maker"
|
||
}
|
||
return "use maker only"
|
||
case "description":
|
||
if lang == "zh" {
|
||
return "描述"
|
||
}
|
||
return "description"
|
||
case "is_public":
|
||
if lang == "zh" {
|
||
return "发布到市场"
|
||
}
|
||
return "publish to market"
|
||
case "config_visible":
|
||
if lang == "zh" {
|
||
return "配置可见"
|
||
}
|
||
return "config visible"
|
||
case "max_positions":
|
||
if lang == "zh" {
|
||
return "最大持仓"
|
||
}
|
||
return "max positions"
|
||
case "min_confidence":
|
||
if lang == "zh" {
|
||
return "最小置信度"
|
||
}
|
||
return "min confidence"
|
||
case "min_risk_reward_ratio":
|
||
if lang == "zh" {
|
||
return "最小盈亏比"
|
||
}
|
||
return "min risk reward ratio"
|
||
case "leverage":
|
||
if lang == "zh" {
|
||
return "杠杆"
|
||
}
|
||
return "leverage"
|
||
case "btceth_max_leverage":
|
||
if lang == "zh" {
|
||
return "BTC/ETH 最大杠杆"
|
||
}
|
||
return "BTC/ETH max leverage"
|
||
case "altcoin_max_leverage":
|
||
if lang == "zh" {
|
||
return "山寨币最大杠杆"
|
||
}
|
||
return "altcoin max leverage"
|
||
case "btceth_max_position_value_ratio":
|
||
if lang == "zh" {
|
||
return "BTC/ETH 最大仓位价值倍数"
|
||
}
|
||
return "BTC/ETH max position value ratio"
|
||
case "altcoin_max_position_value_ratio":
|
||
if lang == "zh" {
|
||
return "山寨币最大仓位价值倍数"
|
||
}
|
||
return "altcoin max position value ratio"
|
||
case "max_margin_usage":
|
||
if lang == "zh" {
|
||
return "最大保证金使用率"
|
||
}
|
||
return "max margin usage"
|
||
case "min_position_size":
|
||
if lang == "zh" {
|
||
return "最小开仓金额"
|
||
}
|
||
return "min position size"
|
||
case "enable_ema":
|
||
if lang == "zh" {
|
||
return "EMA"
|
||
}
|
||
return "EMA"
|
||
case "enable_macd":
|
||
if lang == "zh" {
|
||
return "MACD"
|
||
}
|
||
return "MACD"
|
||
case "enable_rsi":
|
||
if lang == "zh" {
|
||
return "RSI"
|
||
}
|
||
return "RSI"
|
||
case "enable_atr":
|
||
if lang == "zh" {
|
||
return "ATR"
|
||
}
|
||
return "ATR"
|
||
case "enable_boll":
|
||
if lang == "zh" {
|
||
return "Bollinger"
|
||
}
|
||
return "Bollinger"
|
||
case "enable_all_core_indicators":
|
||
if lang == "zh" {
|
||
return "全部核心指标"
|
||
}
|
||
return "all core indicators"
|
||
case "primary_timeframe":
|
||
if lang == "zh" {
|
||
return "主周期"
|
||
}
|
||
return "primary timeframe"
|
||
case "selected_timeframes":
|
||
if lang == "zh" {
|
||
return "多周期时间框架"
|
||
}
|
||
return "selected timeframes"
|
||
case "source_type":
|
||
if lang == "zh" {
|
||
return "来源类型"
|
||
}
|
||
return "source type"
|
||
case "static_coins":
|
||
if lang == "zh" {
|
||
return "静态币种"
|
||
}
|
||
return "static coins"
|
||
case "excluded_coins":
|
||
if lang == "zh" {
|
||
return "排除币种"
|
||
}
|
||
return "excluded coins"
|
||
case "use_ai500":
|
||
if lang == "zh" {
|
||
return "AI500"
|
||
}
|
||
return "use AI500"
|
||
case "ai500_limit":
|
||
if lang == "zh" {
|
||
return "AI500 数量"
|
||
}
|
||
return "AI500 limit"
|
||
case "use_oi_top":
|
||
if lang == "zh" {
|
||
return "OI Top"
|
||
}
|
||
return "use OI Top"
|
||
case "oi_top_limit":
|
||
if lang == "zh" {
|
||
return "OI Top 数量"
|
||
}
|
||
return "OI Top limit"
|
||
case "use_oi_low":
|
||
if lang == "zh" {
|
||
return "OI Low"
|
||
}
|
||
return "use OI Low"
|
||
case "oi_low_limit":
|
||
if lang == "zh" {
|
||
return "OI Low 数量"
|
||
}
|
||
return "OI Low limit"
|
||
case "primary_count":
|
||
if lang == "zh" {
|
||
return "K线数量"
|
||
}
|
||
return "kline count"
|
||
case "ema_periods":
|
||
return "EMA periods"
|
||
case "rsi_periods":
|
||
return "RSI periods"
|
||
case "atr_periods":
|
||
return "ATR periods"
|
||
case "boll_periods":
|
||
return "BOLL periods"
|
||
case "enable_volume":
|
||
if lang == "zh" {
|
||
return "成交量"
|
||
}
|
||
return "volume"
|
||
case "enable_oi":
|
||
if lang == "zh" {
|
||
return "持仓量"
|
||
}
|
||
return "OI"
|
||
case "enable_funding_rate":
|
||
if lang == "zh" {
|
||
return "资金费率"
|
||
}
|
||
return "funding rate"
|
||
case "nofxos_api_key":
|
||
return "NofxOS API key"
|
||
case "enable_quant_data":
|
||
if lang == "zh" {
|
||
return "量化数据"
|
||
}
|
||
return "quant data"
|
||
case "enable_quant_oi":
|
||
return "quant OI"
|
||
case "enable_quant_netflow":
|
||
return "quant netflow"
|
||
case "enable_oi_ranking":
|
||
return "OI ranking"
|
||
case "oi_ranking_duration":
|
||
return "OI ranking duration"
|
||
case "oi_ranking_limit":
|
||
return "OI ranking limit"
|
||
case "enable_netflow_ranking":
|
||
return "netflow ranking"
|
||
case "netflow_ranking_duration":
|
||
return "netflow ranking duration"
|
||
case "netflow_ranking_limit":
|
||
return "netflow ranking limit"
|
||
case "enable_price_ranking":
|
||
return "price ranking"
|
||
case "price_ranking_duration":
|
||
return "price ranking duration"
|
||
case "price_ranking_limit":
|
||
return "price ranking limit"
|
||
case "role_definition":
|
||
if lang == "zh" {
|
||
return "角色定义"
|
||
}
|
||
return "role definition"
|
||
case "trading_frequency":
|
||
if lang == "zh" {
|
||
return "交易频率"
|
||
}
|
||
return "trading frequency"
|
||
case "entry_standards":
|
||
if lang == "zh" {
|
||
return "开仓标准"
|
||
}
|
||
return "entry standards"
|
||
case "decision_process":
|
||
if lang == "zh" {
|
||
return "决策流程"
|
||
}
|
||
return "decision process"
|
||
case "custom_prompt":
|
||
if lang == "zh" {
|
||
return "自定义 Prompt"
|
||
}
|
||
return "custom prompt"
|
||
default:
|
||
return field
|
||
}
|
||
}
|
||
|
||
func extractStrategyConfigValue(text, field string) (string, bool) {
|
||
return "", false
|
||
}
|
||
|
||
type strategyConfigPatch struct {
|
||
Field string
|
||
Value string
|
||
}
|
||
|
||
func detectStrategyConfigPatches(text string) []strategyConfigPatch {
|
||
return nil
|
||
}
|
||
|
||
func applyStrategyConfigPatch(cfg *store.StrategyConfig, field, value string) error {
|
||
ensureGridConfig := func() *store.GridStrategyConfig {
|
||
if cfg.GridConfig == nil {
|
||
defaults := store.GetDefaultStrategyConfig(cfg.Language)
|
||
if defaults.GridConfig != nil {
|
||
copy := *defaults.GridConfig
|
||
cfg.GridConfig = ©
|
||
} else {
|
||
cfg.GridConfig = &store.GridStrategyConfig{}
|
||
}
|
||
}
|
||
return cfg.GridConfig
|
||
}
|
||
|
||
switch field {
|
||
case "strategy_type":
|
||
cfg.StrategyType = value
|
||
case "symbol":
|
||
ensureGridConfig().Symbol = value
|
||
case "grid_count":
|
||
parsed, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return fmt.Errorf("网格数量需要是整数")
|
||
}
|
||
ensureGridConfig().GridCount = parsed
|
||
case "total_investment":
|
||
parsed, err := strconv.ParseFloat(value, 64)
|
||
if err != nil {
|
||
return fmt.Errorf("总投资需要是数字")
|
||
}
|
||
ensureGridConfig().TotalInvestment = parsed
|
||
case "upper_price":
|
||
parsed, err := strconv.ParseFloat(value, 64)
|
||
if err != nil {
|
||
return fmt.Errorf("上沿价格需要是数字")
|
||
}
|
||
ensureGridConfig().UpperPrice = parsed
|
||
case "lower_price":
|
||
parsed, err := strconv.ParseFloat(value, 64)
|
||
if err != nil {
|
||
return fmt.Errorf("下沿价格需要是数字")
|
||
}
|
||
ensureGridConfig().LowerPrice = parsed
|
||
case "use_atr_bounds":
|
||
ensureGridConfig().UseATRBounds = value == "true"
|
||
case "atr_multiplier":
|
||
parsed, err := strconv.ParseFloat(value, 64)
|
||
if err != nil {
|
||
return fmt.Errorf("ATR 倍数需要是数字")
|
||
}
|
||
ensureGridConfig().ATRMultiplier = parsed
|
||
case "distribution":
|
||
ensureGridConfig().Distribution = value
|
||
case "enable_direction_adjust":
|
||
ensureGridConfig().EnableDirectionAdjust = value == "true"
|
||
case "direction_bias_ratio":
|
||
parsed, err := strconv.ParseFloat(value, 64)
|
||
if err != nil {
|
||
return fmt.Errorf("方向偏置比例需要是数字")
|
||
}
|
||
ensureGridConfig().DirectionBiasRatio = parsed
|
||
case "max_drawdown_pct":
|
||
parsed, err := strconv.ParseFloat(value, 64)
|
||
if err != nil {
|
||
return fmt.Errorf("最大回撤需要是数字")
|
||
}
|
||
ensureGridConfig().MaxDrawdownPct = parsed
|
||
case "stop_loss_pct":
|
||
parsed, err := strconv.ParseFloat(value, 64)
|
||
if err != nil {
|
||
return fmt.Errorf("止损比例需要是数字")
|
||
}
|
||
ensureGridConfig().StopLossPct = parsed
|
||
case "daily_loss_limit_pct":
|
||
parsed, err := strconv.ParseFloat(value, 64)
|
||
if err != nil {
|
||
return fmt.Errorf("日亏损限制需要是数字")
|
||
}
|
||
ensureGridConfig().DailyLossLimitPct = parsed
|
||
case "use_maker_only":
|
||
ensureGridConfig().UseMakerOnly = value == "true"
|
||
case "description", "is_public", "config_visible":
|
||
return nil
|
||
case "max_positions":
|
||
parsed, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return fmt.Errorf("最大持仓需要是整数")
|
||
}
|
||
cfg.RiskControl.MaxPositions = parsed
|
||
case "source_type":
|
||
cfg.CoinSource.SourceType = value
|
||
case "static_coins":
|
||
cfg.CoinSource.StaticCoins = cleanStringList(strings.Split(value, ","))
|
||
case "excluded_coins":
|
||
cfg.CoinSource.ExcludedCoins = cleanStringList(strings.Split(value, ","))
|
||
case "use_ai500":
|
||
cfg.CoinSource.UseAI500 = value == "true"
|
||
case "ai500_limit":
|
||
parsed, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return fmt.Errorf("AI500 数量需要是整数")
|
||
}
|
||
cfg.CoinSource.AI500Limit = parsed
|
||
case "use_oi_top":
|
||
cfg.CoinSource.UseOITop = value == "true"
|
||
case "oi_top_limit":
|
||
parsed, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return fmt.Errorf("OI Top 数量需要是整数")
|
||
}
|
||
cfg.CoinSource.OITopLimit = parsed
|
||
case "use_oi_low":
|
||
cfg.CoinSource.UseOILow = value == "true"
|
||
case "oi_low_limit":
|
||
parsed, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return fmt.Errorf("OI Low 数量需要是整数")
|
||
}
|
||
cfg.CoinSource.OILowLimit = parsed
|
||
case "min_confidence":
|
||
parsed, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return fmt.Errorf("最小置信度需要是整数")
|
||
}
|
||
cfg.RiskControl.MinConfidence = parsed
|
||
case "min_risk_reward_ratio":
|
||
parsed, err := strconv.ParseFloat(value, 64)
|
||
if err != nil {
|
||
return fmt.Errorf("最小盈亏比需要是数字")
|
||
}
|
||
cfg.RiskControl.MinRiskRewardRatio = parsed
|
||
case "leverage":
|
||
parsed, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return fmt.Errorf("杠杆需要是整数")
|
||
}
|
||
cfg.RiskControl.BTCETHMaxLeverage = parsed
|
||
cfg.RiskControl.AltcoinMaxLeverage = parsed
|
||
case "btceth_max_leverage":
|
||
parsed, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return fmt.Errorf("BTC/ETH 最大杠杆需要是整数")
|
||
}
|
||
cfg.RiskControl.BTCETHMaxLeverage = parsed
|
||
case "altcoin_max_leverage":
|
||
parsed, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return fmt.Errorf("山寨币最大杠杆需要是整数")
|
||
}
|
||
cfg.RiskControl.AltcoinMaxLeverage = parsed
|
||
case "btceth_max_position_value_ratio":
|
||
parsed, err := strconv.ParseFloat(value, 64)
|
||
if err != nil {
|
||
return fmt.Errorf("BTC/ETH 仓位价值倍数需要是数字")
|
||
}
|
||
cfg.RiskControl.BTCETHMaxPositionValueRatio = parsed
|
||
case "altcoin_max_position_value_ratio":
|
||
parsed, err := strconv.ParseFloat(value, 64)
|
||
if err != nil {
|
||
return fmt.Errorf("山寨币仓位价值倍数需要是数字")
|
||
}
|
||
cfg.RiskControl.AltcoinMaxPositionValueRatio = parsed
|
||
case "max_margin_usage":
|
||
parsed, err := strconv.ParseFloat(value, 64)
|
||
if err != nil {
|
||
return fmt.Errorf("最大保证金使用率需要是数字")
|
||
}
|
||
cfg.RiskControl.MaxMarginUsage = parsed
|
||
case "min_position_size":
|
||
parsed, err := strconv.ParseFloat(value, 64)
|
||
if err != nil {
|
||
return fmt.Errorf("最小开仓金额需要是数字")
|
||
}
|
||
cfg.RiskControl.MinPositionSize = parsed
|
||
case "primary_timeframe":
|
||
cfg.Indicators.Klines.PrimaryTimeframe = value
|
||
case "primary_count":
|
||
parsed, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return fmt.Errorf("K线数量需要是整数")
|
||
}
|
||
cfg.Indicators.Klines.PrimaryCount = parsed
|
||
case "selected_timeframes":
|
||
tfs := strings.Split(value, ",")
|
||
cfg.Indicators.Klines.SelectedTimeframes = tfs
|
||
cfg.Indicators.Klines.EnableMultiTimeframe = len(tfs) > 1
|
||
case "ema_periods":
|
||
cfg.Indicators.EMAPeriods = parseCSVIntegers(value)
|
||
case "rsi_periods":
|
||
cfg.Indicators.RSIPeriods = parseCSVIntegers(value)
|
||
case "atr_periods":
|
||
cfg.Indicators.ATRPeriods = parseCSVIntegers(value)
|
||
case "boll_periods":
|
||
cfg.Indicators.BOLLPeriods = parseCSVIntegers(value)
|
||
case "enable_ema":
|
||
cfg.Indicators.EnableEMA = value == "true"
|
||
case "enable_macd":
|
||
cfg.Indicators.EnableMACD = value == "true"
|
||
case "enable_rsi":
|
||
cfg.Indicators.EnableRSI = value == "true"
|
||
case "enable_atr":
|
||
cfg.Indicators.EnableATR = value == "true"
|
||
case "enable_boll":
|
||
cfg.Indicators.EnableBOLL = value == "true"
|
||
case "enable_volume":
|
||
cfg.Indicators.EnableVolume = value == "true"
|
||
case "enable_oi":
|
||
cfg.Indicators.EnableOI = value == "true"
|
||
case "enable_funding_rate":
|
||
cfg.Indicators.EnableFundingRate = value == "true"
|
||
case "nofxos_api_key":
|
||
cfg.Indicators.NofxOSAPIKey = value
|
||
case "enable_quant_data":
|
||
cfg.Indicators.EnableQuantData = value == "true"
|
||
case "enable_quant_oi":
|
||
cfg.Indicators.EnableQuantOI = value == "true"
|
||
case "enable_quant_netflow":
|
||
cfg.Indicators.EnableQuantNetflow = value == "true"
|
||
case "enable_oi_ranking":
|
||
cfg.Indicators.EnableOIRanking = value == "true"
|
||
case "oi_ranking_duration":
|
||
cfg.Indicators.OIRankingDuration = value
|
||
case "oi_ranking_limit":
|
||
parsed, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return fmt.Errorf("OI 排行数量需要是整数")
|
||
}
|
||
cfg.Indicators.OIRankingLimit = parsed
|
||
case "enable_netflow_ranking":
|
||
cfg.Indicators.EnableNetFlowRanking = value == "true"
|
||
case "netflow_ranking_duration":
|
||
cfg.Indicators.NetFlowRankingDuration = value
|
||
case "netflow_ranking_limit":
|
||
parsed, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return fmt.Errorf("资金流排行数量需要是整数")
|
||
}
|
||
cfg.Indicators.NetFlowRankingLimit = parsed
|
||
case "enable_price_ranking":
|
||
cfg.Indicators.EnablePriceRanking = value == "true"
|
||
case "price_ranking_duration":
|
||
cfg.Indicators.PriceRankingDuration = value
|
||
case "price_ranking_limit":
|
||
parsed, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return fmt.Errorf("涨跌幅排行数量需要是整数")
|
||
}
|
||
cfg.Indicators.PriceRankingLimit = parsed
|
||
case "role_definition":
|
||
cfg.PromptSections.RoleDefinition = value
|
||
case "trading_frequency":
|
||
cfg.PromptSections.TradingFrequency = value
|
||
case "entry_standards":
|
||
cfg.PromptSections.EntryStandards = value
|
||
case "decision_process":
|
||
cfg.PromptSections.DecisionProcess = value
|
||
case "custom_prompt":
|
||
cfg.CustomPrompt = value
|
||
default:
|
||
return fmt.Errorf("unsupported strategy config field: %s", field)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func extractLabeledFloat(text string, labels []string) (float64, bool) {
|
||
lower := strings.ToLower(text)
|
||
for _, label := range labels {
|
||
idx := strings.Index(lower, strings.ToLower(label))
|
||
if idx < 0 {
|
||
continue
|
||
}
|
||
sub := text[idx+len(label):]
|
||
if value, ok := parseStandaloneFloat(sub); ok {
|
||
return value, true
|
||
}
|
||
}
|
||
return 0, false
|
||
}
|
||
|
||
func parseSourceTypeValue(text string) string {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
switch {
|
||
case containsAny(lower, []string{"静态", "固定", "static"}):
|
||
return "static"
|
||
case containsAny(lower, []string{"ai500"}):
|
||
return "ai500"
|
||
case containsAny(lower, []string{"oi top"}):
|
||
return "oi_top"
|
||
case containsAny(lower, []string{"oi low"}):
|
||
return "oi_low"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func extractSymbolList(text string, labels []string) []string {
|
||
segment := extractLongSegmentAfterKeywords(text, labels)
|
||
if segment == "" {
|
||
return nil
|
||
}
|
||
parts := strings.FieldsFunc(segment, func(r rune) bool {
|
||
return r == ',' || r == ',' || r == '、' || r == ' ' || r == '\n' || r == '\t'
|
||
})
|
||
out := make([]string, 0, len(parts))
|
||
for _, part := range parts {
|
||
if !looksLikeCoinSymbol(part) {
|
||
continue
|
||
}
|
||
part = normalizeCoinSymbol(part)
|
||
if part == "" {
|
||
continue
|
||
}
|
||
out = append(out, part)
|
||
}
|
||
return cleanStringList(out)
|
||
}
|
||
|
||
func looksLikeCoinSymbol(value string) bool {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return false
|
||
}
|
||
value = strings.Trim(value, `"'“”‘’()[]{}<>`)
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return false
|
||
}
|
||
return coinSymbolTokenRE.MatchString(value)
|
||
}
|
||
|
||
func normalizeCoinSymbol(symbol string) string {
|
||
symbol = strings.TrimSpace(strings.ToUpper(symbol))
|
||
if symbol == "" {
|
||
return ""
|
||
}
|
||
if strings.HasPrefix(symbol, "XYZ:") {
|
||
return symbol
|
||
}
|
||
if strings.HasSuffix(symbol, "USDT") || strings.HasSuffix(symbol, "USD") || strings.HasSuffix(symbol, "-USDC") {
|
||
return symbol
|
||
}
|
||
return symbol + "USDT"
|
||
}
|
||
|
||
func extractIntegerList(text string) []string {
|
||
matches := firstIntegerPattern.FindAllString(text, -1)
|
||
if len(matches) == 0 {
|
||
return nil
|
||
}
|
||
return matches
|
||
}
|
||
|
||
func parseCSVIntegers(value string) []int {
|
||
parts := strings.Split(value, ",")
|
||
out := make([]int, 0, len(parts))
|
||
for _, part := range parts {
|
||
part = strings.TrimSpace(part)
|
||
if part == "" {
|
||
continue
|
||
}
|
||
n, err := strconv.Atoi(part)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
out = append(out, n)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func extractDurationValue(text string) string {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
switch {
|
||
case strings.Contains(lower, "1h,4h,24h"):
|
||
return "1h,4h,24h"
|
||
case strings.Contains(lower, "24h"):
|
||
return "24h"
|
||
case strings.Contains(lower, "4h"):
|
||
return "4h"
|
||
case strings.Contains(lower, "1h"):
|
||
return "1h"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func parseStrategyTypeValue(text string) string {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
switch {
|
||
case containsAny(lower, []string{"grid", "网格"}):
|
||
return "grid_trading"
|
||
case containsAny(lower, []string{"ai trading", "ai策略", "普通策略"}):
|
||
return "ai_trading"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func extractLongSegmentAfterKeywords(text string, keywords []string) string {
|
||
trimmed := strings.TrimSpace(text)
|
||
if trimmed == "" {
|
||
return ""
|
||
}
|
||
lower := strings.ToLower(trimmed)
|
||
for _, keyword := range keywords {
|
||
idx := strings.Index(lower, strings.ToLower(keyword))
|
||
if idx < 0 {
|
||
continue
|
||
}
|
||
segment := strings.TrimSpace(trimmed[idx+len(keyword):])
|
||
segment = strings.TrimLeft(segment, "“”\"':: ")
|
||
for _, prefix := range []string{"改成", "改为", "设为", "设置为", "变成"} {
|
||
segment = strings.TrimSpace(strings.TrimPrefix(segment, prefix))
|
||
}
|
||
for _, marker := range []string{"排除币", "excluded coins", "exclude coins", "ai500", "oi top", "oi low", "并且", "然后"} {
|
||
if cut := strings.Index(strings.ToLower(segment), marker); cut > 0 {
|
||
segment = strings.TrimSpace(segment[:cut])
|
||
break
|
||
}
|
||
}
|
||
segment = strings.Trim(segment, "“”\"':: ")
|
||
if segment != "" {
|
||
return segment
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func extractDelimitedSegmentAfterKeywords(text string, keywords []string) string {
|
||
segment := extractLongSegmentAfterKeywords(text, keywords)
|
||
if segment == "" {
|
||
return ""
|
||
}
|
||
for _, marker := range []string{",", ",", "。", ".", ";", ";", "\n", "\t", "并且", "然后"} {
|
||
if cut := strings.Index(segment, marker); cut > 0 {
|
||
segment = strings.TrimSpace(segment[:cut])
|
||
break
|
||
}
|
||
}
|
||
return strings.Trim(segment, "“”\"':: ")
|
||
}
|
||
|
||
func extractModelNameValue(text string) string {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if !containsAny(lower, []string{"模型名", "模型名称", "model name"}) {
|
||
return ""
|
||
}
|
||
if value := extractDelimitedSegmentAfterKeywords(text, []string{"model name", "模型名称", "模型名"}); value != "" {
|
||
return value
|
||
}
|
||
if containsAny(lower, []string{"改成", "改为"}) {
|
||
if value := extractDelimitedSegmentAfterKeywords(text, []string{"改成", "改为"}); value != "" {
|
||
return value
|
||
}
|
||
}
|
||
if value := extractQuotedContent(text); value != "" {
|
||
return value
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func sanitizeExtractedURL(raw string) string {
|
||
raw = strings.TrimSpace(raw)
|
||
if raw == "" {
|
||
return ""
|
||
}
|
||
for _, marker := range []string{",", ",", "。", ";", ";", "并且", "然后"} {
|
||
if cut := strings.Index(raw, marker); cut > 0 {
|
||
raw = strings.TrimSpace(raw[:cut])
|
||
break
|
||
}
|
||
}
|
||
return raw
|
||
}
|
||
|
||
func strategyFieldKeywords(field string) []string {
|
||
switch field {
|
||
case "source_type":
|
||
return []string{"来源类型", "source type", "选币来源", "静态来源", "ai500来源", "oi top来源", "oi low来源"}
|
||
case "strategy_type":
|
||
return []string{"策略类型", "strategy type", "网格策略", "grid strategy", "ai策略"}
|
||
case "symbol":
|
||
return []string{"交易对", "symbol", "币对"}
|
||
case "grid_count":
|
||
return []string{"网格数量", "grid count", "grid levels"}
|
||
case "total_investment":
|
||
return []string{"总投入", "总投资", "total investment"}
|
||
case "upper_price":
|
||
return []string{"上沿价格", "上限价格", "upper price"}
|
||
case "lower_price":
|
||
return []string{"下沿价格", "下限价格", "lower price"}
|
||
case "use_atr_bounds":
|
||
return []string{"atr自动边界", "atr边界", "use atr bounds"}
|
||
case "atr_multiplier":
|
||
return []string{"atr倍数", "atr multiplier"}
|
||
case "distribution":
|
||
return []string{"分布方式", "distribution", "均匀分布", "高斯分布", "金字塔分布"}
|
||
case "enable_direction_adjust":
|
||
return []string{"方向调整", "direction adjust"}
|
||
case "direction_bias_ratio":
|
||
return []string{"方向偏置", "bias ratio", "direction bias"}
|
||
case "max_drawdown_pct":
|
||
return []string{"最大回撤", "max drawdown"}
|
||
case "stop_loss_pct":
|
||
return []string{"止损比例", "stop loss"}
|
||
case "daily_loss_limit_pct":
|
||
return []string{"日亏损限制", "daily loss limit"}
|
||
case "use_maker_only":
|
||
return []string{"maker only", "只挂maker", "仅maker"}
|
||
case "description":
|
||
return []string{"描述", "description"}
|
||
case "is_public":
|
||
return []string{"发布到市场", "公开", "publish"}
|
||
case "config_visible":
|
||
return []string{"配置可见", "显示配置", "config visible"}
|
||
case "nofxos_api_key":
|
||
return []string{"nofxos api key", "nofxos key", "api key"}
|
||
case "role_definition":
|
||
return []string{"角色定义", "role definition"}
|
||
case "trading_frequency":
|
||
return []string{"交易频率", "trading frequency"}
|
||
case "entry_standards":
|
||
return []string{"开仓标准", "入场标准", "entry standards"}
|
||
case "decision_process":
|
||
return []string{"决策流程", "decision process"}
|
||
case "custom_prompt":
|
||
return []string{"自定义prompt", "custom prompt", "提示词"}
|
||
case "ema_periods":
|
||
return []string{"ema周期", "ema periods"}
|
||
case "rsi_periods":
|
||
return []string{"rsi周期", "rsi periods"}
|
||
case "atr_periods":
|
||
return []string{"atr周期", "atr periods"}
|
||
case "boll_periods":
|
||
return []string{"boll周期", "布林周期", "boll periods"}
|
||
case "oi_ranking_duration":
|
||
return []string{"oi ranking duration", "oi排行周期"}
|
||
case "netflow_ranking_duration":
|
||
return []string{"netflow ranking duration", "资金流排行周期"}
|
||
case "price_ranking_duration":
|
||
return []string{"price ranking duration", "涨跌幅排行周期"}
|
||
case "oi_ranking_limit":
|
||
return []string{"oi ranking limit", "oi排行数量"}
|
||
case "netflow_ranking_limit":
|
||
return []string{"netflow ranking limit", "资金流排行数量"}
|
||
case "price_ranking_limit":
|
||
return []string{"price ranking limit", "涨跌幅排行数量"}
|
||
case "btceth_max_position_value_ratio":
|
||
return []string{"btc/eth仓位价值倍数", "btc eth position value", "主流币仓位价值倍数"}
|
||
case "altcoin_max_position_value_ratio":
|
||
return []string{"山寨币仓位价值倍数", "altcoin position value"}
|
||
case "max_margin_usage":
|
||
return []string{"最大保证金使用率", "max margin usage"}
|
||
case "min_position_size":
|
||
return []string{"最小开仓金额", "min position size"}
|
||
default:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func matchesStrategyFieldKeywords(text, field string) bool {
|
||
keywords := strategyFieldKeywords(field)
|
||
if len(keywords) == 0 {
|
||
return true
|
||
}
|
||
return containsAny(strings.ToLower(text), keywords)
|
||
}
|
||
|
||
func strategyFieldExplicitlyMentioned(text, field string) bool {
|
||
keywords := strategyFieldKeywords(field)
|
||
if len(keywords) == 0 {
|
||
switch field {
|
||
case "max_positions":
|
||
keywords = []string{"最大持仓", "最多持仓", "max positions"}
|
||
case "symbol":
|
||
keywords = []string{"交易对", "symbol", "币对"}
|
||
case "grid_count":
|
||
keywords = []string{"网格数量", "grid count", "grid levels"}
|
||
case "total_investment":
|
||
keywords = []string{"总投入", "总投资", "total investment"}
|
||
case "upper_price":
|
||
keywords = []string{"上沿价格", "上限价格", "upper price"}
|
||
case "lower_price":
|
||
keywords = []string{"下沿价格", "下限价格", "lower price"}
|
||
case "use_atr_bounds":
|
||
keywords = []string{"atr自动边界", "atr边界", "use atr bounds"}
|
||
case "atr_multiplier":
|
||
keywords = []string{"atr倍数", "atr multiplier"}
|
||
case "distribution":
|
||
keywords = []string{"分布方式", "distribution", "均匀分布", "高斯分布", "金字塔分布"}
|
||
case "enable_direction_adjust":
|
||
keywords = []string{"方向调整", "direction adjust"}
|
||
case "direction_bias_ratio":
|
||
keywords = []string{"方向偏置", "bias ratio", "direction bias"}
|
||
case "max_drawdown_pct":
|
||
keywords = []string{"最大回撤", "max drawdown"}
|
||
case "stop_loss_pct":
|
||
keywords = []string{"止损比例", "stop loss"}
|
||
case "daily_loss_limit_pct":
|
||
keywords = []string{"日亏损限制", "daily loss limit"}
|
||
case "use_maker_only":
|
||
keywords = []string{"maker only", "只挂maker", "仅maker"}
|
||
case "min_confidence":
|
||
keywords = []string{"最低置信度", "最小置信度", "min confidence"}
|
||
case "min_risk_reward_ratio":
|
||
keywords = []string{"最小盈亏比", "风险回报比", "risk reward", "risk/reward"}
|
||
case "leverage":
|
||
keywords = []string{"杠杆", "leverage"}
|
||
case "btceth_max_leverage":
|
||
keywords = []string{"btc/eth杠杆", "btc eth杠杆", "btc/eth leverage", "btc eth leverage", "主流币杠杆"}
|
||
case "altcoin_max_leverage":
|
||
keywords = []string{"山寨币杠杆", "altcoin leverage", "alts leverage"}
|
||
case "btceth_max_position_value_ratio":
|
||
keywords = []string{"btc/eth仓位价值倍数", "btc eth position value", "主流币仓位价值倍数"}
|
||
case "altcoin_max_position_value_ratio":
|
||
keywords = []string{"山寨币仓位价值倍数", "altcoin position value"}
|
||
case "max_margin_usage":
|
||
keywords = []string{"最大保证金使用率", "max margin usage"}
|
||
case "min_position_size":
|
||
keywords = []string{"最小开仓金额", "min position size"}
|
||
case "primary_timeframe":
|
||
keywords = []string{"主周期", "主时间周期", "primary timeframe"}
|
||
case "primary_count":
|
||
keywords = []string{"k线数量", "k线根数", "primary count", "kline count"}
|
||
case "selected_timeframes":
|
||
keywords = []string{"多周期", "时间框架", "timeframes", "selected timeframes"}
|
||
case "enable_ema":
|
||
keywords = []string{"ema"}
|
||
case "enable_macd":
|
||
keywords = []string{"macd"}
|
||
case "enable_rsi":
|
||
keywords = []string{"rsi"}
|
||
case "enable_atr":
|
||
keywords = []string{"atr"}
|
||
case "enable_boll":
|
||
keywords = []string{"boll", "bollinger", "布林"}
|
||
case "enable_volume":
|
||
keywords = []string{"成交量", "volume"}
|
||
case "enable_oi":
|
||
keywords = []string{"持仓量", "open interest", "oi"}
|
||
case "enable_funding_rate":
|
||
keywords = []string{"资金费率", "funding rate"}
|
||
case "source_type":
|
||
keywords = []string{"来源类型", "source type", "选币来源"}
|
||
case "static_coins":
|
||
keywords = []string{"静态币", "固定币", "static coins", "static symbols"}
|
||
case "excluded_coins":
|
||
keywords = []string{"排除币", "排除币种", "excluded coins", "exclude coins"}
|
||
case "use_ai500":
|
||
keywords = []string{"ai500"}
|
||
case "ai500_limit":
|
||
keywords = []string{"ai500 limit", "ai500数量", "ai500上限"}
|
||
case "use_oi_top":
|
||
keywords = []string{"oi top", "持仓量增长", "持仓量排行上涨"}
|
||
case "oi_top_limit":
|
||
keywords = []string{"oi top limit", "oi top数量", "oi top上限"}
|
||
case "use_oi_low":
|
||
keywords = []string{"oi low", "持仓量下降", "持仓量排行下跌"}
|
||
case "oi_low_limit":
|
||
keywords = []string{"oi low limit", "oi low数量", "oi low上限"}
|
||
case "enable_all_core_indicators":
|
||
keywords = []string{"核心指标"}
|
||
}
|
||
}
|
||
if len(keywords) == 0 {
|
||
return false
|
||
}
|
||
return containsAny(strings.ToLower(text), keywords)
|
||
}
|
||
|
||
func (a *Agent) executeTraderManagementAction(storeUserID string, userID int64, lang, text string, session skillSession) string {
|
||
if session.Action == "query_strategy_binding" || session.Action == "query_exchange_binding" || session.Action == "query_model_binding" {
|
||
if detail, ok := a.describeTrader(storeUserID, lang, session.TargetRef); ok {
|
||
return detail
|
||
}
|
||
return formatReadFastPathResponse(lang, "list_traders", a.toolListTraders(storeUserID))
|
||
}
|
||
switch session.Action {
|
||
case "query", "query_list":
|
||
return formatReadFastPathResponse(lang, "list_traders", a.toolListTraders(storeUserID))
|
||
case "query_detail":
|
||
if detail, ok := a.describeTrader(storeUserID, lang, session.TargetRef); ok {
|
||
return detail
|
||
}
|
||
return formatReadFastPathResponse(lang, "list_traders", a.toolListTraders(storeUserID))
|
||
case "start", "stop", "delete":
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "await_confirmation")
|
||
}
|
||
if msg, waiting := a.beginConfirmationIfNeeded(userID, lang, &session, defaultIfEmpty(session.TargetRef.Name, session.TargetRef.ID)); waiting {
|
||
a.saveSkillSession(userID, session)
|
||
return msg
|
||
}
|
||
if msg, waiting := awaitingConfirmationButNotApproved(lang, session, text); waiting {
|
||
a.saveSkillSession(userID, session)
|
||
return msg
|
||
}
|
||
var resp string
|
||
switch session.Action {
|
||
case "start":
|
||
setSkillDAGStep(&session, "execute_start")
|
||
resp = a.toolStartTrader(storeUserID, session.TargetRef.ID)
|
||
case "stop":
|
||
setSkillDAGStep(&session, "execute_stop")
|
||
resp = a.toolStopTrader(storeUserID, session.TargetRef.ID)
|
||
case "delete":
|
||
setSkillDAGStep(&session, "execute_delete")
|
||
resp = a.toolDeleteTrader(storeUserID, session.TargetRef.ID)
|
||
}
|
||
a.clearSkillSession(userID)
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
if lang == "zh" {
|
||
return "执行失败:" + errMsg
|
||
}
|
||
return "Action failed: " + errMsg
|
||
}
|
||
if lang == "zh" {
|
||
return fmt.Sprintf("已完成交易员操作:%s。", session.Action)
|
||
}
|
||
return fmt.Sprintf("Completed trader action: %s.", session.Action)
|
||
case "update", "update_name", "update_bindings", "configure_strategy", "configure_exchange", "configure_model":
|
||
if session.Action == "update_bindings" || session.Action == "configure_strategy" || session.Action == "configure_exchange" || session.Action == "configure_model" {
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "collect_bindings")
|
||
}
|
||
args := manageTraderArgs{
|
||
Action: "update",
|
||
TraderID: session.TargetRef.ID,
|
||
AIModelID: fieldValue(session, "ai_model_id"),
|
||
ExchangeID: fieldValue(session, "exchange_id"),
|
||
StrategyID: fieldValue(session, "strategy_id"),
|
||
}
|
||
if args.AIModelID != "" {
|
||
setField(&session, "ai_model_id", args.AIModelID)
|
||
}
|
||
if args.ExchangeID != "" {
|
||
setField(&session, "exchange_id", args.ExchangeID)
|
||
}
|
||
if args.StrategyID != "" {
|
||
setField(&session, "strategy_id", args.StrategyID)
|
||
}
|
||
selectedField := fieldValue(session, "update_field")
|
||
if selectedField == "" {
|
||
switch session.Action {
|
||
case "configure_strategy":
|
||
selectedField = "strategy_id"
|
||
case "configure_exchange":
|
||
selectedField = "exchange_id"
|
||
case "configure_model":
|
||
selectedField = "ai_model_id"
|
||
default:
|
||
if args.AIModelID == "" && args.ExchangeID == "" && args.StrategyID == "" {
|
||
selectedField = detectCatalogField(text, traderFieldCatalog)
|
||
}
|
||
}
|
||
if selectedField == "name" || selectedField == "initial_balance" || selectedField == "scan_interval_minutes" || selectedField == "is_cross_margin" || selectedField == "show_in_competition" {
|
||
selectedField = ""
|
||
}
|
||
if selectedField != "" {
|
||
setField(&session, "update_field", selectedField)
|
||
}
|
||
}
|
||
if args.AIModelID == "" && args.ExchangeID == "" && args.StrategyID == "" {
|
||
if fieldValue(session, "inline_sub_intent") == "create_sub_resource" {
|
||
delete(session.Fields, "inline_sub_intent")
|
||
a.saveSkillSession(userID, session)
|
||
task := a.buildSuspendedTask(userID, lang)
|
||
if task.Kind != "" && task.SkillSession != nil {
|
||
task.ResumeOnSuccess = true
|
||
var childSkill, childResumeTrigger string
|
||
switch session.Action {
|
||
case "configure_strategy":
|
||
childSkill = "strategy_management"
|
||
childResumeTrigger = "strategy_management"
|
||
case "configure_exchange":
|
||
childSkill = "exchange_management"
|
||
childResumeTrigger = "exchange_management"
|
||
case "configure_model":
|
||
childSkill = "model_management"
|
||
childResumeTrigger = "model_management"
|
||
case "create":
|
||
// infer child skill from which binding slot is missing
|
||
slots := session.Slots
|
||
if slots == nil || slots.StrategyID == "" {
|
||
childSkill = "strategy_management"
|
||
childResumeTrigger = "strategy_management"
|
||
} else if slots.ExchangeID == "" {
|
||
childSkill = "exchange_management"
|
||
childResumeTrigger = "exchange_management"
|
||
} else if slots.ModelID == "" {
|
||
childSkill = "model_management"
|
||
childResumeTrigger = "model_management"
|
||
}
|
||
}
|
||
if childSkill != "" {
|
||
task.ResumeTriggers = []string{childResumeTrigger}
|
||
a.SnapshotManager(userID).Save(task)
|
||
a.clearSkillSession(userID)
|
||
child := skillSession{Name: childSkill, Action: "create", Phase: "collecting"}
|
||
var answer string
|
||
var handled bool
|
||
switch childSkill {
|
||
case "strategy_management":
|
||
answer, handled = a.handleStrategyManagementSkill(storeUserID, userID, lang, text, child)
|
||
case "exchange_management":
|
||
answer, handled = a.handleExchangeManagementSkill(storeUserID, userID, lang, text, child)
|
||
case "model_management":
|
||
answer, handled = a.handleModelManagementSkill(storeUserID, userID, lang, text, child)
|
||
}
|
||
if !handled {
|
||
answer = ""
|
||
}
|
||
return a.maybeResumeParentTaskAfterSuccessfulSkill(storeUserID, userID, lang, childSkill, "create", answer)
|
||
}
|
||
}
|
||
}
|
||
if fieldValue(session, "inline_sub_intent") == "edit_sub_resource" {
|
||
delete(session.Fields, "inline_sub_intent")
|
||
a.saveSkillSession(userID, session)
|
||
task := a.buildSuspendedTask(userID, lang)
|
||
if task.Kind != "" && task.SkillSession != nil {
|
||
task.ResumeOnSuccess = true
|
||
var childSkill string
|
||
switch session.Action {
|
||
case "configure_strategy":
|
||
childSkill = "strategy_management"
|
||
case "configure_exchange":
|
||
childSkill = "exchange_management"
|
||
case "configure_model":
|
||
childSkill = "model_management"
|
||
case "create", "update_bindings":
|
||
childSkill = detectCatalogDomainFromText(text)
|
||
}
|
||
if childSkill != "" {
|
||
task.ResumeTriggers = []string{childSkill}
|
||
a.SnapshotManager(userID).Save(task)
|
||
a.clearSkillSession(userID)
|
||
child := skillSession{Name: childSkill, Action: "update", Phase: "collecting"}
|
||
var answer string
|
||
var handled bool
|
||
switch childSkill {
|
||
case "strategy_management":
|
||
answer, handled = a.handleStrategyManagementSkill(storeUserID, userID, lang, text, child)
|
||
case "exchange_management":
|
||
answer, handled = a.handleExchangeManagementSkill(storeUserID, userID, lang, text, child)
|
||
case "model_management":
|
||
answer, handled = a.handleModelManagementSkill(storeUserID, userID, lang, text, child)
|
||
}
|
||
if !handled {
|
||
answer = ""
|
||
}
|
||
return a.maybeResumeParentTaskAfterSuccessfulSkill(storeUserID, userID, lang, childSkill, "update", answer)
|
||
}
|
||
}
|
||
}
|
||
setSkillDAGStep(&session, "collect_bindings")
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
if selectedField != "" {
|
||
return fmt.Sprintf("还差一步:请告诉我你想换成哪个%s。", displayCatalogFieldName(selectedField, lang))
|
||
}
|
||
switch session.Action {
|
||
case "configure_strategy":
|
||
return "好,我来帮你换策略。直接告诉我想用哪个策略就行。"
|
||
case "configure_exchange":
|
||
return "好,我来帮你换交易所。直接告诉我想用哪个交易所就行。"
|
||
case "configure_model":
|
||
return "好,我来帮你换模型。直接告诉我想用哪个模型就行。"
|
||
default:
|
||
return "好,我来帮你调整交易员绑定。你直接告诉我想换成哪个模型、交易所或策略就行。"
|
||
}
|
||
}
|
||
if selectedField != "" {
|
||
return fmt.Sprintf("One more thing: tell me which %s you want to use.", displayCatalogFieldName(selectedField, lang))
|
||
}
|
||
switch session.Action {
|
||
case "configure_strategy":
|
||
return "Sure. Tell me which strategy you want to use."
|
||
case "configure_exchange":
|
||
return "Sure. Tell me which exchange you want to use."
|
||
case "configure_model":
|
||
return "Sure. Tell me which model you want to use."
|
||
default:
|
||
return "Sure. Tell me which model, exchange, or strategy you want to switch to."
|
||
}
|
||
}
|
||
setSkillDAGStep(&session, "execute_update")
|
||
resp := a.toolUpdateTrader(storeUserID, args)
|
||
a.clearSkillSession(userID)
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
if lang == "zh" {
|
||
return "这次没改成功:" + errMsg
|
||
}
|
||
return "That change did not go through: " + errMsg
|
||
}
|
||
a.rememberReferencesFromToolResult(userID, "manage_trader", resp)
|
||
if lang == "zh" {
|
||
switch session.Action {
|
||
case "configure_strategy":
|
||
return "已更新交易员策略。"
|
||
case "configure_exchange":
|
||
return "已更新交易员交易所。"
|
||
case "configure_model":
|
||
return "已更新交易员模型。"
|
||
default:
|
||
return "已更新交易员绑定。"
|
||
}
|
||
}
|
||
switch session.Action {
|
||
case "configure_strategy":
|
||
return "Updated the trader strategy."
|
||
case "configure_exchange":
|
||
return "Updated the trader exchange."
|
||
case "configure_model":
|
||
return "Updated the trader model."
|
||
default:
|
||
return "Updated trader bindings."
|
||
}
|
||
}
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "collect_name")
|
||
}
|
||
parsedArgs := buildTraderUpdateArgsFromSession(session)
|
||
if !parsedArgs.hasAny() {
|
||
parsedArgs = buildTraderUpdateArgs(a, storeUserID, text)
|
||
}
|
||
selectedField := fieldValue(session, "update_field")
|
||
selectedFieldJustChosen := false
|
||
if selectedField == "" {
|
||
if session.Action == "update_name" {
|
||
selectedField = "name"
|
||
} else if !parsedArgs.hasAny() {
|
||
selectedField = detectCatalogField(text, traderFieldCatalog)
|
||
}
|
||
if selectedField != "" {
|
||
setField(&session, "update_field", selectedField)
|
||
selectedFieldJustChosen = true
|
||
}
|
||
}
|
||
if !parsedArgs.hasAny() && selectedField != "" && !(selectedFieldJustChosen && looksLikeBareFieldSelection(text, traderFieldKeywords(selectedField))) {
|
||
if value, ok := a.parseTraderFieldValue(storeUserID, text, selectedField); ok {
|
||
switch selectedField {
|
||
case "ai_model_id":
|
||
parsedArgs.AIModelID = value
|
||
case "exchange_id":
|
||
parsedArgs.ExchangeID = value
|
||
case "strategy_id":
|
||
parsedArgs.StrategyID = value
|
||
case "initial_balance":
|
||
if parsed, err := strconv.ParseFloat(value, 64); err == nil {
|
||
parsedArgs.InitialBalance = &parsed
|
||
}
|
||
case "scan_interval_minutes":
|
||
if parsed, err := strconv.Atoi(value); err == nil {
|
||
parsedArgs.ScanIntervalMinutes = &parsed
|
||
}
|
||
case "is_cross_margin":
|
||
parsed := value == "true"
|
||
parsedArgs.IsCrossMargin = &parsed
|
||
case "show_in_competition":
|
||
parsed := value == "true"
|
||
parsedArgs.ShowInCompetition = &parsed
|
||
case "btc_eth_leverage":
|
||
if parsed, err := strconv.Atoi(value); err == nil {
|
||
parsedArgs.BTCETHLeverage = &parsed
|
||
}
|
||
case "altcoin_leverage":
|
||
if parsed, err := strconv.Atoi(value); err == nil {
|
||
parsedArgs.AltcoinLeverage = &parsed
|
||
}
|
||
case "trading_symbols":
|
||
parsedArgs.TradingSymbols = value
|
||
case "custom_prompt":
|
||
parsedArgs.CustomPrompt = value
|
||
case "override_base_prompt":
|
||
parsed := value == "true"
|
||
parsedArgs.OverrideBasePrompt = &parsed
|
||
case "system_prompt_template":
|
||
parsedArgs.SystemPromptTemplate = value
|
||
case "use_ai500":
|
||
parsed := value == "true"
|
||
parsedArgs.UseAI500 = &parsed
|
||
case "use_oi_top":
|
||
parsed := value == "true"
|
||
parsedArgs.UseOITop = &parsed
|
||
}
|
||
if selectedField == "name" {
|
||
setField(&session, "name", value)
|
||
}
|
||
}
|
||
}
|
||
applyTraderUpdateArgsToSession(&session, parsedArgs)
|
||
parsedArgs = mergeTraderUpdateArgs(buildTraderUpdateArgsFromSession(session), parsedArgs)
|
||
if parsedArgs.hasAny() {
|
||
normalizedArgs, warnings := normalizeTraderArgsToManualLimits(lang, parsedArgs)
|
||
applyTraderUpdateArgsToSession(&session, normalizedArgs)
|
||
args := manageTraderArgs{
|
||
Action: "update",
|
||
TraderID: session.TargetRef.ID,
|
||
AIModelID: normalizedArgs.AIModelID,
|
||
ExchangeID: normalizedArgs.ExchangeID,
|
||
StrategyID: normalizedArgs.StrategyID,
|
||
InitialBalance: normalizedArgs.InitialBalance,
|
||
ScanIntervalMinutes: normalizedArgs.ScanIntervalMinutes,
|
||
IsCrossMargin: normalizedArgs.IsCrossMargin,
|
||
ShowInCompetition: normalizedArgs.ShowInCompetition,
|
||
BTCETHLeverage: normalizedArgs.BTCETHLeverage,
|
||
AltcoinLeverage: normalizedArgs.AltcoinLeverage,
|
||
TradingSymbols: normalizedArgs.TradingSymbols,
|
||
CustomPrompt: normalizedArgs.CustomPrompt,
|
||
OverrideBasePrompt: normalizedArgs.OverrideBasePrompt,
|
||
SystemPromptTemplate: normalizedArgs.SystemPromptTemplate,
|
||
UseAI500: normalizedArgs.UseAI500,
|
||
UseOITop: normalizedArgs.UseOITop,
|
||
}
|
||
setSkillDAGStep(&session, "execute_update")
|
||
resp := a.toolUpdateTrader(storeUserID, args)
|
||
a.clearSkillSession(userID)
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
if lang == "zh" {
|
||
return "这次没改成功:" + errMsg
|
||
}
|
||
return "That change did not go through: " + errMsg
|
||
}
|
||
if lang == "zh" {
|
||
reply := "已更新交易员配置。"
|
||
if len(warnings) > 0 {
|
||
reply += "\n\n已按手动面板范围自动调整:\n- " + strings.Join(warnings, "\n- ")
|
||
}
|
||
return reply
|
||
}
|
||
reply := "Updated trader config."
|
||
if len(warnings) > 0 {
|
||
reply += "\n\nAdjusted to stay within the manual editor limits:\n- " + strings.Join(warnings, "\n- ")
|
||
}
|
||
return reply
|
||
}
|
||
newName := ""
|
||
if newName != "" {
|
||
setField(&session, "name", newName)
|
||
}
|
||
newName = fieldValue(session, "name")
|
||
if newName == "" {
|
||
if selectedField != "" {
|
||
setSkillDAGStep(&session, "collect_field_value")
|
||
} else {
|
||
setSkillDAGStep(&session, "collect_name")
|
||
}
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
if selectedField != "" {
|
||
if selectedField == "ai_model_id" || selectedField == "exchange_id" || selectedField == "strategy_id" {
|
||
return fmt.Sprintf("还差一步:请告诉我你想换成哪个%s。", displayCatalogFieldName(selectedField, lang))
|
||
}
|
||
return fmt.Sprintf("还差一步:请告诉我新的%s。", displayCatalogFieldName(selectedField, lang))
|
||
}
|
||
return "你可以直接告诉我想改哪一项,比如名称、扫描频率、初始资金、杠杆,或者绑定的模型、交易所、策略。"
|
||
}
|
||
if selectedField != "" {
|
||
if selectedField == "ai_model_id" || selectedField == "exchange_id" || selectedField == "strategy_id" {
|
||
return fmt.Sprintf("One more thing: tell me which %s you want to use.", displayCatalogFieldName(selectedField, lang))
|
||
}
|
||
return fmt.Sprintf("One more thing: tell me the new %s.", displayCatalogFieldName(selectedField, lang))
|
||
}
|
||
return "Tell me what you want to change first, for example the name, scan interval, balance, leverage, or the linked model, exchange, or strategy."
|
||
}
|
||
args := manageTraderArgs{Action: "update", TraderID: session.TargetRef.ID, Name: newName}
|
||
setSkillDAGStep(&session, "execute_update")
|
||
resp := a.toolUpdateTrader(storeUserID, args)
|
||
a.clearSkillSession(userID)
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
if lang == "zh" {
|
||
return "这次没改成功:" + errMsg
|
||
}
|
||
return "That change did not go through: " + errMsg
|
||
}
|
||
if lang == "zh" {
|
||
return fmt.Sprintf("已将交易员改名为“%s”。", newName)
|
||
}
|
||
return fmt.Sprintf("Renamed trader to %q.", newName)
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func (a *Agent) executeExchangeManagementAction(storeUserID string, userID int64, lang, text string, session skillSession) string {
|
||
switch session.Action {
|
||
case "query_detail":
|
||
if detail, ok := a.describeExchange(storeUserID, lang, session.TargetRef); ok {
|
||
return detail
|
||
}
|
||
return formatReadFastPathResponse(lang, "get_exchange_configs", a.toolGetExchangeConfigs(storeUserID))
|
||
case "delete":
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "await_confirmation")
|
||
}
|
||
if msg, waiting := a.beginConfirmationIfNeeded(userID, lang, &session, defaultIfEmpty(session.TargetRef.Name, session.TargetRef.ID)); waiting {
|
||
a.saveSkillSession(userID, session)
|
||
return msg
|
||
}
|
||
if msg, waiting := awaitingConfirmationButNotApproved(lang, session, text); waiting {
|
||
a.saveSkillSession(userID, session)
|
||
return msg
|
||
}
|
||
setSkillDAGStep(&session, "execute_delete")
|
||
args, _ := json.Marshal(map[string]any{"action": "delete", "exchange_id": session.TargetRef.ID})
|
||
resp := a.toolManageExchangeConfig(storeUserID, string(args))
|
||
a.clearSkillSession(userID)
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
if lang == "zh" {
|
||
return "这次没删成功:" + errMsg
|
||
}
|
||
return "That delete did not go through: " + errMsg
|
||
}
|
||
if lang == "zh" {
|
||
return a.maybeResumeParentTaskAfterSuccessfulSkill(storeUserID, userID, lang, "exchange_management", "delete", "已删除交易所配置。")
|
||
}
|
||
return a.maybeResumeParentTaskAfterSuccessfulSkill(storeUserID, userID, lang, "exchange_management", "delete", "Deleted exchange config.")
|
||
case "update", "update_name", "update_status":
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
if session.Action == "update_status" {
|
||
setSkillDAGStep(&session, "collect_enabled")
|
||
} else {
|
||
setSkillDAGStep(&session, "collect_account_name")
|
||
}
|
||
}
|
||
patch := buildExchangeUpdatePatchFromSession(session)
|
||
selectedField := fieldValue(session, "update_field")
|
||
if selectedField == "" && session.Action == "update_status" {
|
||
selectedField = "enabled"
|
||
setField(&session, "update_field", selectedField)
|
||
}
|
||
applyExchangeUpdatePatchToSession(&session, patch)
|
||
patch = mergeExchangeUpdatePatch(buildExchangeUpdatePatchFromSession(session), patch)
|
||
patch, warnings := normalizeExchangePatchToManualLimits(lang, patch)
|
||
applyExchangeUpdatePatchToSession(&session, patch)
|
||
payload := map[string]any{"action": "update", "exchange_id": session.TargetRef.ID}
|
||
accountName := defaultIfEmpty(patch.AccountName, fieldValue(session, "account_name"))
|
||
if accountName != "" && session.Action != "update_status" {
|
||
payload["account_name"] = accountName
|
||
}
|
||
enabledRaw := fieldValue(session, "enabled")
|
||
if patch.Enabled != nil {
|
||
enabledRaw = strconv.FormatBool(*patch.Enabled)
|
||
}
|
||
if enabledRaw != "" {
|
||
payload["enabled"] = enabledRaw == "true"
|
||
}
|
||
if value := defaultIfEmpty(patch.APIKey, fieldValue(session, "api_key")); value != "" {
|
||
payload["api_key"] = value
|
||
}
|
||
if value := defaultIfEmpty(patch.SecretKey, fieldValue(session, "secret_key")); value != "" {
|
||
payload["secret_key"] = value
|
||
}
|
||
if value := defaultIfEmpty(patch.Passphrase, fieldValue(session, "passphrase")); value != "" {
|
||
payload["passphrase"] = value
|
||
}
|
||
testnetRaw := fieldValue(session, "testnet")
|
||
if patch.Testnet != nil {
|
||
testnetRaw = strconv.FormatBool(*patch.Testnet)
|
||
}
|
||
if value := testnetRaw; value != "" {
|
||
payload["testnet"] = value == "true"
|
||
}
|
||
if value := defaultIfEmpty(patch.HyperliquidWalletAddr, fieldValue(session, "hyperliquid_wallet_addr")); value != "" {
|
||
payload["hyperliquid_wallet_addr"] = value
|
||
}
|
||
if value := defaultIfEmpty(patch.AsterUser, fieldValue(session, "aster_user")); value != "" {
|
||
payload["aster_user"] = value
|
||
}
|
||
if value := defaultIfEmpty(patch.AsterSigner, fieldValue(session, "aster_signer")); value != "" {
|
||
payload["aster_signer"] = value
|
||
}
|
||
if value := defaultIfEmpty(patch.AsterPrivateKey, fieldValue(session, "aster_private_key")); value != "" {
|
||
payload["aster_private_key"] = value
|
||
}
|
||
if value := defaultIfEmpty(patch.LighterWalletAddr, fieldValue(session, "lighter_wallet_addr")); value != "" {
|
||
payload["lighter_wallet_addr"] = value
|
||
}
|
||
if value := defaultIfEmpty(patch.LighterAPIKeyPrivateKey, fieldValue(session, "lighter_api_key_private_key")); value != "" {
|
||
payload["lighter_api_key_private_key"] = value
|
||
}
|
||
if patch.LighterAPIKeyIndex != nil {
|
||
payload["lighter_api_key_index"] = *patch.LighterAPIKeyIndex
|
||
} else if value := fieldValue(session, "lighter_api_key_index"); value != "" {
|
||
if parsed, err := strconv.Atoi(value); err == nil {
|
||
payload["lighter_api_key_index"] = parsed
|
||
}
|
||
}
|
||
if session.Action == "update_status" {
|
||
delete(payload, "account_name")
|
||
}
|
||
if len(payload) == 2 {
|
||
if session.Action == "update_status" {
|
||
setSkillDAGStep(&session, "collect_enabled")
|
||
} else {
|
||
if selectedField != "" {
|
||
setSkillDAGStep(&session, "collect_field_value")
|
||
} else {
|
||
setSkillDAGStep(&session, "collect_account_name")
|
||
}
|
||
}
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
if selectedField != "" {
|
||
return fmt.Sprintf("还差一步:请告诉我你想把交易所配置里的%s改成什么。", displayCatalogFieldName(selectedField, lang))
|
||
}
|
||
return "你可以直接告诉我想改交易所配置里的哪一项,比如账户名、启用开关、API Key、Passphrase、钱包地址或 testnet。"
|
||
}
|
||
if selectedField != "" {
|
||
return fmt.Sprintf("One more thing: tell me what you want to change the exchange config %s to.", displayCatalogFieldName(selectedField, lang))
|
||
}
|
||
return "Tell me which exchange config field you want to change, for example the account name, enabled switch, API key, passphrase, wallet address, or testnet."
|
||
}
|
||
if err := a.validateExchangeDraft(
|
||
storeUserID,
|
||
session.TargetRef.ID,
|
||
"",
|
||
payload["enabled"] == true,
|
||
asString(payload["api_key"]),
|
||
asString(payload["secret_key"]),
|
||
asString(payload["passphrase"]),
|
||
asString(payload["hyperliquid_wallet_addr"]),
|
||
asString(payload["aster_user"]),
|
||
asString(payload["aster_signer"]),
|
||
asString(payload["aster_private_key"]),
|
||
asString(payload["lighter_wallet_addr"]),
|
||
asString(payload["lighter_api_key_private_key"]),
|
||
); err != nil {
|
||
a.saveSkillSession(userID, session)
|
||
return formatValidationFeedback(lang, "exchange", err)
|
||
}
|
||
setSkillDAGStep(&session, "execute_update")
|
||
raw, _ := json.Marshal(payload)
|
||
resp := a.toolManageExchangeConfig(storeUserID, string(raw))
|
||
a.clearSkillSession(userID)
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
if lang == "zh" {
|
||
return "这次没改成功:" + errMsg
|
||
}
|
||
return "That change did not go through: " + errMsg
|
||
}
|
||
a.rememberReferencesFromToolResult(userID, "manage_exchange_config", resp)
|
||
if lang == "zh" {
|
||
reply := "已更新交易所配置。"
|
||
if len(warnings) > 0 {
|
||
reply += "\n\n已按手动面板范围自动调整:\n- " + strings.Join(warnings, "\n- ")
|
||
}
|
||
return a.maybeResumeParentTaskAfterSuccessfulSkill(storeUserID, userID, lang, "exchange_management", "update", reply)
|
||
}
|
||
reply := "Updated exchange config."
|
||
if len(warnings) > 0 {
|
||
reply += "\n\nAdjusted to stay within the manual editor limits:\n- " + strings.Join(warnings, "\n- ")
|
||
}
|
||
return a.maybeResumeParentTaskAfterSuccessfulSkill(storeUserID, userID, lang, "exchange_management", "update", reply)
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func (a *Agent) executeModelManagementAction(storeUserID string, userID int64, lang, text string, session skillSession) string {
|
||
switch session.Action {
|
||
case "query_detail":
|
||
if detail, ok := a.describeModel(storeUserID, lang, session.TargetRef); ok {
|
||
return detail
|
||
}
|
||
return formatReadFastPathResponse(lang, "get_model_configs", a.toolGetModelConfigs(storeUserID))
|
||
case "delete":
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "await_confirmation")
|
||
}
|
||
if msg, waiting := a.beginConfirmationIfNeeded(userID, lang, &session, defaultIfEmpty(session.TargetRef.Name, session.TargetRef.ID)); waiting {
|
||
a.saveSkillSession(userID, session)
|
||
return msg
|
||
}
|
||
if msg, waiting := awaitingConfirmationButNotApproved(lang, session, text); waiting {
|
||
a.saveSkillSession(userID, session)
|
||
return msg
|
||
}
|
||
setSkillDAGStep(&session, "execute_delete")
|
||
raw, _ := json.Marshal(map[string]any{"action": "delete", "model_id": session.TargetRef.ID})
|
||
resp := a.toolManageModelConfig(storeUserID, string(raw))
|
||
a.clearSkillSession(userID)
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
if lang == "zh" {
|
||
return "这次没删成功:" + errMsg
|
||
}
|
||
return "That delete did not go through: " + errMsg
|
||
}
|
||
if lang == "zh" {
|
||
return "已删除模型配置。"
|
||
}
|
||
return "Deleted model config."
|
||
case "update", "update_name", "update_endpoint", "update_status":
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
switch session.Action {
|
||
case "update_status":
|
||
setSkillDAGStep(&session, "collect_enabled")
|
||
case "update_endpoint":
|
||
setSkillDAGStep(&session, "collect_custom_api_url")
|
||
default:
|
||
setSkillDAGStep(&session, "collect_custom_model_name")
|
||
}
|
||
}
|
||
payload := map[string]any{"action": "update", "model_id": session.TargetRef.ID}
|
||
patch := buildModelUpdatePatchFromSession(session)
|
||
selectedField := fieldValue(session, "update_field")
|
||
if selectedField == "" {
|
||
switch session.Action {
|
||
case "update_status":
|
||
selectedField = "enabled"
|
||
case "update_endpoint":
|
||
selectedField = "custom_api_url"
|
||
}
|
||
if selectedField != "" {
|
||
setField(&session, "update_field", selectedField)
|
||
}
|
||
}
|
||
applyModelUpdatePatchToSession(&session, patch)
|
||
patch = mergeModelUpdatePatch(buildModelUpdatePatchFromSession(session), patch)
|
||
urlValue := patch.CustomAPIURL
|
||
enabledValue := ""
|
||
if patch.Enabled != nil {
|
||
enabledValue = strconv.FormatBool(*patch.Enabled)
|
||
}
|
||
apiKeyValue := patch.APIKey
|
||
modelNameValue := patch.CustomModelName
|
||
if value := defaultIfEmpty(urlValue, fieldValue(session, "custom_api_url")); value != "" {
|
||
payload["custom_api_url"] = value
|
||
}
|
||
if value := defaultIfEmpty(enabledValue, fieldValue(session, "enabled")); value != "" {
|
||
payload["enabled"] = value == "true"
|
||
}
|
||
if value := defaultIfEmpty(apiKeyValue, fieldValue(session, "api_key")); value != "" {
|
||
payload["api_key"] = value
|
||
}
|
||
if value := defaultIfEmpty(modelNameValue, fieldValue(session, "custom_model_name")); value != "" {
|
||
payload["custom_model_name"] = value
|
||
}
|
||
if session.Action == "update_name" {
|
||
delete(payload, "custom_api_url")
|
||
delete(payload, "enabled")
|
||
delete(payload, "api_key")
|
||
}
|
||
if session.Action == "update_status" {
|
||
delete(payload, "custom_api_url")
|
||
delete(payload, "custom_model_name")
|
||
delete(payload, "api_key")
|
||
}
|
||
if session.Action == "update_endpoint" {
|
||
delete(payload, "custom_model_name")
|
||
delete(payload, "enabled")
|
||
delete(payload, "api_key")
|
||
}
|
||
if len(payload) == 2 {
|
||
switch session.Action {
|
||
case "update_status":
|
||
setSkillDAGStep(&session, "collect_enabled")
|
||
case "update_endpoint":
|
||
setSkillDAGStep(&session, "collect_custom_api_url")
|
||
default:
|
||
if selectedField != "" {
|
||
setSkillDAGStep(&session, "collect_field_value")
|
||
} else {
|
||
setSkillDAGStep(&session, "collect_custom_model_name")
|
||
}
|
||
}
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
if selectedField != "" {
|
||
return fmt.Sprintf("还差一步:请告诉我新的%s。", displayCatalogFieldName(selectedField, lang))
|
||
}
|
||
return "你可以直接告诉我想改哪一项,比如模型名称、接口地址,或者开关状态。"
|
||
}
|
||
if selectedField != "" {
|
||
return fmt.Sprintf("One more thing: tell me the new %s.", displayCatalogFieldName(selectedField, lang))
|
||
}
|
||
return "Tell me what you want to change, for example the model name, endpoint URL, or on or off status."
|
||
}
|
||
if err := a.validateModelDraft(
|
||
storeUserID,
|
||
session.TargetRef.ID,
|
||
"",
|
||
payload["enabled"] == true,
|
||
asString(payload["api_key"]),
|
||
asString(payload["custom_api_url"]),
|
||
asString(payload["custom_model_name"]),
|
||
); err != nil {
|
||
a.saveSkillSession(userID, session)
|
||
return formatValidationFeedback(lang, "model", err)
|
||
}
|
||
setSkillDAGStep(&session, "execute_update")
|
||
raw, _ := json.Marshal(payload)
|
||
resp := a.toolManageModelConfig(storeUserID, string(raw))
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
if strings.Contains(errMsg, "cannot enable model config before API key is configured") {
|
||
return "更新模型配置失败:这个模型还没有配置 API Key,暂时不能启用。你可以直接把 API Key 发给我,我帮你继续配置。"
|
||
}
|
||
return "这次没改成功:" + errMsg
|
||
}
|
||
a.saveSkillSession(userID, session)
|
||
return "That change did not go through: " + errMsg
|
||
}
|
||
a.clearSkillSession(userID)
|
||
a.rememberReferencesFromToolResult(userID, "manage_model_config", resp)
|
||
if lang == "zh" {
|
||
if session.Action == "update_status" {
|
||
return "已更新模型配置启用状态。"
|
||
}
|
||
return "已更新模型配置。"
|
||
}
|
||
return "Updated model config."
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func (a *Agent) executeStrategyManagementAction(storeUserID string, userID int64, lang, text string, session skillSession) string {
|
||
switch session.Action {
|
||
case "query", "query_list":
|
||
return formatReadFastPathResponse(lang, "get_strategies", a.toolGetStrategies(storeUserID))
|
||
case "query_detail":
|
||
if detail, ok := a.describeStrategy(storeUserID, lang, session.TargetRef); ok {
|
||
return detail
|
||
}
|
||
return formatReadFastPathResponse(lang, "get_strategies", a.toolGetStrategies(storeUserID))
|
||
case "activate":
|
||
raw, _ := json.Marshal(map[string]any{"action": "activate", "strategy_id": session.TargetRef.ID})
|
||
resp := a.toolManageStrategy(storeUserID, string(raw))
|
||
a.clearSkillSession(userID)
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
if lang == "zh" {
|
||
return "这次没激活成功:" + errMsg
|
||
}
|
||
return "That activation did not go through: " + errMsg
|
||
}
|
||
if lang == "zh" {
|
||
return "已激活策略。"
|
||
}
|
||
return "Activated strategy."
|
||
case "duplicate":
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "collect_name")
|
||
}
|
||
newName := fieldValue(session, "name")
|
||
if newName != "" {
|
||
setField(&session, "name", newName)
|
||
}
|
||
if newName == "" {
|
||
setSkillDAGStep(&session, "collect_name")
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "还差一步:请给这个新策略起个名字。"
|
||
}
|
||
return "One more thing: give the new strategy a name."
|
||
}
|
||
setSkillDAGStep(&session, "execute_duplicate")
|
||
raw, _ := json.Marshal(map[string]any{"action": "duplicate", "strategy_id": session.TargetRef.ID, "name": newName})
|
||
resp := a.toolManageStrategy(storeUserID, string(raw))
|
||
a.clearSkillSession(userID)
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
if lang == "zh" {
|
||
return "这次没复制成功:" + errMsg
|
||
}
|
||
return "That copy did not go through: " + errMsg
|
||
}
|
||
if lang == "zh" {
|
||
return fmt.Sprintf("已复制策略,新名称为“%s”。", newName)
|
||
}
|
||
return fmt.Sprintf("Duplicated strategy as %q.", newName)
|
||
case "delete":
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "await_confirmation")
|
||
}
|
||
if fieldValue(session, "bulk_scope") == "all" {
|
||
strategies, err := a.store.Strategy().List(storeUserID)
|
||
if err != nil {
|
||
if lang == "zh" {
|
||
return "我这边暂时没读到策略列表:" + err.Error()
|
||
}
|
||
return "I could not load the strategy list just now: " + err.Error()
|
||
}
|
||
|
||
deletable := make([]*store.Strategy, 0, len(strategies))
|
||
skippedDefault := 0
|
||
for _, strategy := range strategies {
|
||
if strategy == nil {
|
||
continue
|
||
}
|
||
if strategy.IsDefault {
|
||
skippedDefault++
|
||
continue
|
||
}
|
||
deletable = append(deletable, strategy)
|
||
}
|
||
if len(deletable) == 0 {
|
||
a.clearSkillSession(userID)
|
||
if lang == "zh" {
|
||
return "当前没有可删除的自定义策略。"
|
||
}
|
||
return "There are no user-created strategies to delete."
|
||
}
|
||
|
||
targetLabel := fmt.Sprintf("全部自定义策略(共 %d 个)", len(deletable))
|
||
if msg, waiting := a.beginConfirmationIfNeeded(userID, lang, &session, targetLabel); waiting {
|
||
a.saveSkillSession(userID, session)
|
||
return msg
|
||
}
|
||
if msg, waiting := awaitingConfirmationButNotApproved(lang, session, text); waiting {
|
||
a.saveSkillSession(userID, session)
|
||
return msg
|
||
}
|
||
|
||
setSkillDAGStep(&session, "execute_delete")
|
||
deletedNames := make([]string, 0, len(deletable))
|
||
failedNames := make([]string, 0)
|
||
for _, strategy := range deletable {
|
||
raw, _ := json.Marshal(map[string]any{"action": "delete", "strategy_id": strategy.ID})
|
||
resp := a.toolManageStrategy(storeUserID, string(raw))
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
failedNames = append(failedNames, fmt.Sprintf("%s(%s)", strategy.Name, errMsg))
|
||
continue
|
||
}
|
||
deletedNames = append(deletedNames, strategy.Name)
|
||
}
|
||
a.clearSkillSession(userID)
|
||
|
||
if lang == "zh" {
|
||
parts := []string{fmt.Sprintf("批量删除策略已完成:成功删除 %d 个。", len(deletedNames))}
|
||
if skippedDefault > 0 {
|
||
parts = append(parts, fmt.Sprintf("已跳过系统默认策略 %d 个。", skippedDefault))
|
||
}
|
||
if len(failedNames) > 0 {
|
||
parts = append(parts, "这些没删成功:"+strings.Join(failedNames, ";"))
|
||
}
|
||
if len(deletedNames) > 0 {
|
||
parts = append(parts, "已删除:"+strings.Join(deletedNames, "、"))
|
||
}
|
||
return strings.Join(parts, "\n")
|
||
}
|
||
|
||
parts := []string{fmt.Sprintf("Bulk strategy deletion finished: deleted %d strategy(s).", len(deletedNames))}
|
||
if skippedDefault > 0 {
|
||
parts = append(parts, fmt.Sprintf("Skipped %d default strategy(ies).", skippedDefault))
|
||
}
|
||
if len(failedNames) > 0 {
|
||
parts = append(parts, "These did not delete successfully: "+strings.Join(failedNames, "; "))
|
||
}
|
||
if len(deletedNames) > 0 {
|
||
parts = append(parts, "Deleted: "+strings.Join(deletedNames, ", "))
|
||
}
|
||
return strings.Join(parts, "\n")
|
||
}
|
||
if msg, waiting := a.beginConfirmationIfNeeded(userID, lang, &session, defaultIfEmpty(session.TargetRef.Name, session.TargetRef.ID)); waiting {
|
||
a.saveSkillSession(userID, session)
|
||
return msg
|
||
}
|
||
if msg, waiting := awaitingConfirmationButNotApproved(lang, session, text); waiting {
|
||
a.saveSkillSession(userID, session)
|
||
return msg
|
||
}
|
||
setSkillDAGStep(&session, "execute_delete")
|
||
raw, _ := json.Marshal(map[string]any{"action": "delete", "strategy_id": session.TargetRef.ID})
|
||
resp := a.toolManageStrategy(storeUserID, string(raw))
|
||
a.clearSkillSession(userID)
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
if lang == "zh" {
|
||
return "这次没删成功:" + errMsg
|
||
}
|
||
return "That delete did not go through: " + errMsg
|
||
}
|
||
if lang == "zh" {
|
||
return "已删除策略。"
|
||
}
|
||
return "Deleted strategy."
|
||
case "update", "update_name", "update_config", "update_prompt":
|
||
if session.Action == "update_prompt" {
|
||
return a.executeStrategyPromptUpdate(storeUserID, userID, lang, text, session)
|
||
}
|
||
if session.Action == "update_config" ||
|
||
fieldValue(session, strategyPendingUpdateConfigField) != "" ||
|
||
fieldValue(session, "config_field") != "" ||
|
||
fieldValue(session, "config_value") != "" {
|
||
return a.executeStrategyConfigUpdate(storeUserID, userID, lang, text, session)
|
||
}
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "collect_name")
|
||
}
|
||
newName := fieldValue(session, "name")
|
||
if newName != "" {
|
||
setField(&session, "name", newName)
|
||
}
|
||
if newName == "" {
|
||
setSkillDAGStep(&session, "collect_name")
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "目前这里先支持改策略名称。你直接把新名字发给我就行。"
|
||
}
|
||
return "For now, this step supports renaming the strategy. Just send me the new name."
|
||
}
|
||
setSkillDAGStep(&session, "execute_update")
|
||
raw, _ := json.Marshal(map[string]any{"action": "update", "strategy_id": session.TargetRef.ID, "name": newName})
|
||
resp := a.toolManageStrategy(storeUserID, string(raw))
|
||
a.clearSkillSession(userID)
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
if lang == "zh" {
|
||
return "这次没改成功:" + errMsg
|
||
}
|
||
return "That change did not go through: " + errMsg
|
||
}
|
||
if lang == "zh" {
|
||
return fmt.Sprintf("已将策略改名为“%s”。", newName)
|
||
}
|
||
return fmt.Sprintf("Renamed strategy to %q.", newName)
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func (a *Agent) executeStrategyPromptUpdate(storeUserID string, userID int64, lang, text string, session skillSession) string {
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "collect_prompt")
|
||
}
|
||
strategy, cfg, err := a.loadStrategyConfigForUpdate(storeUserID, session.TargetRef.ID)
|
||
if err != nil {
|
||
if lang == "zh" {
|
||
return "我这边暂时没读到这份策略:" + err.Error()
|
||
}
|
||
return "I could not load that strategy just now: " + err.Error()
|
||
}
|
||
|
||
prompt := fieldValue(session, "prompt")
|
||
if prompt == "" {
|
||
prompt = fieldValue(session, "custom_prompt")
|
||
if prompt != "" {
|
||
setField(&session, "prompt", prompt)
|
||
}
|
||
}
|
||
if generatedDraftRequiresConfirmation(session) {
|
||
switch {
|
||
case createConfirmationReply(text):
|
||
clearGeneratedDraftConfirmation(&session)
|
||
case isNoReply(text):
|
||
clearGeneratedDraftConfirmation(&session, "prompt", "custom_prompt")
|
||
setSkillDAGStep(&session, "collect_prompt")
|
||
session.Phase = "collecting"
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "好,我先不用这版草稿。你可以告诉我想保留的风格,或者直接让我重新设计一版 prompt。"
|
||
}
|
||
return "Okay, I won't use that draft. Tell me the style you want to keep, or ask me to draft another prompt."
|
||
}
|
||
}
|
||
if prompt == "" {
|
||
prompt = extractQuotedContent(text)
|
||
if prompt != "" {
|
||
setField(&session, "prompt", prompt)
|
||
}
|
||
}
|
||
if prompt == "" {
|
||
setSkillDAGStep(&session, "collect_prompt")
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "还差一步:请把新的提示词内容发给我,直接发正文就行。"
|
||
}
|
||
return "One more thing: send me the new prompt text."
|
||
}
|
||
|
||
cfg.CustomPrompt = prompt
|
||
setSkillDAGStep(&session, "execute_update")
|
||
return a.persistStrategyConfigUpdate(storeUserID, userID, lang, strategy, cfg, "已更新策略 prompt。", "Updated strategy prompt.")
|
||
}
|
||
|
||
func (a *Agent) executeStrategyConfigUpdate(storeUserID string, userID int64, lang, text string, session skillSession) string {
|
||
if rawPending := fieldValue(session, strategyPendingUpdateConfigField); rawPending != "" {
|
||
if createConfirmationReply(text) {
|
||
var pendingCfg store.StrategyConfig
|
||
if err := json.Unmarshal([]byte(rawPending), &pendingCfg); err != nil {
|
||
if session.Fields != nil {
|
||
delete(session.Fields, strategyPendingUpdateConfigField)
|
||
delete(session.Fields, strategyPendingUpdateWarnings)
|
||
delete(session.Fields, strategyPendingUpdateZhMsg)
|
||
delete(session.Fields, strategyPendingUpdateEnMsg)
|
||
}
|
||
session.Phase = "collecting"
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "我这边暂时没读到刚才那版草稿。你再告诉我想改哪一项,我马上继续。"
|
||
}
|
||
return "I could not read that draft just now. Tell me what you want to change and I will continue."
|
||
}
|
||
zhMsg := defaultIfEmpty(fieldValue(session, strategyPendingUpdateZhMsg), "已更新策略参数。")
|
||
enMsg := defaultIfEmpty(fieldValue(session, strategyPendingUpdateEnMsg), "Updated strategy config.")
|
||
return a.persistPendingStrategyConfigUpdate(storeUserID, userID, lang, session, pendingCfg, zhMsg, enMsg)
|
||
}
|
||
if session.Fields != nil {
|
||
delete(session.Fields, strategyPendingUpdateConfigField)
|
||
delete(session.Fields, strategyPendingUpdateWarnings)
|
||
delete(session.Fields, strategyPendingUpdateZhMsg)
|
||
delete(session.Fields, strategyPendingUpdateEnMsg)
|
||
}
|
||
session.Phase = "collecting"
|
||
}
|
||
|
||
if _, ok := getSkillDAG("strategy_management", "update_config"); ok {
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "resolve_config_field")
|
||
}
|
||
}
|
||
|
||
currentStep, _ := currentSkillDAGStep(session)
|
||
strategy, cfg, err := a.loadStrategyConfigForUpdate(storeUserID, session.TargetRef.ID)
|
||
if err != nil {
|
||
if lang == "zh" {
|
||
return "我这边暂时没读到这份策略:" + err.Error()
|
||
}
|
||
return "I could not load that strategy just now: " + err.Error()
|
||
}
|
||
|
||
if generatedDraftRequiresConfirmation(session) && fieldValue(session, "config_field") == "" && fieldValue(session, "config_value") == "" {
|
||
if generated := fieldValue(session, "custom_prompt"); generated != "" {
|
||
setField(&session, "config_field", "custom_prompt")
|
||
setField(&session, "config_value", generated)
|
||
}
|
||
}
|
||
if generatedDraftRequiresConfirmation(session) {
|
||
switch {
|
||
case createConfirmationReply(text):
|
||
clearGeneratedDraftConfirmation(&session)
|
||
case isNoReply(text):
|
||
clearGeneratedDraftConfirmation(&session, "config_field", "config_value", "custom_prompt")
|
||
setSkillDAGStep(&session, "resolve_config_field")
|
||
session.Phase = "collecting"
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "好,我先不用这版草稿。你可以直接告诉我要改哪个配置,或者继续让我重新设计一版。"
|
||
}
|
||
return "Okay, I won't use that draft. Tell me which config to change, or ask me to draft another version."
|
||
}
|
||
}
|
||
|
||
if fieldValue(session, "config_field") == "" && fieldValue(session, "config_value") == "" {
|
||
patches := detectStrategyConfigPatches(text)
|
||
if len(patches) > 1 {
|
||
changed := make([]string, 0, len(patches))
|
||
for _, patch := range patches {
|
||
if err := applyStrategyConfigPatch(&cfg, patch.Field, patch.Value); err != nil {
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "这次没改成功:" + err.Error()
|
||
}
|
||
return "That change did not go through: " + err.Error()
|
||
}
|
||
switch patch.Field {
|
||
case "description":
|
||
strategy.Description = patch.Value
|
||
case "is_public":
|
||
strategy.IsPublic = patch.Value == "true"
|
||
case "config_visible":
|
||
strategy.ConfigVisible = patch.Value == "true"
|
||
}
|
||
changed = append(changed, strategyConfigFieldDisplayName(patch.Field, lang))
|
||
}
|
||
beforeClamp := cfg
|
||
cfg.ClampLimits()
|
||
setSkillDAGStep(&session, "apply_field_update")
|
||
msgZH := "已更新策略参数:" + strings.Join(changed, "、") + "。"
|
||
msgEN := "Updated strategy config fields: " + strings.Join(changed, ", ") + "."
|
||
if warnings := store.StrategyClampWarnings(beforeClamp, cfg, lang); len(warnings) > 0 {
|
||
return a.deferStrategyRiskControlledUpdate(userID, lang, &session, cfg, warnings, msgZH, msgEN)
|
||
}
|
||
setSkillDAGStep(&session, "execute_update")
|
||
return a.persistStrategyConfigUpdate(storeUserID, userID, lang, strategy, cfg, msgZH, msgEN)
|
||
}
|
||
}
|
||
|
||
field := fieldValue(session, "config_field")
|
||
if field == "" {
|
||
field = detectStrategyConfigField(text)
|
||
if field != "" {
|
||
setField(&session, "config_field", field)
|
||
if currentStep.ID == "resolve_config_field" {
|
||
advanceSkillDAGStep(&session, currentStep.ID)
|
||
currentStep, _ = currentSkillDAGStep(session)
|
||
}
|
||
}
|
||
}
|
||
if field == "" {
|
||
setSkillDAGStep(&session, "resolve_config_field")
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "你可以直接告诉我想改哪一项,比如币种来源、杠杆、时间周期、技术指标,或者提示词。"
|
||
}
|
||
return "Tell me what you want to change, for example coin source, leverage, timeframes, indicators, or the prompt."
|
||
}
|
||
|
||
if fieldValue(session, "config_value") == "" {
|
||
if value, ok := extractStrategyConfigValue(text, field); ok {
|
||
setField(&session, "config_value", value)
|
||
if currentStep.ID == "resolve_config_value" {
|
||
advanceSkillDAGStep(&session, currentStep.ID)
|
||
currentStep, _ = currentSkillDAGStep(session)
|
||
}
|
||
}
|
||
}
|
||
value := fieldValue(session, "config_value")
|
||
if value == "" {
|
||
setSkillDAGStep(&session, "resolve_config_value")
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return fmt.Sprintf("还差一步:请告诉我新的%s。", strategyConfigFieldDisplayName(field, lang))
|
||
}
|
||
return fmt.Sprintf("One more thing: tell me the new %s.", strategyConfigFieldDisplayName(field, lang))
|
||
}
|
||
|
||
if err := applyStrategyConfigPatch(&cfg, field, value); err != nil {
|
||
setSkillDAGStep(&session, "resolve_config_value")
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return err.Error()
|
||
}
|
||
return err.Error()
|
||
}
|
||
switch field {
|
||
case "description":
|
||
strategy.Description = value
|
||
case "is_public":
|
||
strategy.IsPublic = value == "true"
|
||
case "config_visible":
|
||
strategy.ConfigVisible = value == "true"
|
||
}
|
||
|
||
beforeClamp := cfg
|
||
cfg.ClampLimits()
|
||
changed := []string{field}
|
||
displayChanged := make([]string, 0, len(changed))
|
||
for _, item := range changed {
|
||
displayChanged = append(displayChanged, strategyConfigFieldDisplayName(item, lang))
|
||
}
|
||
msgZH := "已更新策略参数:" + strings.Join(displayChanged, "、") + "。"
|
||
msgEN := "Updated strategy config fields: " + strings.Join(displayChanged, ", ") + "."
|
||
setSkillDAGStep(&session, "apply_field_update")
|
||
if warnings := store.StrategyClampWarnings(beforeClamp, cfg, lang); len(warnings) > 0 {
|
||
return a.deferStrategyRiskControlledUpdate(userID, lang, &session, cfg, warnings, msgZH, msgEN)
|
||
}
|
||
setSkillDAGStep(&session, "execute_update")
|
||
return a.persistStrategyConfigUpdate(storeUserID, userID, lang, strategy, cfg, msgZH, msgEN)
|
||
}
|
||
|
||
func (a *Agent) loadStrategyConfigForUpdate(storeUserID, strategyID string) (*store.Strategy, store.StrategyConfig, error) {
|
||
strategy, err := a.store.Strategy().Get(storeUserID, strategyID)
|
||
if err != nil {
|
||
return nil, store.StrategyConfig{}, err
|
||
}
|
||
cfg := store.GetDefaultStrategyConfig("zh")
|
||
if strings.TrimSpace(strategy.Config) != "" {
|
||
_ = json.Unmarshal([]byte(strategy.Config), &cfg)
|
||
}
|
||
return strategy, cfg, nil
|
||
}
|
||
|
||
func (a *Agent) deferStrategyRiskControlledUpdate(userID int64, lang string, session *skillSession, cfg store.StrategyConfig, warnings []string, zhMsg, enMsg string) string {
|
||
rawConfig, _ := json.Marshal(cfg)
|
||
setField(session, strategyPendingUpdateConfigField, string(rawConfig))
|
||
setField(session, strategyPendingUpdateWarnings, marshalStringList(warnings))
|
||
setField(session, strategyPendingUpdateZhMsg, zhMsg)
|
||
setField(session, strategyPendingUpdateEnMsg, enMsg)
|
||
session.Phase = "await_confirmation"
|
||
setSkillDAGStep(session, "await_confirmation")
|
||
a.saveSkillSession(userID, *session)
|
||
task := SuspendedTask{
|
||
Kind: "skill_session",
|
||
SkillSession: func() *skillSession {
|
||
copy := normalizeSkillSession(*session)
|
||
return ©
|
||
}(),
|
||
ResumeHint: buildSkillResumeHint(lang, *session),
|
||
}
|
||
a.SnapshotManager(userID).Save(task)
|
||
return formatRiskControlAcceptancePrompt(lang, warnings, "确认应用")
|
||
}
|
||
|
||
func (a *Agent) persistPendingStrategyConfigUpdate(storeUserID string, userID int64, lang string, session skillSession, cfg store.StrategyConfig, zhMsg, enMsg string) string {
|
||
if session.Fields != nil {
|
||
delete(session.Fields, strategyPendingUpdateConfigField)
|
||
delete(session.Fields, strategyPendingUpdateWarnings)
|
||
delete(session.Fields, strategyPendingUpdateZhMsg)
|
||
delete(session.Fields, strategyPendingUpdateEnMsg)
|
||
}
|
||
strategy, _, err := a.loadStrategyConfigForUpdate(storeUserID, session.TargetRef.ID)
|
||
if err != nil {
|
||
if lang == "zh" {
|
||
return "我这边暂时没读到这份策略:" + err.Error()
|
||
}
|
||
return "I could not load that strategy just now: " + err.Error()
|
||
}
|
||
return a.persistStrategyConfigUpdate(storeUserID, userID, lang, strategy, cfg, zhMsg, enMsg)
|
||
}
|
||
|
||
func (a *Agent) persistStrategyConfigUpdate(storeUserID string, userID int64, lang string, strategy *store.Strategy, cfg store.StrategyConfig, zhMsg, enMsg string) string {
|
||
rawConfig, err := json.Marshal(cfg)
|
||
if err != nil {
|
||
if lang == "zh" {
|
||
return "我这边整理这份策略配置时出了点问题:" + err.Error()
|
||
}
|
||
return "I ran into a problem while preparing that strategy config: " + err.Error()
|
||
}
|
||
raw, _ := json.Marshal(map[string]any{
|
||
"action": "update",
|
||
"strategy_id": strategy.ID,
|
||
"name": strategy.Name,
|
||
"description": strategy.Description,
|
||
"is_public": strategy.IsPublic,
|
||
"config_visible": strategy.ConfigVisible,
|
||
"config": json.RawMessage(rawConfig),
|
||
})
|
||
resp := a.toolManageStrategy(storeUserID, string(raw))
|
||
a.clearSkillSession(userID)
|
||
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
|
||
if lang == "zh" {
|
||
return "这次没改成功:" + errMsg
|
||
}
|
||
return "That change did not go through: " + errMsg
|
||
}
|
||
if warnings := parseToolWarnings(resp); len(warnings) > 0 {
|
||
if lang == "zh" {
|
||
zhMsg += "\n\n已按安全范围自动调整:\n- " + strings.Join(warnings, "\n- ")
|
||
} else {
|
||
enMsg += "\n\nAdjusted to stay within safe limits:\n- " + strings.Join(warnings, "\n- ")
|
||
}
|
||
}
|
||
if lang == "zh" {
|
||
return zhMsg
|
||
}
|
||
return enMsg
|
||
}
|
||
|
||
func parseToolWarnings(raw string) []string {
|
||
var payload struct {
|
||
Warnings []string `json:"warnings"`
|
||
}
|
||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||
return nil
|
||
}
|
||
return payload.Warnings
|
||
}
|
||
|
||
func extractQuotedContent(text string) string {
|
||
if matches := quotedContentRE.FindStringSubmatch(text); len(matches) == 2 {
|
||
return strings.TrimSpace(matches[1])
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func extractLabeledInt(text string, labels []string) (int, bool) {
|
||
lower := strings.ToLower(text)
|
||
for _, label := range labels {
|
||
idx := strings.Index(lower, strings.ToLower(label))
|
||
if idx < 0 {
|
||
continue
|
||
}
|
||
segment := text[idx:]
|
||
if match := firstIntegerPattern.FindString(segment); match != "" {
|
||
if value, err := strconv.Atoi(match); err == nil {
|
||
return value, true
|
||
}
|
||
}
|
||
}
|
||
return 0, false
|
||
}
|
||
|
||
func extractTimeframeAfterKeywords(text string, labels []string) string {
|
||
lower := strings.ToLower(text)
|
||
for _, label := range labels {
|
||
idx := strings.Index(lower, strings.ToLower(label))
|
||
if idx < 0 {
|
||
continue
|
||
}
|
||
segment := text[idx:]
|
||
if match := timeframeTokenRE.FindString(segment); match != "" {
|
||
return strings.ToLower(match)
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func extractTimeframes(text string) []string {
|
||
matches := timeframeTokenRE.FindAllString(text, -1)
|
||
if len(matches) == 0 {
|
||
return nil
|
||
}
|
||
seen := make(map[string]struct{}, len(matches))
|
||
out := make([]string, 0, len(matches))
|
||
for _, match := range matches {
|
||
tf := strings.ToLower(strings.TrimSpace(match))
|
||
if tf == "" {
|
||
continue
|
||
}
|
||
if _, ok := seen[tf]; ok {
|
||
continue
|
||
}
|
||
seen[tf] = struct{}{}
|
||
out = append(out, tf)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func (a *Agent) handleTraderDiagnosisSkill(storeUserID, lang, text string) string {
|
||
raw := a.toolListTraders(storeUserID)
|
||
list := formatReadFastPathResponse(lang, "list_traders", raw)
|
||
if lang == "zh" {
|
||
reply := "现象:这是交易员运行诊断问题。\n优先排查:\n1. 交易员是否已创建并处于运行状态。\n2. 绑定的模型、交易所、策略是否齐全。\n3. 是“没有启动”、还是“启动了但 AI 没有下单”、还是“下单失败”。\n当前交易员概览:\n" + list
|
||
if excerpt := backendLogDiagnosisExcerpt(lang, text, "trader"); excerpt != "" {
|
||
reply += "\n" + excerpt
|
||
}
|
||
return reply
|
||
}
|
||
reply := "This looks like a trader diagnosis issue.\nCheck whether the trader exists, is running, and has model/exchange/strategy bindings.\nCurrent trader overview:\n" + list
|
||
if excerpt := backendLogDiagnosisExcerpt(lang, text, "trader"); excerpt != "" {
|
||
reply += "\n" + excerpt
|
||
}
|
||
return reply
|
||
}
|
||
|
||
func (a *Agent) handleStrategyDiagnosisSkill(storeUserID, lang, text string) string {
|
||
raw := a.toolGetStrategies(storeUserID)
|
||
list := formatReadFastPathResponse(lang, "get_strategies", raw)
|
||
if lang == "zh" {
|
||
reply := "现象:这是策略或提示词生效问题。\n优先排查:\n1. 你改的是策略模板,还是 trader 上的 custom prompt。\n2. 策略是否真的保存成功。\n3. 运行结果不符合预期,是配置问题还是市场条件问题。\n当前策略概览:\n" + list
|
||
if excerpt := backendLogDiagnosisExcerpt(lang, text, "strategy"); excerpt != "" {
|
||
reply += "\n" + excerpt
|
||
}
|
||
return reply
|
||
}
|
||
reply := "This looks like a strategy or prompt diagnosis issue.\nCheck whether you changed the strategy template or a trader-specific prompt override.\nCurrent strategy overview:\n" + list
|
||
if excerpt := backendLogDiagnosisExcerpt(lang, text, "strategy"); excerpt != "" {
|
||
reply += "\n" + excerpt
|
||
}
|
||
return reply
|
||
}
|