Files
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

130 lines
4.8 KiB
Go

package db
import (
"encoding/json"
"time"
)
// LLM retry policy: the global "attempts + interval" configuration for the five retry
// layers; see the LLM retry design doc.
// Stored as one JSON value in the settings table — it is one machine-wide runtime knob,
// not worth its own table; reads fall back to built-in defaults, so a missing key
// (fresh database / never configured) behaves exactly like the hard-coded-constant era.
const settingLLMRetryPolicy = "llm_retry_policy"
// RetryRule is one layer's knob pair. The zero value means "unset":
//
// Attempts 0 = use the built-in default attempts; -1 = disable this layer's retry; >0 = use this value
// IntervalMS 0 = keep this layer's original interval policy (usually exponential backoff); >0 = use this fixed millisecond interval
//
// -1 means "explicitly off" rather than "0 attempts", because 0 is already taken by "unset".
type RetryRule struct {
Attempts int `json:"attempts"`
IntervalMS int `json:"interval_ms"`
}
// Interval returns the configured fixed interval, or 0 when unset (caller keeps
// its own default ladder).
func (r RetryRule) Interval() time.Duration {
if r.IntervalMS <= 0 {
return 0
}
return time.Duration(r.IntervalMS) * time.Millisecond
}
// Or returns the rule with each unset field filled in from fallback. Used to
// layer a profile override on top of the global policy field by field, so a
// profile that only pins the interval still inherits the global count.
func (r RetryRule) Or(fallback RetryRule) RetryRule {
if r.Attempts == 0 {
r.Attempts = fallback.Attempts
}
if r.IntervalMS == 0 {
r.IntervalMS = fallback.IntervalMS
}
return r
}
// retry knob bounds. A count above the cap turns a blip into a token bonfire;
// an interval above an hour outlives any transient failure worth waiting out.
const (
maxRetryAttempts = 20
maxRetryIntervalMS = 3600_000 // 1h
)
// Clamped returns the rule with out-of-range values pulled back into the sane
// band (attempts within [-1, 20], interval within [0, 1h]).
func (r RetryRule) Clamped() RetryRule {
if r.Attempts < -1 {
r.Attempts = -1
}
if r.Attempts > maxRetryAttempts {
r.Attempts = maxRetryAttempts
}
if r.IntervalMS < 0 {
r.IntervalMS = 0
}
if r.IntervalMS > maxRetryIntervalMS {
r.IntervalMS = maxRetryIntervalMS
}
return r
}
// Clamped bounds a profile's override the same way the global policy is bounded,
// so a hand-crafted API payload can't land a value the CHECK constraint rejects.
func (o RetryOverride) Clamped() RetryOverride {
o.Connect, o.Empty, o.Stream = o.Connect.Clamped(), o.Empty.Clamped(), o.Stream.Clamped()
return o
}
// LLMRetryPolicy holds the five-layer retry configuration. Connect/Empty/Stream are the
// per-request layers (a profile may override them, see LLMProfile.Retry);
// Breaker and Intent are process-wide by nature and live only here.
type LLMRetryPolicy struct {
// Connect: SDK connect retry (connection reset/timeout/429/5xx, before the stream starts). Default 3 attempts, exponential backoff.
Connect RetryRule `json:"connect"`
// Empty: SDK empty-response retry (completed but no content block, openai format only). Default 2 attempts, exponential backoff.
Empty RetryRule `json:"empty"`
// Stream: same-provider safe-window retry (replay of a broken stream before output was delivered). Default 2 attempts, exponential from 0.5s (capped at 4s).
Stream RetryRule `json:"stream"`
// Breaker: rotation breaker. Attempts=consecutive transient failures before tripping (default 3; -1=never trip on
// transient failures; hard failures like insufficient balance/invalid key still trip immediately); IntervalMS=fixed cooldown (0=default 1/5/30min ladder).
Breaker RetryRule `json:"breaker"`
// Intent: full-intent rerun after a worker ends with model_error. Default 2 attempts, fixed 3s.
Intent RetryRule `json:"intent"`
}
// Clamped returns the policy with every rule clamped.
func (p LLMRetryPolicy) Clamped() LLMRetryPolicy {
p.Connect, p.Empty, p.Stream = p.Connect.Clamped(), p.Empty.Clamped(), p.Stream.Clamped()
p.Breaker, p.Intent = p.Breaker.Clamped(), p.Intent.Clamped()
return p
}
// LLMRetryPolicy reads the global retry policy. A missing or unparseable value
// yields the zero policy — i.e. every layer on its built-in default.
func (d *DB) LLMRetryPolicy() LLMRetryPolicy {
var p LLMRetryPolicy
if d == nil {
return p
}
raw, ok, err := d.GetSetting(settingLLMRetryPolicy)
if err != nil || !ok || raw == "" {
return p
}
if err := json.Unmarshal([]byte(raw), &p); err != nil {
return LLMRetryPolicy{}
}
return p.Clamped()
}
// SetLLMRetryPolicy persists the global retry policy (values are clamped first).
func (d *DB) SetLLMRetryPolicy(p LLMRetryPolicy) error {
raw, err := json.Marshal(p.Clamped())
if err != nil {
return err
}
return d.SetSetting(settingLLMRetryPolicy, string(raw))
}