| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549 |
- package service
- import (
- "bufio"
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "io"
- "net/http"
- "strings"
- "sync"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
- "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
- "github.com/Wei-Shaw/sub2api/internal/util/logredact"
- "github.com/gin-gonic/gin"
- )
- type intelligentRunKey struct{}
- type intelligentRunContext struct {
- prompt string
- capture *intelligentCapture
- }
- // Only an empty selection receives the protocol default. Explicit choices
- // remain observable even when the upstream rejects the requested model.
- func resolveIntelligentTestModel(account *Account, configured string) string {
- model := strings.TrimSpace(configured)
- if account != nil && account.UsesOpenAICodexProtocol() && model == "" {
- return "gpt-5.3-codex"
- }
- return model
- }
- func intelligentContext(ctx context.Context) *intelligentRunContext {
- v, _ := ctx.Value(intelligentRunKey{}).(*intelligentRunContext)
- return v
- }
- func intelligentPrompt(ctx context.Context) string {
- if v := intelligentContext(ctx); v != nil {
- return v.prompt
- }
- return ""
- }
- func applyIntelligentPayloadPrompt(ctx context.Context, payload map[string]any) {
- prompt := intelligentPrompt(ctx)
- if prompt == "" {
- return
- }
- if _, ok := payload["input"]; ok {
- payload["input"] = []map[string]any{{"role": "user", "content": []map[string]any{{"type": "input_text", "text": prompt}}}}
- }
- if _, ok := payload["messages"]; ok {
- payload["messages"] = []map[string]any{{"role": "user", "content": []map[string]any{{"type": "text", "text": prompt}}}}
- if _, bounded := payload["max_tokens"]; bounded {
- payload["max_tokens"] = 8192
- }
- }
- }
- func intelligentAntigravityBody(ctx context.Context, body []byte) ([]byte, error) {
- var envelope map[string]any
- if err := json.Unmarshal(body, &envelope); err != nil {
- return nil, err
- }
- request, ok := envelope["request"].(map[string]any)
- if !ok {
- return nil, errors.New("unsupported Antigravity request envelope")
- }
- request["contents"] = []map[string]any{{"role": "user", "parts": []map[string]any{{"text": intelligentPrompt(ctx)}}}}
- cfg, ok := request["generationConfig"].(map[string]any)
- if !ok {
- cfg = map[string]any{}
- request["generationConfig"] = cfg
- }
- cfg["maxOutputTokens"] = 8192
- return json.Marshal(envelope)
- }
- func (s *AntigravityGatewayService) runIntelligentAntigravity(ctx context.Context, account *Account, token string, body []byte) (*http.Response, error) {
- base := resolveAntigravityForwardBaseURL(account)
- if base == "" {
- return nil, errors.New("no antigravity forward base url configured")
- }
- req, err := antigravity.NewAPIRequestWithURL(ctx, base, "streamGenerateContent", token, body)
- if err != nil {
- return nil, err
- }
- proxy := ""
- if account.Proxy != nil && account.ProxyID != nil {
- proxy = account.Proxy.URL()
- }
- resp, err := s.httpUpstream.Do(WithAccountTrafficRequest(req, account), proxy, account.ID, account.Concurrency)
- return captureIntelligentResponse(req, resp), err
- }
- // RunIntelligentTest uses the existing authenticated outbound protocol adapters.
- // A per-run service avoids mutating shared service state. Runtime health writes
- // are suppressed: an observation never changes account policy or scheduling.
- func (s *AccountTestService) RunIntelligentTest(ctx context.Context, r *IntelligentTestRecord) error {
- account, err := s.accountRepo.GetByID(ctx, r.AccountID)
- if err != nil {
- r.Status = "account_error"
- return errors.New("account unavailable")
- }
- r.AntiDegradation = account.AntiDegradationEnabled()
- if r.ConfigSnapshot == nil {
- return errors.New("missing configuration")
- }
- r.Model = resolveIntelligentTestModel(account, r.ConfigSnapshot.Model)
- if err := accountTestCooldown(ctx, account, r.Model, time.Now()); err != nil {
- return err
- }
- release, err := s.acquireTestAccountSlot(ctx, account)
- if err != nil {
- return err
- }
- defer release()
- if r.ConfigSnapshot == nil {
- return errors.New("missing configuration")
- }
- // Keep the requested model and the resolved execution model separately.
- r.Model = resolveIntelligentTestModel(account, r.ConfigSnapshot.Model)
- if err := validateIntelligentTextModel(account.GetMappedModel(r.Model)); err != nil {
- r.Status = "request_error"
- return err
- }
- r.ConfigSnapshot.Execution = ResolveProtectionRuntime(account, s.cfg, s.pluginManager)
- r.ConfigSnapshot.Execution.RequestedModel = r.ConfigSnapshot.Model
- r.ConfigSnapshot.Execution.PromptDigest = protectionPromptDigest(r.Input)
- r.ConfigSnapshot.Execution.Model = r.Model
- capture := &intelligentCapture{}
- capture.collectCredentialSecrets(account.Credentials)
- ctx = context.WithValue(ctx, intelligentRunKey{}, &intelligentRunContext{prompt: r.Input, capture: capture})
- runCtx, cancel := context.WithCancel(ctx)
- defer cancel()
- recorder := &intelligentSSEWriter{header: http.Header{}, cancel: cancel}
- c, _ := gin.CreateTestContext(recorder)
- c.Request = (&http.Request{Method: http.MethodPost, Header: http.Header{}}).WithContext(runCtx)
- readRepo := &intelligentReadOnlyAccountRepo{AccountRepository: s.accountRepo, capture: capture, snapshot: account}
- clone := NewAccountTestService(readRepo, s.geminiTokenProvider, s.claudeTokenProvider, s.grokTokenProvider, s.antigravityGatewayService, &intelligentHTTPUpstream{inner: s.httpUpstream, capture: capture}, s.cfg, s.tlsFPProfileService)
- clone.settingService = s.settingService
- clone.pluginManager = s.pluginManager
- clone.openaiGatewayService = s.openaiGatewayService
- clone.agentIdentityWS = s.agentIdentityWS
- clone.grokWSDialer = s.grokWSDialer
- prepared, err := readRepo.GetByID(ctx, r.AccountID)
- if err != nil {
- r.Status = "account_error"
- return errors.New("account unavailable")
- }
- // Adaptive connection diagnostics probe several protocols and concatenate
- // answers. Capability tests use its native chat route once for a fair result.
- if prepared.IsCNProvider() && prepared.GetAPIProtocol() == APIProtocolAdaptive {
- err = clone.testCNProviderChatCompletionsConnection(c, prepared, r.Model, r.Input)
- } else {
- err = clone.testAccountConnectionForAccount(c, prepared, r.Model, r.Input, AccountTestModeDefault, AccountTestOptions{})
- }
- textOutput, eventError, model, complete := parseIntelligentSSE(recorder.body.String())
- if capture.trafficWait != nil && capture.status == 0 {
- return &TestAdmissionWaitError{Until: time.Now().Add(capture.trafficWait.RetryAfter), Reason: capture.trafficWait.Reason}
- }
- if capture.body.Len() > 0 && !intelligentRawComplete(capture.body.String()) {
- complete = false
- }
- r.Result = capture.redact(textOutput)
- if model != "" {
- r.Model = model
- }
- r.ConfigSnapshot.Execution.Model = r.Model
- r.RawResponse = capture.redact(capture.body.String())
- r.RawTruncated = capture.truncated || recorder.truncated
- if r.RawResponse == "" {
- r.RawResponse = capture.redact(recorder.body.String())
- }
- r.ErrorMessage = capture.redact(eventError)
- if r.ErrorMessage == "" && err != nil {
- r.ErrorMessage = capture.redact(err.Error())
- }
- if len(r.Result) > 512<<10 {
- r.Result = r.Result[:512<<10]
- r.RawTruncated = true
- err = errors.New("test output exceeded capture limit")
- }
- if len(r.ErrorMessage) > 4096 {
- r.ErrorMessage = r.ErrorMessage[:4096]
- }
- r.Result = strings.ToValidUTF8(r.Result, "�")
- r.RawResponse = strings.ToValidUTF8(r.RawResponse, "�")
- r.ErrorMessage = strings.ToValidUTF8(r.ErrorMessage, "�")
- if err != nil || eventError != "" || !complete || strings.TrimSpace(r.Result) == "" || recorder.truncated || capture.truncated {
- if err == nil {
- err = errors.New("upstream did not return a complete, nonempty test result")
- }
- if r.ErrorMessage == "" {
- r.ErrorMessage = err.Error()
- }
- r.Status = classifyIntelligentError(capture.status, r.ErrorMessage)
- return err
- }
- return nil
- }
- // Some old connectivity adapters treat EOF as success. Capability scoring
- // requires a real terminal event and rejects token-budget truncation.
- func intelligentRawComplete(raw string) bool {
- complete, truncated := false, false
- var inspect func(map[string]any)
- inspect = func(v map[string]any) {
- if nested, ok := v["response"].(map[string]any); ok {
- inspect(nested)
- }
- kind, _ := v["type"].(string)
- if kind == "response.completed" || kind == "response.done" || kind == "message_stop" {
- complete = true
- }
- if status, _ := v["status"].(string); status == "incomplete" || status == "failed" {
- truncated = true
- }
- if reason, _ := v["stop_reason"].(string); reason != "" {
- if reason == "max_tokens" {
- truncated = true
- } else {
- complete = true
- }
- }
- if delta, ok := v["delta"].(map[string]any); ok {
- inspect(delta)
- }
- if choices, ok := v["choices"].([]any); ok {
- for _, item := range choices {
- if choice, ok := item.(map[string]any); ok {
- if reason, _ := choice["finish_reason"].(string); reason != "" {
- if reason == "length" || reason == "content_filter" {
- truncated = true
- } else {
- complete = true
- }
- }
- }
- }
- }
- if candidates, ok := v["candidates"].([]any); ok {
- for _, item := range candidates {
- if candidate, ok := item.(map[string]any); ok {
- if reason, _ := candidate["finishReason"].(string); reason != "" {
- if reason == "STOP" {
- complete = true
- } else {
- truncated = true
- }
- }
- }
- }
- }
- }
- var body map[string]any
- if json.Unmarshal([]byte(raw), &body) == nil {
- inspect(body)
- } else {
- scanner := bufio.NewScanner(strings.NewReader(raw))
- scanner.Buffer(make([]byte, 8192), 4<<20)
- for scanner.Scan() {
- line := strings.TrimSpace(scanner.Text())
- if !strings.HasPrefix(line, "data:") {
- continue
- }
- var event map[string]any
- if json.Unmarshal([]byte(strings.TrimSpace(strings.TrimPrefix(line, "data:"))), &event) == nil {
- inspect(event)
- }
- }
- }
- return complete && !truncated
- }
- func parseIntelligentSSE(raw string) (output, errorMessage, model string, complete bool) {
- var text strings.Builder
- scanner := bufio.NewScanner(strings.NewReader(raw))
- scanner.Buffer(make([]byte, 8192), 1<<20)
- for scanner.Scan() {
- line := scanner.Text()
- if !strings.HasPrefix(line, "data:") {
- continue
- }
- var e TestEvent
- if json.Unmarshal([]byte(strings.TrimSpace(strings.TrimPrefix(line, "data:"))), &e) != nil {
- continue
- }
- switch e.Type {
- case "content":
- text.WriteString(e.Text)
- case "error":
- errorMessage = e.Error
- case "test_start":
- model = e.Model
- case "test_complete":
- complete = e.Success
- }
- }
- return text.String(), errorMessage, model, complete
- }
- func classifyIntelligentError(code int, message string) string {
- lower := strings.ToLower(message)
- switch {
- case code == 429 || strings.Contains(lower, "rate limit") || strings.Contains(lower, "限流"):
- return "rate_limited"
- case code == 401 || code == 403:
- return "account_error"
- case code >= 500:
- return "network_error"
- case strings.Contains(lower, "max_tokens") || strings.Contains(lower, "max_output_tokens") || strings.Contains(lower, "max_completion_tokens"):
- return "request_error"
- case code == 404 || strings.Contains(lower, "model not found") || strings.Contains(lower, "unsupported model"):
- return "model_error"
- case code == 400 || code == 422:
- return "request_error"
- case strings.Contains(lower, "expired token") || strings.Contains(lower, "access token") || strings.Contains(lower, "credential") || strings.Contains(lower, "api key") || strings.Contains(lower, "account not found") || strings.Contains(message, "账号认证"):
- return "account_error"
- case strings.Contains(lower, "timeout") || strings.Contains(lower, "connection") || strings.Contains(lower, "deadline"):
- return "network_error"
- default:
- return "failed"
- }
- }
- func validateIntelligentTextModel(model string) error {
- lower := strings.ToLower(strings.TrimSpace(model))
- if isOpenAIImageModel(model) || strings.HasPrefix(lower, "grok-imagine") || strings.HasPrefix(lower, "tts-") || strings.HasPrefix(lower, "whisper") || strings.HasPrefix(lower, "sora") {
- return errors.New("当前题目需要文本生成模型;鹈鹕测试通过 SVG 源码绘图,请勿选择原生图片、音频或视频模型")
- }
- return nil
- }
- type intelligentSSEWriter struct {
- header http.Header
- body bytes.Buffer
- cancel context.CancelFunc
- truncated bool
- }
- func (w *intelligentSSEWriter) Header() http.Header { return w.header }
- func (w *intelligentSSEWriter) WriteHeader(int) {}
- func (w *intelligentSSEWriter) Flush() {}
- func (w *intelligentSSEWriter) Write(p []byte) (int, error) {
- if w.body.Len()+len(p) > 2<<20 {
- w.truncated = true
- w.cancel()
- return 0, errors.New("test output too large")
- }
- return w.body.Write(p)
- }
- type intelligentCapture struct {
- trafficWait *AccountTrafficLimitError
- mu sync.Mutex
- body bytes.Buffer
- status int
- truncated bool
- secrets []string
- }
- func (c *intelligentCapture) Write(p []byte) (int, error) {
- c.mu.Lock()
- defer c.mu.Unlock()
- n := len(p)
- available := (1 << 20) - c.body.Len()
- if available < len(p) {
- c.truncated = true
- if available > 0 {
- _, _ = c.body.Write(p[:available])
- }
- } else {
- _, _ = c.body.Write(p)
- }
- return n, nil
- }
- func (c *intelligentCapture) collectCredentialSecrets(values map[string]any) {
- for key, value := range values {
- if nested, ok := value.(map[string]any); ok {
- c.collectCredentialSecrets(nested)
- }
- lower := strings.ToLower(key)
- if strings.Contains(lower, "token") || strings.Contains(lower, "secret") || strings.Contains(lower, "password") || strings.Contains(lower, "key") || strings.Contains(lower, "cookie") {
- if text, ok := value.(string); ok && len(text) >= 6 {
- c.secrets = append(c.secrets, text)
- }
- }
- }
- }
- func (c *intelligentCapture) redact(raw string) string {
- for _, secret := range c.secrets {
- raw = strings.ReplaceAll(raw, secret, "[REDACTED]")
- }
- return logredact.RedactText(raw)
- }
- type intelligentCaptureBody struct {
- io.Reader
- closer io.Closer
- }
- func (b *intelligentCaptureBody) Close() error { return b.closer.Close() }
- func (c *intelligentCapture) response(req *http.Request, resp *http.Response) *http.Response {
- if resp == nil {
- return resp
- }
- c.status = resp.StatusCode
- for _, key := range []string{"Authorization", "X-Api-Key", "X-Goog-Api-Key", "Cookie"} {
- v := req.Header.Get(key)
- if len(v) > 6 {
- c.secrets = append(c.secrets, v)
- if strings.HasPrefix(v, "Bearer ") {
- c.secrets = append(c.secrets, strings.TrimPrefix(v, "Bearer "))
- }
- }
- }
- if resp.Body != nil {
- if _, ok := resp.Body.(*intelligentCaptureBody); !ok {
- resp.Body = &intelligentCaptureBody{Reader: io.TeeReader(io.LimitReader(resp.Body, 4<<20), c), closer: resp.Body}
- }
- }
- return resp
- }
- type intelligentHTTPUpstream struct {
- inner HTTPUpstream
- capture *intelligentCapture
- }
- func (h *intelligentHTTPUpstream) Do(req *http.Request, proxy string, id int64, concurrency int) (*http.Response, error) {
- resp, err := h.inner.Do(req, proxy, id, concurrency)
- var wait *AccountTrafficLimitError
- if errors.As(err, &wait) {
- h.capture.trafficWait = wait
- }
- return h.capture.response(req, resp), err
- }
- func (h *intelligentHTTPUpstream) DoWithTLS(req *http.Request, proxy string, id int64, concurrency int, p *tlsfingerprint.Profile) (*http.Response, error) {
- resp, err := h.inner.DoWithTLS(req, proxy, id, concurrency, p)
- var wait *AccountTrafficLimitError
- if errors.As(err, &wait) {
- h.capture.trafficWait = wait
- }
- return h.capture.response(req, resp), err
- }
- func (h *intelligentHTTPUpstream) AccountTrafficController() *AccountTrafficService {
- return accountTrafficController(h.inner)
- }
- func captureIntelligentResponse(req *http.Request, resp *http.Response) *http.Response {
- if v := intelligentContext(req.Context()); v != nil {
- return v.capture.response(req, resp)
- }
- return resp
- }
- type intelligentReadOnlyAccountRepo struct {
- AccountRepository
- capture *intelligentCapture
- snapshot *Account
- }
- func (r *intelligentReadOnlyAccountRepo) GetByID(ctx context.Context, id int64) (*Account, error) {
- a := r.snapshot
- if a == nil || a.ID != id {
- var err error
- a, err = r.AccountRepository.GetByID(ctx, id)
- if err != nil {
- return nil, err
- }
- }
- copy := *a
- raw, _ := json.Marshal(a.Extra)
- copy.Extra = nil
- _ = json.Unmarshal(raw, ©.Extra)
- raw, _ = json.Marshal(a.Credentials)
- copy.Credentials = nil
- _ = json.Unmarshal(raw, ©.Credentials)
- r.capture.collectCredentialSecrets(copy.Credentials)
- return ©, nil
- }
- func (r *intelligentReadOnlyAccountRepo) SetError(context.Context, int64, string) error { return nil }
- func (r *intelligentReadOnlyAccountRepo) ClearError(context.Context, int64) error { return nil }
- func (r *intelligentReadOnlyAccountRepo) SetRateLimited(context.Context, int64, time.Time) error {
- return nil
- }
- func (r *intelligentReadOnlyAccountRepo) ClearRateLimit(context.Context, int64) error { return nil }
- func (r *intelligentReadOnlyAccountRepo) SetModelRateLimit(context.Context, int64, string, time.Time, ...string) error {
- return nil
- }
- func (r *intelligentReadOnlyAccountRepo) SetTempUnschedulable(context.Context, int64, time.Time, string) error {
- return nil
- }
- func (r *intelligentReadOnlyAccountRepo) SetOverloaded(context.Context, int64, time.Time) error {
- return nil
- }
- func (r *intelligentReadOnlyAccountRepo) SetSchedulable(context.Context, int64, bool) error {
- return nil
- }
- func (r *intelligentReadOnlyAccountRepo) UpdateExtra(context.Context, int64, map[string]any) error {
- return nil
- }
- func (r *intelligentReadOnlyAccountRepo) Update(context.Context, *Account) error {
- return errors.New("capability tests do not modify account settings; refresh account authentication separately")
- }
- func (r *intelligentReadOnlyAccountRepo) Create(context.Context, *Account) error {
- return errors.New("capability tests cannot create accounts")
- }
- func (r *intelligentReadOnlyAccountRepo) Delete(context.Context, int64) error {
- return errors.New("capability tests cannot delete accounts")
- }
- func (r *intelligentReadOnlyAccountRepo) UpdateLastUsed(context.Context, int64) error { return nil }
- func (r *intelligentReadOnlyAccountRepo) BatchUpdateLastUsed(context.Context, map[int64]time.Time) error {
- return nil
- }
- func (r *intelligentReadOnlyAccountRepo) AutoPauseExpiredAccounts(context.Context, time.Time) (int64, error) {
- return 0, nil
- }
- func (r *intelligentReadOnlyAccountRepo) BindGroups(context.Context, int64, []int64) error {
- return errors.New("capability tests cannot change account groups")
- }
- func (r *intelligentReadOnlyAccountRepo) ClearTempUnschedulable(context.Context, int64) error {
- return nil
- }
- func (r *intelligentReadOnlyAccountRepo) ClearAntigravityQuotaScopes(context.Context, int64) error {
- return nil
- }
- func (r *intelligentReadOnlyAccountRepo) ClearModelRateLimits(context.Context, int64) error {
- return nil
- }
- func (r *intelligentReadOnlyAccountRepo) UpdateSessionWindow(context.Context, int64, *time.Time, *time.Time, string) error {
- return nil
- }
- func (r *intelligentReadOnlyAccountRepo) UpdateSessionWindowEnd(context.Context, int64, time.Time) error {
- return nil
- }
- func (r *intelligentReadOnlyAccountRepo) BulkUpdate(context.Context, []int64, AccountBulkUpdate) (int64, error) {
- return 0, errors.New("capability tests cannot bulk edit account settings")
- }
- func (r *intelligentReadOnlyAccountRepo) IncrementQuotaUsed(context.Context, int64, float64) error {
- return nil
- }
- func (r *intelligentReadOnlyAccountRepo) ResetQuotaUsedAndClearRateLimitCooldown(context.Context, int64) error {
- return nil
- }
- func (r *intelligentReadOnlyAccountRepo) RevertProxyFallback(context.Context, int64) error {
- return errors.New("capability tests cannot change account proxy settings")
- }
- var _ HTTPUpstream = (*intelligentHTTPUpstream)(nil)
|