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.
259 lines
8.9 KiB
Go
259 lines
8.9 KiB
Go
package model
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"math"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/setting"
|
|
"github.com/QuantumNous/new-api/setting/config"
|
|
"github.com/QuantumNous/new-api/setting/operation_setting"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// RequestPolicySnapshot is the validated read model behind the request policy
|
|
// settings API. It is rebuilt from the option map on every option write, so
|
|
// the runtime globals updated by the same write never disagree with it.
|
|
type RequestPolicySnapshot struct {
|
|
Affinity operation_setting.ChannelAffinitySetting
|
|
RetryTimes int
|
|
RetryCodes []operation_setting.StatusCodeRange
|
|
DisableCodes []operation_setting.StatusCodeRange
|
|
DisableKeywords []string
|
|
CheckText bool
|
|
TextKeywords []string
|
|
AutoDisable bool
|
|
Options map[string]string
|
|
}
|
|
|
|
var requestPolicySnapshot atomic.Pointer[RequestPolicySnapshot]
|
|
var requestPolicyOptionMutex sync.Mutex
|
|
var requestPolicyDefaults = requestPolicyDefaultOptions()
|
|
|
|
func requestPolicyDefaultOptions() map[string]string {
|
|
defaults := make(map[string]string)
|
|
for prefix, value := range map[string]any{
|
|
"channel_affinity_setting.": operation_setting.GetChannelAffinitySetting(),
|
|
"monitor_setting.": operation_setting.GetMonitorSetting(),
|
|
} {
|
|
fields, err := config.ConfigToMap(value)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
for field, value := range fields {
|
|
defaults[prefix+field] = value
|
|
}
|
|
}
|
|
defaults["RetryTimes"] = strconv.Itoa(common.RetryTimes)
|
|
defaults["AutomaticRetryStatusCodes"] = operation_setting.AutomaticRetryStatusCodesToString()
|
|
defaults["AutomaticDisableStatusCodes"] = operation_setting.AutomaticDisableStatusCodesToString()
|
|
defaults["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString()
|
|
defaults["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled)
|
|
defaults["CheckSensitiveEnabled"] = strconv.FormatBool(setting.CheckSensitiveEnabled)
|
|
defaults["CheckSensitiveOnPromptEnabled"] = strconv.FormatBool(setting.CheckSensitiveOnPromptEnabled)
|
|
defaults["SensitiveWords"] = setting.SensitiveWordsToString()
|
|
defaults["AutomaticEnableChannelEnabled"] = strconv.FormatBool(common.AutomaticEnableChannelEnabled)
|
|
defaults["ChannelDisableThreshold"] = strconv.FormatFloat(common.ChannelDisableThreshold, 'f', -1, 64)
|
|
return defaults
|
|
}
|
|
|
|
func IsRequestPolicyOption(key string) bool {
|
|
if strings.HasPrefix(key, "channel_affinity_setting.") {
|
|
return true
|
|
}
|
|
switch key {
|
|
case "CheckSensitiveEnabled", "CheckSensitiveOnPromptEnabled", "SensitiveWords", "AutomaticEnableChannelEnabled", "ChannelDisableThreshold", "monitor_setting.auto_test_channel_enabled", "monitor_setting.auto_test_channel_minutes", "monitor_setting.channel_test_concurrency", "monitor_setting.channel_test_mode", "RetryTimes", "AutomaticRetryStatusCodes", "AutomaticDisableChannelEnabled", "AutomaticDisableStatusCodes", "AutomaticDisableKeywords":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func CurrentRequestPolicy() *RequestPolicySnapshot {
|
|
if snapshot := requestPolicySnapshot.Load(); snapshot != nil {
|
|
return snapshot
|
|
}
|
|
// Startup and test fixtures may not have loaded options yet.
|
|
snapshot, err := BuildRequestPolicy(nil)
|
|
if err != nil {
|
|
return &RequestPolicySnapshot{Options: map[string]string{}}
|
|
}
|
|
return snapshot
|
|
}
|
|
|
|
func BuildRequestPolicy(options map[string]string) (*RequestPolicySnapshot, error) {
|
|
defaults := requestPolicyDefaults
|
|
|
|
raw := make(map[string]string)
|
|
for key, value := range defaults {
|
|
if IsRequestPolicyOption(key) {
|
|
raw[key] = value
|
|
}
|
|
}
|
|
maps.Copy(raw, options)
|
|
snapshot := &RequestPolicySnapshot{Options: raw}
|
|
affinityFields := map[string]string{}
|
|
for key, value := range raw {
|
|
if field, ok := strings.CutPrefix(key, "channel_affinity_setting."); ok {
|
|
switch field {
|
|
case "enabled", "session_mode", "switch_on_success", "keep_on_channel_disabled", "max_entries", "default_ttl_seconds", "rules":
|
|
default:
|
|
return nil, fmt.Errorf("unknown affinity option: %s", key)
|
|
}
|
|
affinityFields[field] = value
|
|
}
|
|
}
|
|
// Decode through JSON so malformed scalar/array values cannot be silently ignored.
|
|
affinityJSON := map[string]json.RawMessage{}
|
|
for key, value := range affinityFields {
|
|
if key == "session_mode" {
|
|
encoded, _ := common.Marshal(value)
|
|
affinityJSON[key] = encoded
|
|
} else {
|
|
affinityJSON[key] = json.RawMessage(value)
|
|
}
|
|
}
|
|
encoded, err := common.Marshal(affinityJSON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := common.Unmarshal(encoded, &snapshot.Affinity); err != nil {
|
|
return nil, err
|
|
}
|
|
snapshot.RetryTimes, err = strconv.Atoi(raw["RetryTimes"])
|
|
if err != nil || snapshot.RetryTimes < 0 || snapshot.RetryTimes == math.MaxInt {
|
|
return nil, fmt.Errorf("retry times must be a non-negative integer with room for the initial attempt")
|
|
}
|
|
snapshot.AutoDisable, err = strconv.ParseBool(raw["AutomaticDisableChannelEnabled"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snapshot.RetryCodes, err = operation_setting.ParseHTTPStatusCodeRanges(raw["AutomaticRetryStatusCodes"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snapshot.DisableCodes, err = operation_setting.ParseHTTPStatusCodeRanges(raw["AutomaticDisableStatusCodes"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snapshot.DisableKeywords = strings.Split(raw["AutomaticDisableKeywords"], "\n")
|
|
for _, key := range []string{"CheckSensitiveEnabled", "CheckSensitiveOnPromptEnabled", "AutomaticEnableChannelEnabled", "monitor_setting.auto_test_channel_enabled"} {
|
|
if _, err := strconv.ParseBool(raw[key]); err != nil {
|
|
return nil, fmt.Errorf("invalid boolean: %s", key)
|
|
}
|
|
}
|
|
snapshot.CheckText = raw["CheckSensitiveEnabled"] == "true" && raw["CheckSensitiveOnPromptEnabled"] == "true"
|
|
for word := range strings.SplitSeq(raw["SensitiveWords"], "\n") {
|
|
if word = strings.TrimSpace(word); word != "" {
|
|
snapshot.TextKeywords = append(snapshot.TextKeywords, word)
|
|
}
|
|
}
|
|
if err := operation_setting.ValidateChannelTestConcurrency(raw["monitor_setting.channel_test_concurrency"]); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, key := range []string{"ChannelDisableThreshold", "monitor_setting.auto_test_channel_minutes"} {
|
|
if raw[key] == "" && key == "ChannelDisableThreshold" {
|
|
continue
|
|
}
|
|
value, err := strconv.ParseFloat(raw[key], 64)
|
|
if err != nil || value < 0 || math.IsNaN(value) || math.IsInf(value, 0) {
|
|
return nil, fmt.Errorf("invalid numeric value: %s", key)
|
|
}
|
|
}
|
|
switch raw["monitor_setting.channel_test_mode"] {
|
|
case "scheduled_all", "auto_ban_only", "passive_recovery":
|
|
default:
|
|
return nil, fmt.Errorf("invalid channel test mode")
|
|
}
|
|
|
|
if snapshot.Affinity.MaxEntries < 0 || snapshot.Affinity.DefaultTTLSeconds < 0 {
|
|
return nil, fmt.Errorf("invalid affinity cache limits")
|
|
}
|
|
switch snapshot.Affinity.SessionMode {
|
|
case "", "off", "prefer", "strict":
|
|
default:
|
|
return nil, fmt.Errorf("invalid global session mode")
|
|
}
|
|
for _, rule := range snapshot.Affinity.Rules {
|
|
switch rule.SessionMode {
|
|
case "", "inherit", "off", "prefer", "strict":
|
|
default:
|
|
return nil, fmt.Errorf("invalid session mode for rule %q", rule.Name)
|
|
}
|
|
patterns := append(append([]string{}, rule.ModelRegex...), rule.PathRegex...)
|
|
if rule.ValueRegex != "" {
|
|
patterns = append(patterns, rule.ValueRegex)
|
|
}
|
|
for _, pattern := range patterns {
|
|
if _, err := regexp.Compile(pattern); err != nil {
|
|
return nil, fmt.Errorf("rule %q: %w", rule.Name, err)
|
|
}
|
|
}
|
|
if rule.TTLSeconds < 0 {
|
|
return nil, fmt.Errorf("negative affinity TTL")
|
|
}
|
|
}
|
|
return snapshot, nil
|
|
}
|
|
|
|
func refreshRequestPolicySnapshot() error {
|
|
common.OptionMapRWMutex.RLock()
|
|
options := make(map[string]string)
|
|
for key, value := range common.OptionMap {
|
|
if IsRequestPolicyOption(key) {
|
|
options[key] = value
|
|
}
|
|
}
|
|
common.OptionMapRWMutex.RUnlock()
|
|
snapshot, err := BuildRequestPolicy(options)
|
|
if err == nil {
|
|
requestPolicySnapshot.Store(snapshot)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// UpdateRequestPolicyOptions validates the entire resulting policy before any
|
|
// write. Unknown JSON rule fields stay in the original option string.
|
|
func UpdateRequestPolicyOptions(values map[string]string) error {
|
|
requestPolicyOptionMutex.Lock()
|
|
defer requestPolicyOptionMutex.Unlock()
|
|
options := maps.Clone(CurrentRequestPolicy().Options)
|
|
for key, value := range values {
|
|
if !IsRequestPolicyOption(key) {
|
|
return fmt.Errorf("not a request policy option: %s", key)
|
|
}
|
|
options[key] = value
|
|
}
|
|
snapshot, err := BuildRequestPolicy(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := DB.Transaction(func(tx *gorm.DB) error {
|
|
for key, value := range values {
|
|
option := Option{Key: key}
|
|
if err := tx.FirstOrCreate(&option, Option{Key: key}).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Model(&option).Update("value", value).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
for key, value := range values {
|
|
if err := updateOptionMap(key, value); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
requestPolicySnapshot.Store(snapshot)
|
|
return nil
|
|
}
|