mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
3ca95b294d
* feat: integrate NOFXi agent into dev * Enhance NOFXi agent workflow and diagnostics
1223 lines
42 KiB
Go
1223 lines
42 KiB
Go
package agent
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"regexp"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"nofx/store"
|
||
)
|
||
|
||
var (
|
||
firstIntegerPattern = regexp.MustCompile(`\d+`)
|
||
timeframeTokenRE = regexp.MustCompile(`(?i)\b\d{1,2}[mhdw]\b`)
|
||
)
|
||
|
||
func parseStandaloneInteger(text string) (int, bool) {
|
||
match := firstIntegerPattern.FindString(strings.TrimSpace(text))
|
||
if match == "" {
|
||
return 0, false
|
||
}
|
||
value, err := strconv.Atoi(match)
|
||
if err != nil {
|
||
return 0, false
|
||
}
|
||
return value, true
|
||
}
|
||
|
||
func parseEnabledValue(text string) (bool, bool) {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
switch {
|
||
case containsAny(lower, []string{"启用", "打开", "开启", "enable", "enabled", "on"}):
|
||
return true, true
|
||
case containsAny(lower, []string{"禁用", "关闭", "停用", "disable", "disabled", "off"}):
|
||
return false, true
|
||
default:
|
||
return false, false
|
||
}
|
||
}
|
||
|
||
func parseFlagValue(text string, keywords []string) (bool, bool) {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if lower == "" || !containsAny(lower, keywords) {
|
||
return false, false
|
||
}
|
||
switch {
|
||
case containsAny(lower, []string{"启用", "打开", "开启", "使用", "用", "是", "true", "enable", "enabled", "on", "use"}):
|
||
return true, true
|
||
case containsAny(lower, []string{"禁用", "关闭", "停用", "不用", "不要", "否", "false", "disable", "disabled", "off", "don't use", "do not use"}):
|
||
return false, true
|
||
default:
|
||
return false, false
|
||
}
|
||
}
|
||
|
||
func extractCredentialValue(text string, keywords []string) string {
|
||
if value := extractQuotedContent(text); value != "" && containsAny(strings.ToLower(text), keywords) {
|
||
return value
|
||
}
|
||
return extractPostKeywordName(text, keywords)
|
||
}
|
||
|
||
func parseScanIntervalMinutes(text string) (int, bool) {
|
||
if value, ok := extractLabeledInt(text, []string{"扫描间隔", "扫描频率", "scan interval", "scan frequency"}); ok {
|
||
return value, true
|
||
}
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
if !containsAny(lower, []string{"扫描间隔", "扫描频率", "scan interval", "scan frequency"}) {
|
||
return 0, false
|
||
}
|
||
return parseStandaloneInteger(text)
|
||
}
|
||
|
||
func detectStrategyConfigField(text string) string {
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
switch {
|
||
case containsAny(lower, []string{"最大持仓", "最多持仓", "max positions"}):
|
||
return "max_positions"
|
||
case containsAny(lower, []string{"最低置信度", "最小置信度", "min confidence"}):
|
||
return "min_confidence"
|
||
case containsAny(lower, []string{"btc/eth杠杆", "btc eth杠杆", "btc eth leverage", "btc/eth leverage", "主流币杠杆"}):
|
||
return "btceth_max_leverage"
|
||
case containsAny(lower, []string{"山寨币杠杆", "altcoin leverage", "alts leverage"}):
|
||
return "altcoin_max_leverage"
|
||
case containsAny(lower, []string{"ema"}):
|
||
return "enable_ema"
|
||
case containsAny(lower, []string{"macd"}):
|
||
return "enable_macd"
|
||
case containsAny(lower, []string{"rsi"}):
|
||
return "enable_rsi"
|
||
case containsAny(lower, []string{"atr"}):
|
||
return "enable_atr"
|
||
case containsAny(lower, []string{"boll", "bollinger", "布林"}):
|
||
return "enable_boll"
|
||
case containsAny(lower, []string{"核心指标"}) && containsAny(lower, []string{"全选", "全部", "全开", "都开", "都启用", "全部启用"}):
|
||
return "enable_all_core_indicators"
|
||
case containsAny(lower, []string{"主周期", "主时间周期", "primary timeframe"}):
|
||
return "primary_timeframe"
|
||
case containsAny(lower, []string{"多周期", "时间框架", "timeframes", "selected timeframes"}):
|
||
return "selected_timeframes"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func strategyConfigFieldDisplayName(field, lang string) string {
|
||
switch field {
|
||
case "max_positions":
|
||
if lang == "zh" {
|
||
return "最大持仓"
|
||
}
|
||
return "max positions"
|
||
case "min_confidence":
|
||
if lang == "zh" {
|
||
return "最小置信度"
|
||
}
|
||
return "min confidence"
|
||
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 "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"
|
||
default:
|
||
return field
|
||
}
|
||
}
|
||
|
||
func extractStrategyConfigValue(text, field string) (string, bool) {
|
||
switch field {
|
||
case "max_positions":
|
||
if value, ok := extractLabeledInt(text, []string{"最大持仓", "最多持仓", "max positions"}); ok {
|
||
return strconv.Itoa(value), true
|
||
}
|
||
if value, ok := parseStandaloneInteger(text); ok {
|
||
return strconv.Itoa(value), true
|
||
}
|
||
case "min_confidence":
|
||
if value, ok := extractLabeledInt(text, []string{"最低置信度", "最小置信度", "min confidence"}); ok {
|
||
return strconv.Itoa(value), true
|
||
}
|
||
if value, ok := parseStandaloneInteger(text); ok {
|
||
return strconv.Itoa(value), true
|
||
}
|
||
case "btceth_max_leverage":
|
||
if value, ok := extractLabeledInt(text, []string{"btc/eth杠杆", "btc eth杠杆", "btc/eth leverage", "btc eth leverage", "主流币杠杆"}); ok {
|
||
return strconv.Itoa(value), true
|
||
}
|
||
if value, ok := parseStandaloneInteger(text); ok {
|
||
return strconv.Itoa(value), true
|
||
}
|
||
case "altcoin_max_leverage":
|
||
if value, ok := extractLabeledInt(text, []string{"山寨币杠杆", "altcoin leverage", "alts leverage"}); ok {
|
||
return strconv.Itoa(value), true
|
||
}
|
||
if value, ok := parseStandaloneInteger(text); ok {
|
||
return strconv.Itoa(value), true
|
||
}
|
||
case "enable_ema", "enable_macd", "enable_rsi", "enable_atr", "enable_boll":
|
||
if enabled, ok := parseEnabledValue(text); ok {
|
||
return strconv.FormatBool(enabled), true
|
||
}
|
||
case "enable_all_core_indicators":
|
||
lower := strings.ToLower(strings.TrimSpace(text))
|
||
switch {
|
||
case containsAny(lower, []string{"全选", "全部", "全开", "都开", "都启用", "全部启用"}):
|
||
return "true", true
|
||
case containsAny(lower, []string{"关闭", "停用", "禁用", "全部关闭", "全部禁用"}):
|
||
return "false", true
|
||
}
|
||
case "primary_timeframe":
|
||
if tf := extractTimeframeAfterKeywords(text, []string{"主周期", "主时间周期", "primary timeframe", "timeframe"}); tf != "" {
|
||
return tf, true
|
||
}
|
||
case "selected_timeframes":
|
||
if tfs := extractTimeframes(text); len(tfs) > 0 {
|
||
return strings.Join(tfs, ","), true
|
||
}
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
type strategyConfigPatch struct {
|
||
Field string
|
||
Value string
|
||
}
|
||
|
||
func detectStrategyConfigPatches(text string) []strategyConfigPatch {
|
||
seen := map[string]string{}
|
||
addPatch := func(field, value string) {
|
||
field = strings.TrimSpace(field)
|
||
value = strings.TrimSpace(value)
|
||
if field == "" || value == "" {
|
||
return
|
||
}
|
||
seen[field] = value
|
||
}
|
||
|
||
for _, field := range []string{
|
||
"max_positions",
|
||
"min_confidence",
|
||
"btceth_max_leverage",
|
||
"altcoin_max_leverage",
|
||
"primary_timeframe",
|
||
"selected_timeframes",
|
||
"enable_ema",
|
||
"enable_macd",
|
||
"enable_rsi",
|
||
"enable_atr",
|
||
"enable_boll",
|
||
"enable_all_core_indicators",
|
||
} {
|
||
if value, ok := extractStrategyConfigValue(text, field); ok {
|
||
if field == "enable_all_core_indicators" {
|
||
addPatch("enable_ema", value)
|
||
addPatch("enable_macd", value)
|
||
addPatch("enable_rsi", value)
|
||
addPatch("enable_atr", value)
|
||
addPatch("enable_boll", value)
|
||
continue
|
||
}
|
||
addPatch(field, value)
|
||
}
|
||
}
|
||
|
||
fields := make([]string, 0, len(seen))
|
||
for field := range seen {
|
||
fields = append(fields, field)
|
||
}
|
||
sort.Strings(fields)
|
||
|
||
patches := make([]strategyConfigPatch, 0, len(fields))
|
||
for _, field := range fields {
|
||
patches = append(patches, strategyConfigPatch{Field: field, Value: seen[field]})
|
||
}
|
||
return patches
|
||
}
|
||
|
||
func applyStrategyConfigPatch(cfg *store.StrategyConfig, field, value string) error {
|
||
switch field {
|
||
case "max_positions":
|
||
parsed, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return fmt.Errorf("最大持仓需要是整数")
|
||
}
|
||
cfg.RiskControl.MaxPositions = parsed
|
||
case "min_confidence":
|
||
parsed, err := strconv.Atoi(value)
|
||
if err != nil {
|
||
return fmt.Errorf("最小置信度需要是整数")
|
||
}
|
||
cfg.RiskControl.MinConfidence = 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 "primary_timeframe":
|
||
cfg.Indicators.Klines.PrimaryTimeframe = value
|
||
case "selected_timeframes":
|
||
tfs := strings.Split(value, ",")
|
||
cfg.Indicators.Klines.SelectedTimeframes = tfs
|
||
cfg.Indicators.Klines.EnableMultiTimeframe = len(tfs) > 1
|
||
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"
|
||
default:
|
||
return fmt.Errorf("unsupported strategy config field: %s", field)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (a *Agent) executeTraderManagementAction(storeUserID string, userID int64, lang, text string, session skillSession) string {
|
||
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 := 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":
|
||
if session.Action == "update_bindings" {
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "collect_bindings")
|
||
}
|
||
args := manageTraderArgs{Action: "update", TraderID: session.TargetRef.ID}
|
||
if match := pickMentionedOption(text, a.loadEnabledModelOptions(storeUserID)); match != nil {
|
||
args.AIModelID = match.ID
|
||
}
|
||
if match := pickMentionedOption(text, a.loadExchangeOptions(storeUserID)); match != nil {
|
||
args.ExchangeID = match.ID
|
||
}
|
||
if match := pickMentionedOption(text, a.loadStrategyOptions(storeUserID)); match != nil {
|
||
args.StrategyID = match.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)
|
||
}
|
||
if value := fieldValue(session, "ai_model_id"); value != "" {
|
||
args.AIModelID = value
|
||
}
|
||
if value := fieldValue(session, "exchange_id"); value != "" {
|
||
args.ExchangeID = value
|
||
}
|
||
if value := fieldValue(session, "strategy_id"); value != "" {
|
||
args.StrategyID = value
|
||
}
|
||
if args.AIModelID == "" && args.ExchangeID == "" && args.StrategyID == "" {
|
||
setSkillDAGStep(&session, "collect_bindings")
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "这次是更新交易员绑定,请直接说要换成哪个模型、交易所或策略。"
|
||
}
|
||
return "This action updates trader bindings. Tell me which model, exchange, or strategy 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 "Failed to update trader bindings: " + errMsg
|
||
}
|
||
if lang == "zh" {
|
||
return "已更新交易员绑定。"
|
||
}
|
||
return "Updated trader bindings."
|
||
}
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "collect_name")
|
||
}
|
||
args := manageTraderArgs{Action: "update", TraderID: session.TargetRef.ID}
|
||
if minutes, ok := parseScanIntervalMinutes(text); ok && minutes > 0 {
|
||
args.ScanIntervalMinutes = &minutes
|
||
}
|
||
if value, ok := extractStrategyConfigValue(text, "btceth_max_leverage"); ok {
|
||
if parsed, err := strconv.Atoi(value); err == nil {
|
||
args.BTCETHLeverage = &parsed
|
||
}
|
||
}
|
||
if value, ok := extractStrategyConfigValue(text, "altcoin_max_leverage"); ok {
|
||
if parsed, err := strconv.Atoi(value); err == nil {
|
||
args.AltcoinLeverage = &parsed
|
||
}
|
||
}
|
||
if prompt := extractCredentialValue(text, []string{"自定义提示词", "提示词", "custom prompt", "prompt"}); prompt != "" &&
|
||
containsAny(strings.ToLower(text), []string{"提示词", "prompt"}) {
|
||
args.CustomPrompt = prompt
|
||
}
|
||
if enabled, ok := parseFlagValue(text, []string{"ai500"}); ok {
|
||
args.UseAI500 = &enabled
|
||
}
|
||
if enabled, ok := parseFlagValue(text, []string{"oi top", "oitop", "持仓量排名"}); ok {
|
||
args.UseOITop = &enabled
|
||
}
|
||
if args.ScanIntervalMinutes != nil || args.BTCETHLeverage != nil || args.AltcoinLeverage != nil || args.CustomPrompt != "" || args.UseAI500 != nil || args.UseOITop != nil {
|
||
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 "Failed to update trader: " + errMsg
|
||
}
|
||
if lang == "zh" {
|
||
return "已更新交易员配置。"
|
||
}
|
||
return "Updated trader config."
|
||
}
|
||
newName := extractTraderName(text)
|
||
if newName == "" {
|
||
newName = extractPostKeywordName(text, []string{"改成", "改为", "rename to"})
|
||
}
|
||
if newName != "" {
|
||
setField(&session, "name", newName)
|
||
}
|
||
newName = fieldValue(session, "name")
|
||
if newName == "" {
|
||
setSkillDAGStep(&session, "collect_name")
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "目前更新交易员这条 skill 先支持改名。请直接告诉我新的名字。"
|
||
}
|
||
return "This trader update skill currently supports renaming first. Tell me the new name."
|
||
}
|
||
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 "Failed to update trader: " + 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 := 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 "Failed to delete exchange config: " + errMsg
|
||
}
|
||
if lang == "zh" {
|
||
return "已删除交易所配置。"
|
||
}
|
||
return "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")
|
||
}
|
||
}
|
||
accountName := extractTraderName(text)
|
||
if accountName == "" {
|
||
accountName = extractPostKeywordName(text, []string{"改成", "改为", "账户名改成", "rename to"})
|
||
}
|
||
if accountName != "" {
|
||
setField(&session, "account_name", accountName)
|
||
}
|
||
if enabled, ok := parseEnabledValue(text); ok {
|
||
setField(&session, "enabled", strconv.FormatBool(enabled))
|
||
}
|
||
if value := extractCredentialValue(text, []string{"api key", "apikey", "api_key"}); value != "" {
|
||
setField(&session, "api_key", value)
|
||
}
|
||
if value := extractCredentialValue(text, []string{"secret key", "secret", "secret_key"}); value != "" {
|
||
setField(&session, "secret_key", value)
|
||
}
|
||
if value := extractCredentialValue(text, []string{"passphrase", "密码短语"}); value != "" {
|
||
setField(&session, "passphrase", value)
|
||
}
|
||
if testnet, ok := parseFlagValue(text, []string{"testnet", "测试网"}); ok {
|
||
setField(&session, "testnet", strconv.FormatBool(testnet))
|
||
}
|
||
payload := map[string]any{"action": "update", "exchange_id": session.TargetRef.ID}
|
||
accountName = fieldValue(session, "account_name")
|
||
if accountName != "" && session.Action != "update_status" {
|
||
payload["account_name"] = accountName
|
||
}
|
||
if enabledRaw := fieldValue(session, "enabled"); enabledRaw != "" {
|
||
payload["enabled"] = enabledRaw == "true"
|
||
}
|
||
if value := fieldValue(session, "api_key"); value != "" {
|
||
payload["api_key"] = value
|
||
}
|
||
if value := fieldValue(session, "secret_key"); value != "" {
|
||
payload["secret_key"] = value
|
||
}
|
||
if value := fieldValue(session, "passphrase"); value != "" {
|
||
payload["passphrase"] = value
|
||
}
|
||
if value := fieldValue(session, "testnet"); value != "" {
|
||
payload["testnet"] = value == "true"
|
||
}
|
||
if session.Action == "update_status" {
|
||
delete(payload, "account_name")
|
||
}
|
||
if len(payload) == 2 {
|
||
if session.Action == "update_status" {
|
||
setSkillDAGStep(&session, "collect_enabled")
|
||
} else {
|
||
setSkillDAGStep(&session, "collect_account_name")
|
||
}
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "目前更新交易所 skill 支持改账户名、启用状态、API Key、Secret、Passphrase 和 testnet。请告诉我你要改什么。"
|
||
}
|
||
return "This exchange update skill supports account name, enabled state, API key, secret, passphrase, and testnet."
|
||
}
|
||
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 "Failed to update exchange config: " + errMsg
|
||
}
|
||
if lang == "zh" {
|
||
return "已更新交易所配置。"
|
||
}
|
||
return "Updated exchange config."
|
||
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 := 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 "Failed to delete model config: " + 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}
|
||
if url := extractURL(text); url != "" {
|
||
setField(&session, "custom_api_url", url)
|
||
}
|
||
if enabled, ok := parseEnabledValue(text); ok {
|
||
setField(&session, "enabled", strconv.FormatBool(enabled))
|
||
}
|
||
if apiKey := extractCredentialValue(text, []string{"api key", "apikey", "api_key"}); apiKey != "" {
|
||
setField(&session, "api_key", apiKey)
|
||
}
|
||
if modelName := extractPostKeywordName(text, []string{"model name", "模型名", "模型名称", "改成"}); modelName != "" {
|
||
setField(&session, "custom_model_name", modelName)
|
||
}
|
||
if value := fieldValue(session, "custom_api_url"); value != "" {
|
||
payload["custom_api_url"] = value
|
||
}
|
||
if value := fieldValue(session, "enabled"); value != "" {
|
||
payload["enabled"] = value == "true"
|
||
}
|
||
if value := fieldValue(session, "api_key"); value != "" {
|
||
payload["api_key"] = value
|
||
}
|
||
if value := 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:
|
||
setSkillDAGStep(&session, "collect_custom_model_name")
|
||
}
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "目前更新模型 skill 支持改 API Key、URL、模型名和启用状态。请告诉我你要改什么。"
|
||
}
|
||
return "This model update skill supports API key, URL, model name, and enabled state."
|
||
}
|
||
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 "Failed to update model config: " + errMsg
|
||
}
|
||
a.clearSkillSession(userID)
|
||
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 "Failed to activate strategy: " + errMsg
|
||
}
|
||
if lang == "zh" {
|
||
return "已激活策略。"
|
||
}
|
||
return "Activated strategy."
|
||
case "duplicate":
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "collect_name")
|
||
}
|
||
newName := extractTraderName(text)
|
||
if newName == "" {
|
||
newName = extractPostKeywordName(text, []string{"叫", "名为", "改成", "rename to"})
|
||
}
|
||
if newName != "" {
|
||
setField(&session, "name", newName)
|
||
}
|
||
newName = fieldValue(session, "name")
|
||
if newName == "" {
|
||
setSkillDAGStep(&session, "collect_name")
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "复制策略时,我还需要一个新名称。"
|
||
}
|
||
return "I still need a new name for the duplicated strategy."
|
||
}
|
||
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 "Failed to duplicate strategy: " + 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 "Failed to load strategies: " + 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 := 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, "Failed: "+strings.Join(failedNames, "; "))
|
||
}
|
||
if len(deletedNames) > 0 {
|
||
parts = append(parts, "Deleted: "+strings.Join(deletedNames, ", "))
|
||
}
|
||
return strings.Join(parts, "\n")
|
||
}
|
||
if msg, waiting := 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 "Failed to delete strategy: " + 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" {
|
||
return a.executeStrategyConfigUpdate(storeUserID, userID, lang, text, session)
|
||
}
|
||
if fieldValue(session, skillDAGStepField) == "" {
|
||
setSkillDAGStep(&session, "collect_name")
|
||
}
|
||
newName := extractTraderName(text)
|
||
if newName == "" {
|
||
newName = extractPostKeywordName(text, []string{"改成", "改为", "rename to"})
|
||
}
|
||
if newName != "" {
|
||
setField(&session, "name", newName)
|
||
}
|
||
newName = fieldValue(session, "name")
|
||
if newName == "" {
|
||
setSkillDAGStep(&session, "collect_name")
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "目前更新策略 skill 先支持改名。请告诉我新的策略名称。"
|
||
}
|
||
return "This strategy update skill currently supports renaming first."
|
||
}
|
||
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 "Failed to update strategy: " + 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 "Failed to load strategy: " + err.Error()
|
||
}
|
||
|
||
prompt := extractQuotedContent(text)
|
||
if prompt == "" {
|
||
prompt = extractPostKeywordName(text, []string{"prompt改成", "prompt 改成", "提示词改成", "提示词改为", "custom prompt 改成"})
|
||
}
|
||
if prompt != "" {
|
||
setField(&session, "prompt", prompt)
|
||
}
|
||
prompt = fieldValue(session, "prompt")
|
||
if prompt == "" {
|
||
setSkillDAGStep(&session, "collect_prompt")
|
||
a.saveSkillSession(userID, session)
|
||
if lang == "zh" {
|
||
return "这次是更新策略 prompt,请直接把新的 prompt 内容发给我,最好放在引号里。"
|
||
}
|
||
return "This action updates the strategy prompt. Send me the new prompt text, ideally inside quotes."
|
||
}
|
||
|
||
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 _, 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 "Failed to load strategy: " + err.Error()
|
||
}
|
||
|
||
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 "Failed to update strategy config: " + err.Error()
|
||
}
|
||
changed = append(changed, strategyConfigFieldDisplayName(patch.Field, lang))
|
||
}
|
||
cfg.ClampLimits()
|
||
setSkillDAGStep(&session, "apply_field_update")
|
||
setSkillDAGStep(&session, "execute_update")
|
||
msgZH := "已更新策略参数:" + strings.Join(changed, "、") + "。"
|
||
msgEN := "Updated strategy config fields: " + strings.Join(changed, ", ") + "."
|
||
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 "This action updates strategy config. I currently support max positions, min confidence, primary timeframe, and selected timeframes. Tell me which field to change first."
|
||
}
|
||
|
||
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("I still need the target value for %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()
|
||
}
|
||
|
||
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")
|
||
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) 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 "Failed to serialize strategy config: " + err.Error()
|
||
}
|
||
raw, _ := json.Marshal(map[string]any{
|
||
"action": "update",
|
||
"strategy_id": strategy.ID,
|
||
"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 "Failed to update strategy: " + errMsg
|
||
}
|
||
if lang == "zh" {
|
||
return zhMsg
|
||
}
|
||
return enMsg
|
||
}
|
||
|
||
func extractQuotedContent(text string) string {
|
||
if matches := quotedNamePattern.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
|
||
}
|