mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-11 23:07:01 +08:00
fix: toFloat handles json.Number, CORS credentials+wildcard bug, strategy config error handling
- agent/agent.go: toFloat() now handles json.Number and int32 types to prevent silent data loss - api/server.go: CORS fix — echo specific origin instead of '*' when credentials are enabled (browsers reject Allow-Credentials with wildcard) - api/strategy.go: all 4 json.Unmarshal calls now check errors and return 'config_error' field instead of silently serving zero-value configs - store/decision.go: explicitly mark best-effort unmarshal with _ = assignment for clarity
This commit is contained in:
@@ -7,6 +7,7 @@ package agent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -548,7 +549,9 @@ func toFloat(v interface{}) float64 {
|
|||||||
case float32: return float64(x)
|
case float32: return float64(x)
|
||||||
case int: return float64(x)
|
case int: return float64(x)
|
||||||
case int64: return float64(x)
|
case int64: return float64(x)
|
||||||
|
case int32: return float64(x)
|
||||||
case string: f, _ := strconv.ParseFloat(x, 64); return f
|
case string: f, _ := strconv.ParseFloat(x, 64); return f
|
||||||
|
case json.Number: f, _ := x.Float64(); return f
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,14 @@ func corsMiddleware() gin.HandlerFunc {
|
|||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
origin := c.Request.Header.Get("Origin")
|
origin := c.Request.Header.Get("Origin")
|
||||||
if allowAll {
|
if allowAll {
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
if origin != "" {
|
||||||
|
// When credentials are needed, we must echo the specific origin
|
||||||
|
// instead of "*" (browsers reject Allow-Credentials with wildcard).
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
||||||
|
c.Writer.Header().Set("Vary", "Origin")
|
||||||
|
} else {
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
}
|
||||||
} else if origin != "" && allowed[origin] {
|
} else if origin != "" && allowed[origin] {
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
||||||
c.Writer.Header().Set("Vary", "Origin")
|
c.Writer.Header().Set("Vary", "Origin")
|
||||||
|
|||||||
@@ -56,8 +56,11 @@ func (s *Server) handlePublicStrategies(c *gin.Context) {
|
|||||||
// Only include config if config_visible is true
|
// Only include config if config_visible is true
|
||||||
if st.ConfigVisible {
|
if st.ConfigVisible {
|
||||||
var config store.StrategyConfig
|
var config store.StrategyConfig
|
||||||
json.Unmarshal([]byte(st.Config), &config)
|
if err := json.Unmarshal([]byte(st.Config), &config); err != nil {
|
||||||
item["config"] = config
|
item["config_error"] = "invalid config format"
|
||||||
|
} else {
|
||||||
|
item["config"] = config
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result = append(result, item)
|
result = append(result, item)
|
||||||
@@ -86,9 +89,9 @@ func (s *Server) handleGetStrategies(c *gin.Context) {
|
|||||||
result := make([]gin.H, 0, len(strategies))
|
result := make([]gin.H, 0, len(strategies))
|
||||||
for _, st := range strategies {
|
for _, st := range strategies {
|
||||||
var config store.StrategyConfig
|
var config store.StrategyConfig
|
||||||
json.Unmarshal([]byte(st.Config), &config)
|
configErr := json.Unmarshal([]byte(st.Config), &config)
|
||||||
|
|
||||||
result = append(result, gin.H{
|
item := gin.H{
|
||||||
"id": st.ID,
|
"id": st.ID,
|
||||||
"name": st.Name,
|
"name": st.Name,
|
||||||
"description": st.Description,
|
"description": st.Description,
|
||||||
@@ -96,10 +99,15 @@ func (s *Server) handleGetStrategies(c *gin.Context) {
|
|||||||
"is_default": st.IsDefault,
|
"is_default": st.IsDefault,
|
||||||
"is_public": st.IsPublic,
|
"is_public": st.IsPublic,
|
||||||
"config_visible": st.ConfigVisible,
|
"config_visible": st.ConfigVisible,
|
||||||
"config": config,
|
|
||||||
"created_at": st.CreatedAt,
|
"created_at": st.CreatedAt,
|
||||||
"updated_at": st.UpdatedAt,
|
"updated_at": st.UpdatedAt,
|
||||||
})
|
}
|
||||||
|
if configErr != nil {
|
||||||
|
item["config_error"] = "invalid config format"
|
||||||
|
} else {
|
||||||
|
item["config"] = config
|
||||||
|
}
|
||||||
|
result = append(result, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
@@ -124,18 +132,23 @@ func (s *Server) handleGetStrategy(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config store.StrategyConfig
|
var config store.StrategyConfig
|
||||||
json.Unmarshal([]byte(strategy.Config), &config)
|
configErr := json.Unmarshal([]byte(strategy.Config), &config)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
result := gin.H{
|
||||||
"id": strategy.ID,
|
"id": strategy.ID,
|
||||||
"name": strategy.Name,
|
"name": strategy.Name,
|
||||||
"description": strategy.Description,
|
"description": strategy.Description,
|
||||||
"is_active": strategy.IsActive,
|
"is_active": strategy.IsActive,
|
||||||
"is_default": strategy.IsDefault,
|
"is_default": strategy.IsDefault,
|
||||||
"config": config,
|
|
||||||
"created_at": strategy.CreatedAt,
|
"created_at": strategy.CreatedAt,
|
||||||
"updated_at": strategy.UpdatedAt,
|
"updated_at": strategy.UpdatedAt,
|
||||||
})
|
}
|
||||||
|
if configErr != nil {
|
||||||
|
result["config_error"] = "invalid config format"
|
||||||
|
} else {
|
||||||
|
result["config"] = config
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleCreateStrategy Create strategy.
|
// handleCreateStrategy Create strategy.
|
||||||
@@ -383,18 +396,23 @@ func (s *Server) handleGetActiveStrategy(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config store.StrategyConfig
|
var config store.StrategyConfig
|
||||||
json.Unmarshal([]byte(strategy.Config), &config)
|
configErr := json.Unmarshal([]byte(strategy.Config), &config)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
result := gin.H{
|
||||||
"id": strategy.ID,
|
"id": strategy.ID,
|
||||||
"name": strategy.Name,
|
"name": strategy.Name,
|
||||||
"description": strategy.Description,
|
"description": strategy.Description,
|
||||||
"is_active": strategy.IsActive,
|
"is_active": strategy.IsActive,
|
||||||
"is_default": strategy.IsDefault,
|
"is_default": strategy.IsDefault,
|
||||||
"config": config,
|
|
||||||
"created_at": strategy.CreatedAt,
|
"created_at": strategy.CreatedAt,
|
||||||
"updated_at": strategy.UpdatedAt,
|
"updated_at": strategy.UpdatedAt,
|
||||||
})
|
}
|
||||||
|
if configErr != nil {
|
||||||
|
result["config_error"] = "invalid config format"
|
||||||
|
} else {
|
||||||
|
result["config"] = config
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetDefaultStrategyConfig Get default strategy configuration template
|
// handleGetDefaultStrategyConfig Get default strategy configuration template
|
||||||
|
|||||||
@@ -138,9 +138,10 @@ func (db *DecisionRecordDB) toRecord() *DecisionRecord {
|
|||||||
ErrorMessage: db.ErrorMessage,
|
ErrorMessage: db.ErrorMessage,
|
||||||
AIRequestDurationMs: db.AIRequestDurationMs,
|
AIRequestDurationMs: db.AIRequestDurationMs,
|
||||||
}
|
}
|
||||||
json.Unmarshal([]byte(db.CandidateCoins), &record.CandidateCoins)
|
// Best-effort unmarshal — empty/malformed JSON fields are left as zero values.
|
||||||
json.Unmarshal([]byte(db.ExecutionLog), &record.ExecutionLog)
|
_ = json.Unmarshal([]byte(db.CandidateCoins), &record.CandidateCoins)
|
||||||
json.Unmarshal([]byte(db.Decisions), &record.Decisions)
|
_ = json.Unmarshal([]byte(db.ExecutionLog), &record.ExecutionLog)
|
||||||
|
_ = json.Unmarshal([]byte(db.Decisions), &record.Decisions)
|
||||||
return record
|
return record
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user