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
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
250 lines
7.5 KiB
Go
250 lines
7.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/model"
|
|
fingerprint "github.com/QuantumNous/new-api/pkg/model_fingerprint"
|
|
)
|
|
|
|
type DetectionHistory struct {
|
|
SourceURL string `json:"source_url,omitempty"`
|
|
ID string `json:"id"`
|
|
At int64 `json:"at"`
|
|
Status string `json:"status"`
|
|
Quality string `json:"quality"`
|
|
UserInitiated bool `json:"user_initiated"`
|
|
HasHTML bool `json:"has_html"`
|
|
}
|
|
type PelicanSummary struct {
|
|
Channels int `json:"channels"`
|
|
IntervalMinutes int `json:"interval_minutes"`
|
|
IntervalMaxMinutes int `json:"interval_max_minutes"`
|
|
WindowHours int `json:"window_hours"`
|
|
Normal int `json:"normal"`
|
|
Degraded int `json:"degraded"`
|
|
Unknown int `json:"unknown"`
|
|
Failed int `json:"failed"`
|
|
Running int `json:"running"`
|
|
History []DetectionHistory `json:"history"`
|
|
}
|
|
type ChannelDetectionEvidence struct {
|
|
At int64
|
|
Success bool
|
|
}
|
|
type GroupDetectionSummary struct {
|
|
ProbeHistory []ProbeHistoryPoint
|
|
Pelican *PelicanSummary
|
|
Status int
|
|
At int64
|
|
}
|
|
|
|
func DetectionFallbackStatus(ids []int, evidence map[int]ChannelDetectionEvidence, now int64) (int, int64) {
|
|
good, bad, at := 0, 0, int64(0)
|
|
for _, id := range ids {
|
|
e, ok := evidence[id]
|
|
if !ok || e.At < now-7200 {
|
|
continue
|
|
}
|
|
at = max(at, e.At)
|
|
if e.Success {
|
|
good++
|
|
} else {
|
|
bad++
|
|
}
|
|
}
|
|
if len(ids) > 0 && good == len(ids) {
|
|
return 1, at
|
|
}
|
|
if good > 0 {
|
|
return 2, at
|
|
}
|
|
if len(ids) > 0 && bad == len(ids) {
|
|
return 0, at
|
|
}
|
|
return -1, at
|
|
}
|
|
|
|
func GroupDetectionSummaries(ctx context.Context, now time.Time, fingerprints map[string]*AstraSamplingSummary, settings map[string]model.StatusPageGroup) (map[string]GroupDetectionSummary, error) {
|
|
channels, err := samplingChannels(ctx, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
groups := map[string][]int{}
|
|
byChannel := map[int][]string{}
|
|
pelicans := map[string]*PelicanSummary{}
|
|
evidence := map[int]ChannelDetectionEvidence{}
|
|
observe := func(id int, at int64, success bool) {
|
|
if at > evidence[id].At {
|
|
evidence[id] = ChannelDetectionEvidence{At: at, Success: success}
|
|
}
|
|
}
|
|
for _, channel := range channels {
|
|
byChannel[channel.Id] = channel.GetGroups()
|
|
for _, group := range channel.GetGroups() {
|
|
groups[group] = append(groups[group], channel.Id)
|
|
if ChannelSupportsAstra(&channel) {
|
|
if pelicans[group] == nil {
|
|
pelicans[group] = &PelicanSummary{IntervalMinutes: 30, WindowHours: 6, History: []DetectionHistory{}}
|
|
}
|
|
pelicans[group].Channels++
|
|
}
|
|
}
|
|
}
|
|
var tasks []model.SystemTask
|
|
ranges, err := DetectionScheduleRanges(ctx, channels, "pelican")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for group, summary := range pelicans {
|
|
r := ranges[group]
|
|
summary.IntervalMinutes, summary.IntervalMaxMinutes = r[0], r[1]
|
|
}
|
|
if err = model.DB.WithContext(ctx).Where("type IN ? AND created_at >= ?", []string{AstraSamplingTaskType, model.SystemTaskTypeChannelAvailability}, now.Add(-6*time.Hour).Unix()).Find(&tasks).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
for _, task := range tasks {
|
|
if task.Type == AstraSamplingTaskType {
|
|
var state AstraSamplingState
|
|
if task.DecodeState(&state) != nil {
|
|
continue
|
|
}
|
|
for _, round := range state.Rounds {
|
|
if round.FinishedAt <= 0 {
|
|
continue
|
|
}
|
|
if round.Result.ValidSamples > 0 {
|
|
observe(round.ChannelID, round.FinishedAt, true)
|
|
} else if round.Reason == "transport_error" || round.Reason == "channel_unavailable" {
|
|
observe(round.ChannelID, round.FinishedAt, false)
|
|
}
|
|
}
|
|
} else {
|
|
var state ChannelAvailabilityState
|
|
if task.DecodeState(&state) != nil {
|
|
continue
|
|
}
|
|
for _, round := range state.Rounds {
|
|
observe(round.ChannelID, round.At, round.Success)
|
|
}
|
|
}
|
|
}
|
|
var runs []model.DetectionRun
|
|
if err = model.DB.WithContext(ctx).Omit("html", "price", "remote_id").Where("created_at >= ?", now.Add(-6*time.Hour).Unix()).Find(&runs).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
var artworkIDs []string
|
|
if err = model.DB.WithContext(ctx).Model(&model.DetectionRun{}).Where("created_at >= ? AND html <> ?", now.Add(-6*time.Hour).Unix(), "").Pluck("id", &artworkIDs).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
artwork := map[string]bool{}
|
|
for _, id := range artworkIDs {
|
|
artwork[id] = true
|
|
}
|
|
ids := []string{}
|
|
for _, run := range runs {
|
|
ids = append(ids, run.ID)
|
|
}
|
|
var calls []model.DetectionCall
|
|
if len(ids) > 0 {
|
|
if err = model.DB.WithContext(ctx).Select("run_id", "finished_at", "available").Where("run_id IN ? AND state = ?", ids, "finished").Find(&calls).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
runChannels := map[string]int{}
|
|
for _, run := range runs {
|
|
runChannels[run.ID] = run.ChannelID
|
|
}
|
|
for _, call := range calls {
|
|
if call.Available != nil {
|
|
observe(runChannels[call.RunID], call.FinishedAt, *call.Available)
|
|
}
|
|
}
|
|
for _, run := range runs {
|
|
for _, group := range byChannel[run.ChannelID] {
|
|
if cutoff := settings[group].ResetBefore; cutoff > 0 && run.CreatedAt <= cutoff {
|
|
continue
|
|
}
|
|
if run.Kind == "fingerprint" {
|
|
summary := fingerprints[group]
|
|
if summary == nil || !run.Settled {
|
|
continue
|
|
}
|
|
var result fingerprint.Result
|
|
if common.UnmarshalJsonStr(run.Result, &result) != nil {
|
|
result.Status = "error"
|
|
}
|
|
if result.Status == "" {
|
|
result.Status = "error"
|
|
}
|
|
result.Status = AstraSamplingResultStatus(result)
|
|
switch result.Status {
|
|
case "match":
|
|
summary.Matches++
|
|
case "mismatch":
|
|
summary.Mismatches++
|
|
case "inconclusive":
|
|
summary.Inconclusive++
|
|
default:
|
|
summary.Errors++
|
|
}
|
|
summary.History = append(summary.History, AstraSamplingHistory{ID: run.ID, At: run.CreatedAt, Status: result.Status, ValidSamples: result.ValidSamples, Closest: result.Closest, Gap: result.Gap, UserInitiated: true})
|
|
continue
|
|
}
|
|
s := pelicans[group]
|
|
if s == nil {
|
|
continue
|
|
}
|
|
history := DetectionHistory{ID: run.ID, At: run.CreatedAt, Status: run.Status, Quality: run.Quality, UserInitiated: run.UserID > 0, HasHTML: artwork[run.ID]}
|
|
var metadata struct {
|
|
SourceURL string `json:"source_url"`
|
|
}
|
|
if common.UnmarshalJsonStr(run.Result, &metadata) == nil {
|
|
history.SourceURL = metadata.SourceURL
|
|
}
|
|
s.History = append(s.History, history)
|
|
switch run.Status {
|
|
case "pending", "running":
|
|
s.Running++
|
|
case "failed":
|
|
s.Failed++
|
|
default:
|
|
switch run.Quality {
|
|
case "normal":
|
|
s.Normal++
|
|
case "degraded":
|
|
s.Degraded++
|
|
default:
|
|
s.Unknown++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, s := range fingerprints {
|
|
if n := s.Matches + s.Mismatches; n > 0 {
|
|
rate := float64(s.Mismatches) / float64(n)
|
|
s.Rate = &rate
|
|
}
|
|
sort.SliceStable(s.History, func(i, j int) bool { return s.History[i].At > s.History[j].At })
|
|
if len(s.History) > 12 {
|
|
s.History = s.History[:12]
|
|
}
|
|
}
|
|
results := map[string]GroupDetectionSummary{}
|
|
for group, ids := range groups {
|
|
status, at := DetectionFallbackStatus(ids, evidence, now.Unix())
|
|
s := pelicans[group]
|
|
if s != nil {
|
|
sort.SliceStable(s.History, func(i, j int) bool { return s.History[i].At > s.History[j].At })
|
|
if len(s.History) > 12 {
|
|
s.History = s.History[:12]
|
|
}
|
|
}
|
|
results[group] = GroupDetectionSummary{Pelican: s, Status: status, At: at, ProbeHistory: GroupProbeHistory(ids, tasks)}
|
|
}
|
|
return results, nil
|
|
}
|