package controller import ( "context" "crypto/sha256" "crypto/subtle" "encoding/hex" "fmt" "net/http" "os" "slices" "strings" "sync" "time" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" fingerprint "github.com/QuantumNous/new-api/pkg/model_fingerprint" "github.com/QuantumNous/new-api/service" "github.com/gin-gonic/gin" ) var cialloEnqueue sync.Mutex func scheduleCialloDetections(ctx context.Context) error { cialloEnqueue.Lock() defer cialloEnqueue.Unlock() channels, schedules, err := service.AutomaticDetectionChannelsDue(ctx, "ciallo", time.Now()) if err != nil { return err } for _, channel := range channels { groups := channel.GetGroups() if len(groups) == 0 { continue } id, err := common.GenerateRandomCharsKey(32) if err != nil { return err } run := model.DetectionRun{ID: id, RequestKey: fmt.Sprintf("ciallo:%d:%s", channel.Id, schedules[channel.Id].LastResultID), ChannelID: channel.Id, GroupName: groups[0], Kind: "ciallo", Status: "pending", CreatedAt: time.Now().Unix()} if err := model.CreateDetection(&run); err != nil { return err } } return nil } // The derived capability only authorizes Ciallo probes, not balance queries. func BotCiallo(c *gin.Context) { c.Header("Cache-Control", "no-store") key := os.Getenv("BOT_BALANCE_KEY") derived := sha256.Sum256([]byte("newapi-ciallo-v1:" + key)) raw, bearer := strings.CutPrefix(c.GetHeader("Authorization"), "Bearer ") expected, supplied := sha256.Sum256([]byte(hex.EncodeToString(derived[:]))), sha256.Sum256([]byte(raw)) if len(key) < 32 || !bearer || subtle.ConstantTimeCompare(expected[:], supplied[:]) != 1 { c.JSON(http.StatusUnauthorized, gin.H{"success": false, "code": "unauthorized"}) return } var run model.DetectionRun if c.Request.Method == http.MethodGet { if model.DB.Where("id = ? AND kind = ?", c.Param("id"), "ciallo").First(&run).Error != nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "code": "not_found"}) return } } else { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 1024) var input struct { Group string `json:"group"` } if c.ShouldBindJSON(&input) != nil || strings.TrimSpace(input.Group) == "" || len(input.Group) > 128 { c.JSON(http.StatusBadRequest, gin.H{"success": false, "code": "invalid_group"}) return } input.Group = strings.TrimSpace(input.Group) channels, err := service.AstraSamplingChannels(c.Request.Context()) if err != nil { c.JSON(503, gin.H{"success": false, "code": "unavailable"}) return } selected := 0 for _, channel := range channels { if slices.Contains(channel.GetGroups(), input.Group) { selected = channel.Id break } } if selected == 0 { c.JSON(404, gin.H{"success": false, "code": "group_unavailable"}) return } cialloEnqueue.Lock() var active []model.DetectionRun err = model.DB.Where("channel_id = ? AND kind = ? AND settled = ?", selected, "ciallo", false).Order("created_at asc").Limit(1).Find(&active).Error if err == nil && len(active) > 0 { run = active[0] } else if err == nil { id, idErr := common.GenerateRandomCharsKey(32) err = idErr if err == nil { run = model.DetectionRun{ID: id, RequestKey: "bot-ciallo:" + id, ChannelID: selected, GroupName: input.Group, Kind: "ciallo", Status: "pending", CreatedAt: time.Now().Unix(), Deadline: time.Now().Unix() + 200} err = model.CreateDetection(&run) } } cialloEnqueue.Unlock() if err != nil { c.JSON(503, gin.H{"success": false, "code": "unavailable"}) return } // Start immediately; the durable worker reconciles interruptions and settlement. if len(active) == 0 { go processDetection(context.Background(), run) } } var result fingerprint.Result _ = common.UnmarshalJsonStr(run.Result, &result) c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"id": run.ID, "status": run.Status, "settled": run.Settled, "quality": run.Quality, "text": result.Closest}}) }