Files
new-api-custom/service/detection_schedule.go
T
cneicy a5f7b8e586
Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
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.
2026-09-26 04:49:11 +08:00

196 lines
5.9 KiB
Go

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
}