Files
dsfree2api/internal/upstream/errors.go
T

178 lines
4.5 KiB
Go

package upstream
import (
"errors"
"fmt"
"strings"
)
// UpstreamError is any failure talking to the deepseek.* sites.
type UpstreamError struct {
Msg string
ContentStarted bool
Cause error
}
func (e *UpstreamError) Error() string {
if e.Msg != "" {
return e.Msg
}
if e.Cause != nil {
return e.Cause.Error()
}
return "upstream failed"
}
func (e *UpstreamError) Unwrap() error { return e.Cause }
// QuotaExhaustedError means the site ran out of free quota for this session.
type QuotaExhaustedError struct{ UpstreamError }
// SessionVerificationError means Turnstile/nonce verification must redone.
type SessionVerificationError struct{ UpstreamError }
func errf(format string, a ...any) *UpstreamError {
return &UpstreamError{Msg: fmt.Sprintf(format, a...)}
}
func newQuota(msg string) *QuotaExhaustedError {
return &QuotaExhaustedError{UpstreamError: UpstreamError{Msg: msg}}
}
func newSession(msg string) *SessionVerificationError {
return &SessionVerificationError{UpstreamError: UpstreamError{Msg: msg}}
}
// AsUpstream unwraps any error into an *UpstreamError.
func AsUpstream(err error) *UpstreamError {
if err == nil {
return nil
}
var ue *UpstreamError
if errors.As(err, &ue) {
return ue
}
return &UpstreamError{Msg: err.Error(), Cause: err}
}
func contentStarted(err error) bool {
var ue *UpstreamError
if errors.As(err, &ue) {
return ue.ContentStarted
}
return false
}
func isQuota(err error) bool {
var q *QuotaExhaustedError
return errors.As(err, &q)
}
func isSession(err error) bool {
var s *SessionVerificationError
return errors.As(err, &s)
}
// isSessionPayload classifies upstream JSON payloads that indicate the
// Turnstile cookie / nonce has to be re-established. It deliberately matches
// only explicit markers so a randomly embedded "nonce" string in echoed
// request data cannot trigger a costly re-solve.
func isSessionPayload(payload map[string]any) bool {
var texts []string
for _, k := range []string{"error", "message", "code"} {
if v, ok := payload[k]; ok && v != nil {
texts = append(texts, stringify(v))
}
}
if data, ok := payload["data"].(map[string]any); ok {
for _, k := range []string{"code", "message", "error"} {
if v, ok := data[k]; ok && v != nil {
texts = append(texts, stringify(v))
}
}
}
if notice, ok := payload["quota_notice"].(map[string]any); ok {
if v, ok := notice["message"]; ok {
texts = append(texts, stringify(v))
}
}
text := strings.ToLower(strings.Join(texts, " "))
for _, m := range []string{
"deepseek_ts_required",
"ts_required",
"nonce_failure",
"session_expired",
"forbidden",
"unauthorized",
"csrf",
"verification_required",
"sicherheitspruefung",
} {
if strings.Contains(text, m) {
return true
}
}
if v, ok := payload["ts_required"]; ok {
switch t := v.(type) {
case bool:
return t
case string:
return strings.TrimSpace(t) != "" && t != "0" && !strings.EqualFold(t, "false")
default:
return true
}
}
return false
}
// isConfigProblem reports errors that retrying cannot fix: the site stopped
// serving the AIPKit chat page (it now answers with a redirect or HTML), or
// the config points at something that does not exist.
func isConfigProblem(err error) bool {
return strings.Contains(strings.ToLower(err.Error()), "nonce fetch failed")
}
// isCacheEmptyProblem matches the site's cache-message rejection that a
// dropped config or a cookie re-solve never fixes on the same site: the
// mirrors carry independent sessions, so callers fail over instead of
// burning refresh cycles.
func isCacheEmptyProblem(err error) bool {
return strings.Contains(err.Error(), "empty_data_to_cache")
}
// isQuotaPayload mirrors the German/English quota notices used upstream.
func isQuotaPayload(payload map[string]any) bool {
if _, ok := payload["quota_notice"]; ok {
return true
}
var texts []string
for _, k := range []string{"error", "message"} {
if v, ok := payload[k]; ok && v != nil {
texts = append(texts, stringify(v))
}
}
if notice, ok := payload["quota_notice"].(map[string]any); ok {
if v, ok := notice["message"]; ok {
texts = append(texts, stringify(v))
}
}
text := strings.ToLower(strings.Join(texts, " "))
if strings.Contains(text, "guthaben") || strings.Contains(text, "quota") {
return true
}
return strings.Contains(text, "token") && strings.Contains(text, "aufgebraucht")
}
func stringify(v any) string {
switch t := v.(type) {
case string:
return t
case float64:
return fmt.Sprintf("%v", t)
case bool:
return fmt.Sprintf("%v", t)
default:
return fmt.Sprintf("%v", t)
}
}