157 lines
5.1 KiB
Go
157 lines
5.1 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"errors"
|
|
"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"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var chainEnqueue sync.Mutex
|
|
var chainKinds = []string{"fingerprint", "ciallo", "vision"}
|
|
|
|
// All three child tasks are committed together, using the existing durable ledger.
|
|
func enqueueChainTest(ctx context.Context, group string, channelID int) ([]model.DetectionRun, error) {
|
|
chainEnqueue.Lock()
|
|
defer chainEnqueue.Unlock()
|
|
var runs []model.DetectionRun
|
|
err := model.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var active []model.DetectionRun
|
|
if err := tx.Where("group_name = ? AND request_key LIKE ? AND settled = ? AND deadline > ?", group, "chain:%", false, time.Now().Unix()).Order("created_at asc").Limit(1).Find(&active).Error; err != nil {
|
|
return err
|
|
}
|
|
if len(active) > 0 {
|
|
id, _, _ := strings.Cut(active[0].ID, "_")
|
|
return tx.Where("id IN ?", []string{id + "_fingerprint", id + "_ciallo", id + "_vision"}).Find(&runs).Error
|
|
}
|
|
id, err := common.GenerateRandomCharsKey(32)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
now := time.Now().Unix()
|
|
for _, kind := range chainKinds {
|
|
runs = append(runs, model.DetectionRun{ID: id + "_" + kind, RequestKey: "chain:" + id + ":" + kind, ChannelID: channelID, GroupName: group, Kind: kind, Status: "pending", CreatedAt: now, Deadline: now + 600})
|
|
}
|
|
return tx.Create(&runs).Error
|
|
})
|
|
return runs, err
|
|
}
|
|
|
|
func BotChainTest(c *gin.Context) {
|
|
c.Header("Cache-Control", "no-store")
|
|
key := os.Getenv("BOT_BALANCE_KEY")
|
|
derived := sha256.Sum256([]byte("newapi-chaintest-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 runs []model.DetectionRun
|
|
if c.Request.Method == http.MethodPost {
|
|
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(400, 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
|
|
}
|
|
slices.SortFunc(channels, func(a, b model.Channel) int { return a.Id - b.Id })
|
|
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
|
|
}
|
|
runs, err = enqueueChainTest(c.Request.Context(), input.Group, selected)
|
|
if err != nil {
|
|
c.JSON(503, gin.H{"success": false, "code": "unavailable"})
|
|
return
|
|
}
|
|
for _, run := range runs {
|
|
go processDetection(context.Background(), run)
|
|
}
|
|
} else {
|
|
id := c.Param("id")
|
|
if len(id) != 32 || strings.ContainsAny(id, "_%") {
|
|
c.JSON(404, gin.H{"success": false, "code": "not_found"})
|
|
return
|
|
}
|
|
err := model.DB.WithContext(c.Request.Context()).Where("id IN ? AND request_key LIKE ?", []string{id + "_fingerprint", id + "_ciallo", id + "_vision"}, "chain:"+id+":%").Find(&runs).Error
|
|
if err != nil {
|
|
c.JSON(503, gin.H{"success": false, "code": "unavailable"})
|
|
return
|
|
}
|
|
}
|
|
data, err := chainTestResult(runs, time.Now().Unix())
|
|
if err != nil {
|
|
c.JSON(404, gin.H{"success": false, "code": "not_found"})
|
|
return
|
|
}
|
|
c.JSON(200, gin.H{"success": true, "data": data})
|
|
}
|
|
|
|
func chainTestResult(runs []model.DetectionRun, now int64) (gin.H, error) {
|
|
if len(runs) != 3 {
|
|
return nil, errors.New("incomplete chain")
|
|
}
|
|
id, _, _ := strings.Cut(runs[0].ID, "_")
|
|
results := make(map[string]any, 3)
|
|
done := true
|
|
for _, run := range runs {
|
|
run = service.NormalizeVisionRun(run)
|
|
state, text, match := "running", "", ""
|
|
if run.Settled {
|
|
state = "failed"
|
|
var result fingerprint.Result
|
|
if run.Status == "succeeded" && common.UnmarshalJsonStr(run.Result, &result) == nil {
|
|
text = result.Closest
|
|
if run.Kind == "fingerprint" {
|
|
match = service.AstraSamplingResultStatus(result)
|
|
state = match
|
|
if state != "match" && state != "mismatch" && state != "inconclusive" {
|
|
state = "failed"
|
|
}
|
|
} else if strings.TrimSpace(text) != "" {
|
|
state = service.VisionQuality(text)
|
|
if run.Kind == "ciallo" {
|
|
state = service.CialloQuality(text)
|
|
}
|
|
}
|
|
}
|
|
} else if now >= run.Deadline {
|
|
state = "failed"
|
|
} else {
|
|
done = false
|
|
}
|
|
results[run.Kind] = gin.H{"status": state, "text": text}
|
|
}
|
|
return gin.H{"id": id, "group": runs[0].GroupName, "deadline": runs[0].Deadline, "done": done, "results": results}, nil
|
|
}
|