Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backend/internal/service/account_test_prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ func accountTextTestOutputLimit(prompts ...string) int {
}

const accountTestPromptContextKey = "account_test_user_prompt"
const accountTestScheduledDefaultsContextKey = "account_test_scheduled_defaults"

func resolveAntigravityTestPrompt(prompts ...string) string {
if len(prompts) == 0 {
return "."
}
return resolveAccountTestPrompt(prompts...)
}

func ValidateAccountTextTestPrompt(prompt, model, mode string) error {
switch strings.ToLower(strings.TrimSpace(mode)) {
Expand Down
31 changes: 31 additions & 0 deletions backend/internal/service/account_test_prompt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,37 @@ func TestAccountTestPromptBuildersPreserveUserText(t *testing.T) {
}
}

func TestAccountTestPromptAntigravityScheduledDefaults(t *testing.T) {
svc := &AntigravityGatewayService{}
for _, tc := range []struct {
name, model string
build func(string, string, ...string) ([]byte, error)
}{
{"gemini", "gemini-3.1-pro", svc.buildGeminiTestRequest},
{"claude", "claude-sonnet-4-6", svc.buildClaudeTestRequest},
} {
t.Run(tc.name, func(t *testing.T) {
for _, input := range []struct {
name string
prompts []string
want string
limit int64
}{
{"scheduled", nil, ".", 1},
{"blank manual", []string{""}, "hi", 1},
{"custom manual", []string{"custom question"}, "custom question", 1024},
} {
t.Run(input.name, func(t *testing.T) {
raw, err := tc.build("project", tc.model, input.prompts...)
require.NoError(t, err)
require.Equal(t, input.want, gjson.GetBytes(raw, "request.contents.0.parts.0.text").String())
require.Equal(t, input.limit, gjson.GetBytes(raw, "request.generationConfig.maxOutputTokens").Int())
})
}
})
}
}

func TestAccountTestPromptAdaptiveAndOpenCodeFinalRequests(t *testing.T) {
prompt := " preserve 用户原文\n"
account := adaptiveCNAccountTestAccount(991, PlatformDeepseek)
Expand Down
13 changes: 11 additions & 2 deletions backend/internal/service/account_test_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2495,7 +2495,11 @@ func (s *AccountTestService) testAntigravityAccountConnection(c *gin.Context, ac
s.sendEvent(c, TestEvent{Type: "test_start", Model: testModelID})

// 调用 AntigravityGatewayService.TestConnection(复用协议转换逻辑)
result, err := s.antigravityGatewayService.TestConnection(ctx, account, testModelID, c.GetString(accountTestPromptContextKey))
prompts := []string{c.GetString(accountTestPromptContextKey)}
if c.GetBool(accountTestScheduledDefaultsContextKey) {
prompts = nil
}
result, err := s.antigravityGatewayService.TestConnection(ctx, account, testModelID, prompts...)
if err != nil {
return s.sendErrorAndEnd(c, err.Error())
}
Expand Down Expand Up @@ -3372,7 +3376,12 @@ func (s *AccountTestService) runTestBackground(ctx context.Context, accountID in
ginCtx, _ := gin.CreateTestContext(httptest.NewRecorder())
ginCtx.Request = (&http.Request{}).WithContext(ctx)
ginCtx.Set("account_test_event_collector", collector)
testErr := s.TestAccountConnection(ginCtx, accountID, modelID, resolveAccountTestPrompt(prompts...), AccountTestModeDefault, AccountTestOptions{requireSupportedModel: requireSupported})
ginCtx.Set(accountTestScheduledDefaultsContextKey, !requireSupported)
prompt := ""
if len(prompts) > 0 {
prompt = prompts[0]
}
testErr := s.TestAccountConnection(ginCtx, accountID, modelID, prompt, AccountTestModeDefault, AccountTestOptions{requireSupportedModel: requireSupported})
if testErr == nil {
testErr = ctx.Err()
}
Expand Down
4 changes: 2 additions & 2 deletions backend/internal/service/antigravity_gateway_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ func (s *AntigravityGatewayService) buildGeminiTestRequest(projectID, model stri
{
"role": "user",
"parts": []map[string]any{
{"text": resolveAccountTestPrompt(prompts...)},
{"text": resolveAntigravityTestPrompt(prompts...)},
},
},
},
Expand All @@ -478,7 +478,7 @@ func (s *AntigravityGatewayService) buildGeminiTestRequest(projectID, model stri
// buildClaudeTestRequest 构建 Claude 格式测试请求并转换为 Gemini 格式
// 使用最小 token 消耗:输入 "." + MaxTokens: 1
func (s *AntigravityGatewayService) buildClaudeTestRequest(projectID, mappedModel string, prompts ...string) ([]byte, error) {
content, err := json.Marshal(resolveAccountTestPrompt(prompts...))
content, err := json.Marshal(resolveAntigravityTestPrompt(prompts...))
if err != nil {
return nil, err
}
Expand Down
2 changes: 2 additions & 0 deletions backend/internal/service/codex_ticket_lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ func CodexTicketFailure(code string) CodexTicketResult {
"ticket_proxy_protocol": "代理协议握手失败,请核对HTTP或SOCKS协议",
"ticket_proxy_dns": "代理主机DNS解析失败",
"ticket_proxy_connect": "代理连接被拒绝或CONNECT失败",
"ticket_proxy_eof": "连接被对端提前关闭(EOF)",
"ticket_proxy_reset": "连接被对端重置",
"ticket_proxy_tls": "代理TLS证书验证失败,请测试代理连接",
}
message, ok := messages[code]
Expand Down
23 changes: 23 additions & 0 deletions backend/internal/service/codex_ticket_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptrace"
Expand Down Expand Up @@ -42,6 +43,7 @@ type CodexTicketProxyStage struct {
}

type CodexTicketProxyTestResult struct {
FailureDetail string `json:"failure_detail,omitempty"`
Success bool `json:"success"`
NetworkReachable bool `json:"network_reachable"`
Protocol string `json:"protocol"`
Expand Down Expand Up @@ -327,6 +329,7 @@ func (s *OpenAIGatewayService) prepareCodexTicketProxy(ctx context.Context, raw
if err != nil {
result.Code = codexTicketTransportFailureCode(err)
result.Message = CodexTicketFailure(result.Code).Message
result.FailureDetail = codexTicketProxyFailureDetail(err, normal)
if strings.HasPrefix(u.Scheme, "socks") && errors.Is(err, context.DeadlineExceeded) {
result.ProtocolSuggestion = "尝试确认该端口是否为HTTP CONNECT代理"
}
Expand Down Expand Up @@ -382,6 +385,7 @@ func (s *OpenAIGatewayService) TestCodexTicketProxy(ctx context.Context, raw str
result.Success = false
result.Code = codexTicketTransportFailureCode(followErr)
result.Message = CodexTicketFailure(result.Code).Message
result.FailureDetail = codexTicketProxyFailureDetail(followErr, normalized)
result.Stages = append(result.Stages, CodexTicketProxyStage{Name: "pinned_connection", Success: false, DurationMS: time.Since(started).Milliseconds(), Message: result.Message})
return result, nil
}
Expand Down Expand Up @@ -428,9 +432,28 @@ func codexTicketTransportFailureCode(err error) string {
if strings.Contains(t, "connection refused") || strings.Contains(t, "connect rejected") {
return "ticket_proxy_connect"
}
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
return "ticket_proxy_eof"
}
if strings.Contains(t, "connection reset") || strings.Contains(t, "forcibly closed") {
return "ticket_proxy_reset"
}
return "ticket_transport"
}

func codexTicketProxyFailureDetail(err error, proxyURL string) string {
secrets := []string{proxyURL}
if parsed, parseErr := url.Parse(proxyURL); parseErr == nil && parsed.User != nil {
password, _ := parsed.User.Password()
secrets = append(secrets, parsed.User.Username(), password)
}
text := []rune(codexTicketSafeUpstreamMessage(err.Error(), secrets...))
if len(text) > 256 {
text = text[:256]
}
return string(text)
}

var ticketSensitiveValuePattern = regexp.MustCompile(`(?i)(?:bearer\s+\S+|(?:https?|socks5h?)://\S+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|sk-[A-Za-z0-9_-]+|gAAAAA[A-Za-z0-9_=-]+)`)

func codexTicketSafeUpstreamMessage(raw string, secretValues ...string) string {
Expand Down
13 changes: 13 additions & 0 deletions backend/internal/service/codex_ticket_proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"errors"
"io"
"math/big"
"net"
Expand Down Expand Up @@ -196,3 +197,15 @@ func TestCodexTicketProxyAuthenticationFailureAndErrorRedaction(t *testing.T) {
require.NotContains(t, msg, "secret-value")
require.False(t, strings.Contains(msg, "gAAAAA"))
}

func TestCodexTicketProxyDisconnectCauseIsRedacted(t *testing.T) {
proxyURL := "http://diagnostic-user:diagnostic-password@proxy.example.com:8080"
err := &url.Error{Op: "Head", URL: proxyURL, Err: io.EOF}
require.Equal(t, "ticket_proxy_eof", codexTicketTransportFailureCode(err))
detail := codexTicketProxyFailureDetail(err, proxyURL)
require.Contains(t, detail, "EOF")
require.NotContains(t, detail, "diagnostic-user")
require.NotContains(t, detail, "diagnostic-password")
require.Equal(t, "ticket_proxy_reset", codexTicketTransportFailureCode(errors.New("read: connection reset by peer")))
require.LessOrEqual(t, len([]rune(codexTicketProxyFailureDetail(errors.New(strings.Repeat("failure ", 100)), proxyURL))), 256)
}
1 change: 1 addition & 0 deletions frontend/src/api/admin/codexTickets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface TicketResult {
duration_ms?: number; expires_at?: string; fingerprint?: string
}
export interface ProxyTestResult {
failure_detail?: string
success: boolean; network_reachable: boolean; protocol: string; http_status?: number; code: string; message: string
stages: Array<{ name: string; success: boolean; duration_ms: number; message?: string }>
certificate_trust?: string; certificate_fingerprint?: string; protocol_suggestion?: string
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/admin/CodexTicketProxyEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<p v-if="error" role="alert" class="text-red-600 text-sm">{{ error }}</p>
<div v-if="result" role="status" class="space-y-1 border p-3 text-sm">
<p :class="result.success ? 'text-emerald-600' : 'text-amber-600'">{{ result.message }}</p>
<p v-if="result.failure_detail" class="break-words text-red-600">{{ result.failure_detail }}</p>
<p v-for="(stage, index) in result.stages" :key="index">{{ t('admin.accounts.ticketProxy.stages.' + stage.name) }} · {{ stage.success ? '✓' : '✗' }} {{ stage.message }} {{ stage.duration_ms }} ms</p>
<p v-if="result.certificate_trust">{{ t('admin.accounts.ticketProxy.trust') }}: {{ result.certificate_trust }} {{ result.certificate_fingerprint }}</p>
<p v-if="result.protocol_suggestion">{{ result.protocol_suggestion }}</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,11 @@ describe('draft ticket proxy editor', () => {
expect(wrapper.get('[role="alert"]').text()).toContain('duplicate_field')
wrapper.unmount()
})
it('shows the safe transport cause supplied by the server', async () => {
const wrapper = mount(CodexTicketProxyEditor, { props: { modelValue: 'http://proxy.example.com:8080' } })
mocks.testProxy.mockResolvedValue({ success: false, network_reachable: false, code: 'ticket_proxy_eof', message: 'Connection closed', failure_detail: 'Head [redacted]: EOF', stages: [] })
await wrapper.findAll('button').find(b => b.text().endsWith('.test'))!.trigger('click'); await flushPromises()
expect(wrapper.get('[role="status"]').text()).toContain('Head [redacted]: EOF')
wrapper.unmount()
})
})
Loading