feat: sync deployed v1.0.0-rc.38-custom.42
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
This commit is contained in:
1 parent
aa6c3f50f4
commit
ceddfd3d6b
828 files changed
+122253
-13702
No files matched your search
@@ -0,0 +1,195 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
)
|
||||
|
||||
type automaticDetectionResult struct {
|
||||
channelID int
|
||||
id string
|
||||
at int64
|
||||
outcome string
|
||||
}
|
||||
|
||||
func FingerprintScheduleOutcome(status string) string {
|
||||
switch status {
|
||||
case "match":
|
||||
return "normal"
|
||||
case "mismatch":
|
||||
return "degraded"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// ReconcileAutomaticDetectionSchedules seeds pre-upgrade history once and
|
||||
// recovers results persisted before a worker lost its lease or process.
|
||||
func ReconcileAutomaticDetectionSchedules(ctx context.Context, channels []model.Channel, kind string) (map[int]model.DetectionSchedule, map[int]bool, error) {
|
||||
states := map[int]model.DetectionSchedule{}
|
||||
active := map[int]bool{}
|
||||
if len(channels) == 0 {
|
||||
return states, active, nil
|
||||
}
|
||||
ids := make([]int, 0, len(channels))
|
||||
for _, c := range channels {
|
||||
ids = append(ids, c.Id)
|
||||
}
|
||||
var rows []model.DetectionSchedule
|
||||
if err := model.DB.WithContext(ctx).Where("channel_id IN ? AND kind = ?", ids, kind).Find(&rows).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
cutoff := time.Now().Unix()
|
||||
for _, row := range rows {
|
||||
states[row.ChannelID] = row
|
||||
cutoff = min(cutoff, row.LastFinishedAt)
|
||||
}
|
||||
if len(rows) != len(ids) {
|
||||
cutoff = 0
|
||||
}
|
||||
var results []automaticDetectionResult
|
||||
if kind == "fingerprint" {
|
||||
var tasks []model.SystemTask
|
||||
if err := model.DB.WithContext(ctx).Select("status", "payload", "state", "updated_at").Where("type = ? AND updated_at >= ?", AstraSamplingTaskType, cutoff).Find(&tasks).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, task := range tasks {
|
||||
var payload struct {
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
if task.DecodePayload(&payload) != nil || payload.Force {
|
||||
continue
|
||||
}
|
||||
var state AstraSamplingState
|
||||
if task.DecodeState(&state) != nil {
|
||||
continue
|
||||
}
|
||||
terminal := task.Status == model.SystemTaskStatusSucceeded || task.Status == model.SystemTaskStatusFailed
|
||||
for _, round := range state.Rounds {
|
||||
if round.FinishedAt == 0 && !terminal {
|
||||
active[round.ChannelID] = true
|
||||
continue
|
||||
}
|
||||
at, outcome := round.FinishedAt, FingerprintScheduleOutcome(AstraSamplingResultStatus(round.Result))
|
||||
if at == 0 {
|
||||
at, outcome = task.UpdatedAt, "unknown"
|
||||
}
|
||||
results = append(results, automaticDetectionResult{round.ChannelID, round.ID, at, outcome})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var runs []model.DetectionRun
|
||||
if err := model.DB.WithContext(ctx).Select("id", "channel_id", "status", "quality", "settled", "finished_at").Where("channel_id IN ? AND user_id = 0 AND kind = ? AND (finished_at >= ? OR settled = ?)", ids, kind, cutoff, false).Find(&runs).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, run := range runs {
|
||||
if !run.Settled {
|
||||
active[run.ChannelID] = true
|
||||
continue
|
||||
}
|
||||
outcome := "unknown"
|
||||
if run.Status == "succeeded" && (run.Quality == "normal" || run.Quality == "degraded") {
|
||||
outcome = run.Quality
|
||||
}
|
||||
results = append(results, automaticDetectionResult{run.ChannelID, run.ID, run.FinishedAt, outcome})
|
||||
}
|
||||
}
|
||||
sort.SliceStable(results, func(i, j int) bool {
|
||||
if results[i].at == results[j].at {
|
||||
return results[i].id < results[j].id
|
||||
}
|
||||
return results[i].at < results[j].at
|
||||
})
|
||||
for _, channel := range channels {
|
||||
if _, exists := states[channel.Id]; exists {
|
||||
continue
|
||||
}
|
||||
seed := model.DetectionSchedule{ChannelID: channel.Id, Kind: kind, IntervalMinutes: 30}
|
||||
for _, result := range results {
|
||||
if result.channelID != channel.Id {
|
||||
continue
|
||||
}
|
||||
seed.LastFinishedAt, seed.LastResultID = result.at, result.id
|
||||
seed.IntervalMinutes = 15
|
||||
if result.outcome == "normal" || result.outcome == "degraded" {
|
||||
seed.LastValid = result.outcome
|
||||
seed.IntervalMinutes = 30
|
||||
}
|
||||
seed.NextDueAt = result.at + int64(seed.IntervalMinutes)*60
|
||||
}
|
||||
if err := model.EnsureDetectionSchedule(ctx, seed); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
for _, result := range results {
|
||||
// Newly seeded clocks already represent the latest historical result.
|
||||
if state, existed := states[result.channelID]; !existed || result.at <= state.LastFinishedAt {
|
||||
continue
|
||||
}
|
||||
if err := model.ApplyDetectionScheduleResult(ctx, result.channelID, kind, result.id, result.at, result.outcome); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
rows = nil
|
||||
if err := model.DB.WithContext(ctx).Where("channel_id IN ? AND kind = ?", ids, kind).Find(&rows).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, row := range rows {
|
||||
states[row.ChannelID] = row
|
||||
}
|
||||
return states, active, nil
|
||||
}
|
||||
|
||||
func AutomaticDetectionChannelsDue(ctx context.Context, kind string, now time.Time) ([]model.Channel, map[int]model.DetectionSchedule, error) {
|
||||
channels, err := AstraSamplingChannels(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
states, active, err := ReconcileAutomaticDetectionSchedules(ctx, channels, kind)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var due []model.Channel
|
||||
for _, channel := range channels {
|
||||
if !active[channel.Id] && states[channel.Id].NextDueAt <= now.Unix() {
|
||||
due = append(due, channel)
|
||||
}
|
||||
}
|
||||
return due, states, nil
|
||||
}
|
||||
|
||||
// Public group summaries expose ranges, never channel identities.
|
||||
func DetectionScheduleRanges(ctx context.Context, channels []model.Channel, kind string) (map[string][2]int, error) {
|
||||
var rows []model.DetectionSchedule
|
||||
if err := model.DB.WithContext(ctx).Where("kind = ?", kind).Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
intervals := map[int]int{}
|
||||
for _, row := range rows {
|
||||
intervals[row.ChannelID] = row.IntervalMinutes
|
||||
}
|
||||
ranges := map[string][2]int{}
|
||||
for _, channel := range channels {
|
||||
if channel.Status != common.ChannelStatusEnabled || !ChannelSupportsAstra(&channel) {
|
||||
continue
|
||||
}
|
||||
interval := intervals[channel.Id]
|
||||
if interval == 0 {
|
||||
interval = 30
|
||||
}
|
||||
for _, group := range channel.GetGroups() {
|
||||
r := ranges[group]
|
||||
if r[0] == 0 {
|
||||
r = [2]int{interval, interval}
|
||||
} else {
|
||||
r = [2]int{min(r[0], interval), max(r[1], interval)}
|
||||
}
|
||||
ranges[group] = r
|
||||
}
|
||||
}
|
||||
return ranges, nil
|
||||
}
|
||||
Reference in new issue
Block a user