change v1

This commit is contained in:
lky-spec
2026-04-25 16:18:45 +08:00
parent 737f9bca95
commit c244e4cdf1
89 changed files with 17382 additions and 6198 deletions

View File

@@ -39,6 +39,10 @@ func (s *Server) handleCreateAgentPreference(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "text required"})
return
}
if len([]rune(strings.TrimSpace(req.Text))) > 500 {
c.JSON(http.StatusBadRequest, gin.H{"error": "text too long"})
return
}
created, err := agent.NewPersistentPreference(req.Text)
if err != nil {

View File

@@ -11,11 +11,27 @@ import (
func (s *Server) RegisterAgentHandler(h *agent.WebHandler) {
// Chat requires auth — can trigger trades and access account data
s.router.POST("/api/agent/chat", s.authMiddleware(), func(c *gin.Context) {
req := c.Request.WithContext(agent.WithStoreUserID(c.Request.Context(), c.GetString("user_id")))
isAdmin := c.GetString("user_id") == "admin"
ctx := agent.WithStoreUserID(c.Request.Context(), c.GetString("user_id"))
ctx = agent.WithSessionPolicy(ctx, agent.SessionPolicy{
Authenticated: true,
IsAdmin: isAdmin,
CanExecuteTrade: isAdmin,
CanViewSensitiveSecrets: false,
})
req := c.Request.WithContext(ctx)
h.HandleChat(c.Writer, req)
})
s.router.POST("/api/agent/chat/stream", s.authMiddleware(), func(c *gin.Context) {
req := c.Request.WithContext(agent.WithStoreUserID(c.Request.Context(), c.GetString("user_id")))
isAdmin := c.GetString("user_id") == "admin"
ctx := agent.WithStoreUserID(c.Request.Context(), c.GetString("user_id"))
ctx = agent.WithSessionPolicy(ctx, agent.SessionPolicy{
Authenticated: true,
IsAdmin: isAdmin,
CanExecuteTrade: isAdmin,
CanViewSensitiveSecrets: false,
})
req := c.Request.WithContext(ctx)
h.HandleChatStream(c.Writer, req)
})
// Public endpoints — read-only market data

View File

@@ -10,6 +10,7 @@ import (
"nofx/crypto"
"nofx/logger"
"nofx/security"
"nofx/store"
"nofx/wallet"
"github.com/gin-gonic/gin"
@@ -77,8 +78,11 @@ func (s *Server) handleGetModelConfigs(c *gin.Context) {
logger.Infof("✅ Found %d AI model configs", len(models))
// Convert to safe response structure, remove sensitive information
safeModels := make([]SafeModelConfig, len(models))
for i, model := range models {
safeModels := make([]SafeModelConfig, 0, len(models))
for _, model := range models {
if !store.IsVisibleAIModel(model) {
continue
}
safeModel := SafeModelConfig{
ID: model.ID,
Name: model.Name,
@@ -100,7 +104,23 @@ func (s *Server) handleGetModelConfigs(c *gin.Context) {
}
}
safeModels[i] = safeModel
safeModels = append(safeModels, safeModel)
}
if len(safeModels) == 0 {
logger.Infof("⚠️ No visible AI models in database, returning defaults")
defaultModels := []SafeModelConfig{
{ID: "deepseek", Name: "DeepSeek AI", Provider: "deepseek", Enabled: false, HasAPIKey: false},
{ID: "qwen", Name: "Qwen AI", Provider: "qwen", Enabled: false, HasAPIKey: false},
{ID: "openai", Name: "OpenAI", Provider: "openai", Enabled: false, HasAPIKey: false},
{ID: "claude", Name: "Claude AI", Provider: "claude", Enabled: false, HasAPIKey: false},
{ID: "gemini", Name: "Gemini AI", Provider: "gemini", Enabled: false, HasAPIKey: false},
{ID: "grok", Name: "Grok AI", Provider: "grok", Enabled: false, HasAPIKey: false},
{ID: "kimi", Name: "Kimi AI", Provider: "kimi", Enabled: false, HasAPIKey: false},
{ID: "minimax", Name: "MiniMax AI", Provider: "minimax", Enabled: false, HasAPIKey: false},
}
c.JSON(http.StatusOK, defaultModels)
return
}
c.JSON(http.StatusOK, safeModels)

View File

@@ -8,6 +8,7 @@ import (
"nofx/config"
"nofx/crypto"
"nofx/logger"
"nofx/store"
"github.com/gin-gonic/gin"
)
@@ -96,9 +97,12 @@ func (s *Server) handleGetExchangeConfigs(c *gin.Context) {
logger.Infof("✅ Found %d exchange configs", len(exchanges))
// Convert to safe response structure, remove sensitive information
safeExchanges := make([]SafeExchangeConfig, len(exchanges))
for i, exchange := range exchanges {
safeExchanges[i] = SafeExchangeConfig{
safeExchanges := make([]SafeExchangeConfig, 0, len(exchanges))
for _, exchange := range exchanges {
if !store.IsVisibleExchange(exchange) {
continue
}
safeExchanges = append(safeExchanges, SafeExchangeConfig{
ID: exchange.ID,
ExchangeType: exchange.ExchangeType,
AccountName: exchange.AccountName,
@@ -110,7 +114,7 @@ func (s *Server) handleGetExchangeConfigs(c *gin.Context) {
AsterUser: exchange.AsterUser,
AsterSigner: exchange.AsterSigner,
LighterWalletAddr: exchange.LighterWalletAddr,
}
})
}
c.JSON(http.StatusOK, safeExchanges)

View File

@@ -14,6 +14,11 @@ import (
"gorm.io/gorm"
)
const (
maxManualBTCETHLeverage = 20
maxManualAltLeverage = 20
)
// AI trader management related structures
type CreateTraderRequest struct {
Name string `json:"name" binding:"required"`
@@ -65,6 +70,16 @@ func traderCreationRequestError(reason string) string {
return formatTraderCreationError(reason, "请检查你刚刚填写的内容后,再重新提交")
}
func validateTraderLeverageRange(btcEthLeverage, altcoinLeverage int) (string, string) {
if btcEthLeverage < 0 || btcEthLeverage > maxManualBTCETHLeverage {
return traderCreationRequestError("BTC/ETH 杠杆倍数需要在 1 到 20 倍之间"), "trader.create.invalid_btc_eth_leverage"
}
if altcoinLeverage < 0 || altcoinLeverage > maxManualAltLeverage {
return traderCreationRequestError("山寨币杠杆倍数需要在 1 到 20 倍之间"), "trader.create.invalid_altcoin_leverage"
}
return "", ""
}
func exchangeDisplayName(exchange *store.Exchange) string {
if exchange == nil {
return "所选交易所账户"
@@ -306,13 +321,9 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
return
}
// Validate leverage values
if req.BTCETHLeverage < 0 || req.BTCETHLeverage > 50 {
SafeBadRequestWithDetails(c, traderCreationRequestError("BTC/ETH 杠杆倍数需要在 1 到 50 倍之间"), "trader.create.invalid_btc_eth_leverage", nil)
return
}
if req.AltcoinLeverage < 0 || req.AltcoinLeverage > 20 {
SafeBadRequestWithDetails(c, traderCreationRequestError("山寨币杠杆倍数需要在 1 到 20 倍之间"), "trader.create.invalid_altcoin_leverage", nil)
// Validate leverage values against the same limits exposed by manual user config.
if errMsg, errCode := validateTraderLeverageRange(req.BTCETHLeverage, req.AltcoinLeverage); errMsg != "" {
SafeBadRequestWithDetails(c, errMsg, errCode, nil)
return
}
@@ -574,6 +585,11 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
return
}
if errMsg, errCode := validateTraderLeverageRange(req.BTCETHLeverage, req.AltcoinLeverage); errMsg != "" {
SafeBadRequestWithDetails(c, errMsg, errCode, nil)
return
}
// Set default values
isCrossMargin := existingTrader.IsCrossMargin // Keep original value
if req.IsCrossMargin != nil {

View File

@@ -0,0 +1,17 @@
package api
import "testing"
func TestValidateTraderLeverageRangeMatchesManualLimits(t *testing.T) {
if msg, code := validateTraderLeverageRange(20, 20); msg != "" || code != "" {
t.Fatalf("expected 20/20 leverage to be accepted, got msg=%q code=%q", msg, code)
}
if msg, code := validateTraderLeverageRange(21, 20); msg == "" || code != "trader.create.invalid_btc_eth_leverage" {
t.Fatalf("expected BTC/ETH leverage > 20 to be rejected, got msg=%q code=%q", msg, code)
}
if msg, code := validateTraderLeverageRange(20, 21); msg == "" || code != "trader.create.invalid_altcoin_leverage" {
t.Fatalf("expected altcoin leverage > 20 to be rejected, got msg=%q code=%q", msg, code)
}
}

View File

@@ -182,6 +182,8 @@ func (s *Server) handleCreateStrategy(c *gin.Context) {
defaultCfg := store.GetDefaultStrategyConfig(lang)
req.Config = &defaultCfg
}
beforeClamp := *req.Config
req.Config.ClampLimits()
// Serialize configuration
configJSON, err := json.Marshal(req.Config)
@@ -207,6 +209,7 @@ func (s *Server) handleCreateStrategy(c *gin.Context) {
// Validate configuration and collect warnings
warnings := validateStrategyConfig(req.Config)
warnings = append(warnings, store.StrategyClampWarnings(beforeClamp, *req.Config, req.Config.Language)...)
response := gin.H{
"id": strategy.ID,
@@ -263,14 +266,21 @@ func (s *Server) handleUpdateStrategy(c *gin.Context) {
mergedConfig = store.StrategyConfig{}
}
// Apply incoming config on top: top-level sections present in the request overwrite
// their corresponding existing section; absent sections remain unchanged.
// Apply incoming config on top while preserving nested fields that were not sent.
if len(req.Config) > 0 && string(req.Config) != "null" {
if err := json.Unmarshal(req.Config, &mergedConfig); err != nil {
var patch map[string]any
if err := json.Unmarshal(req.Config, &patch); err != nil {
SafeBadRequest(c, "Invalid config JSON")
return
}
mergedConfig, err = store.MergeStrategyConfig(mergedConfig, patch)
if err != nil {
SafeBadRequest(c, "Invalid config JSON")
return
}
}
beforeClamp := mergedConfig
mergedConfig.ClampLimits()
// Preserve existing name/description when not supplied
name := req.Name
@@ -324,6 +334,7 @@ func (s *Server) handleUpdateStrategy(c *gin.Context) {
// Validate merged configuration and collect warnings
warnings := validateStrategyConfig(&mergedConfig)
warnings = append(warnings, store.StrategyClampWarnings(beforeClamp, mergedConfig, mergedConfig.Language)...)
response := gin.H{"message": "Strategy updated successfully"}
if len(warnings) > 0 {