From 08e994ad86b790989a7d2f328f5ecb5eaeed2926 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Thu, 16 Jul 2026 23:21:58 +0800 Subject: [PATCH 1/7] fix(frontend): lazy-load Stripe payment dependency --- .../payment/StripePaymentInline.vue | 2 +- frontend/src/views/user/StripePaymentView.vue | 2 +- frontend/src/views/user/StripePopupView.vue | 2 +- .../user/__tests__/StripePaymentView.spec.ts | 2 +- .../user/__tests__/stripeLazyLoading.spec.ts | 33 +++++++++++++++++++ frontend/vite.config.ts | 5 +++ 6 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 frontend/src/views/user/__tests__/stripeLazyLoading.spec.ts diff --git a/frontend/src/components/payment/StripePaymentInline.vue b/frontend/src/components/payment/StripePaymentInline.vue index 65f5bb18c7..48b0748e74 100644 --- a/frontend/src/components/payment/StripePaymentInline.vue +++ b/frontend/src/components/payment/StripePaymentInline.vue @@ -111,7 +111,7 @@ let elementsInstance: StripeElements | null = null onMounted(async () => { try { - const { loadStripe } = await import('@stripe/stripe-js') + const { loadStripe } = await import('@stripe/stripe-js/pure') const stripe = await loadStripe(props.publishableKey) if (!stripe) { initError.value = t('payment.stripeLoadFailed'); return } diff --git a/frontend/src/views/user/StripePaymentView.vue b/frontend/src/views/user/StripePaymentView.vue index 56a5bc2b66..840eea912a 100644 --- a/frontend/src/views/user/StripePaymentView.vue +++ b/frontend/src/views/user/StripePaymentView.vue @@ -165,7 +165,7 @@ onMounted(async () => { const publishableKey = paymentStore.config?.stripe_publishable_key if (!publishableKey) { initError.value = t('payment.stripeNotConfigured'); return } - const { loadStripe } = await import('@stripe/stripe-js') + const { loadStripe } = await import('@stripe/stripe-js/pure') const stripe = await loadStripe(publishableKey) if (!stripe) { initError.value = t('payment.stripeLoadFailed'); return } diff --git a/frontend/src/views/user/StripePopupView.vue b/frontend/src/views/user/StripePopupView.vue index 0e852e5206..0c3629c546 100644 --- a/frontend/src/views/user/StripePopupView.vue +++ b/frontend/src/views/user/StripePopupView.vue @@ -137,7 +137,7 @@ async function initStripe(clientSecret: string, publishableKey: string) { return } try { - const { loadStripe } = await import('@stripe/stripe-js') + const { loadStripe } = await import('@stripe/stripe-js/pure') const stripe = await loadStripe(publishableKey) if (!stripe) { error.value = t('payment.stripeLoadFailed'); return } diff --git a/frontend/src/views/user/__tests__/StripePaymentView.spec.ts b/frontend/src/views/user/__tests__/StripePaymentView.spec.ts index 1d53c49948..9b672048b8 100644 --- a/frontend/src/views/user/__tests__/StripePaymentView.spec.ts +++ b/frontend/src/views/user/__tests__/StripePaymentView.spec.ts @@ -56,7 +56,7 @@ vi.mock('@/api/payment', () => ({ }, })) -vi.mock('@stripe/stripe-js', () => ({ +vi.mock('@stripe/stripe-js/pure', () => ({ loadStripe, })) diff --git a/frontend/src/views/user/__tests__/stripeLazyLoading.spec.ts b/frontend/src/views/user/__tests__/stripeLazyLoading.spec.ts new file mode 100644 index 0000000000..be1c1f59b2 --- /dev/null +++ b/frontend/src/views/user/__tests__/stripeLazyLoading.spec.ts @@ -0,0 +1,33 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +const frontendRoot = resolve(__dirname, '../../../..') +const stripeConsumers = [ + 'src/views/user/StripePaymentView.vue', + 'src/views/user/StripePopupView.vue', + 'src/components/payment/StripePaymentInline.vue', +] + +function readFrontendFile(path: string): string { + return readFileSync(resolve(frontendRoot, path), 'utf8') +} + +describe('Stripe lazy-loading contract', () => { + it.each(stripeConsumers)('%s uses the side-effect-free Stripe loader', (path) => { + const source = readFrontendFile(path) + + expect(source).toContain("await import('@stripe/stripe-js/pure')") + expect(source).not.toMatch(/await import\(['"]@stripe\/stripe-js['"]\)/) + }) + + it('keeps Stripe out of the shared vendor chunk', () => { + const viteConfig = readFrontendFile('vite.config.ts') + const stripeRule = viteConfig.indexOf("id.includes('/@stripe/stripe-js/')") + const miscFallback = viteConfig.indexOf("return 'vendor-misc'") + + expect(stripeRule).toBeGreaterThan(-1) + expect(viteConfig.slice(stripeRule, miscFallback)).toContain("return 'vendor-stripe'") + expect(stripeRule).toBeLessThan(miscFallback) + }) +}) diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 3877070453..303a4bf77c 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -96,6 +96,11 @@ export default defineConfig(({ mode }) => { return 'vendor-i18n' } + // Stripe 仅在支付流程中按需加载,避免进入首页公共依赖。 + if (id.includes('/@stripe/stripe-js/')) { + return 'vendor-stripe' + } + // 其他小型第三方库合并 return 'vendor-misc' } From 29e39b96e5896889ec25a62f3b74336d20aee690 Mon Sep 17 00:00:00 2001 From: rurucxk <2568617273@qq.com> Date: Fri, 17 Jul 2026 00:23:19 +0800 Subject: [PATCH 2/7] fix(grok): enable free prompt cache for Responses function tools Reuse applyGrokFreeMessagesFunctionToolCacheRoute on native /v1/responses and the Grok WS HTTP bridge so Free OAuth requests with client function tools get the same mixed-tools cache route as the Messages bridge (append/convert web_search and x_search). Also dedupe: Grok Build already declares function tools named web_search, so naive append caused "Duplicate tool names: web_search". Convert those function entries to native tool types and skip duplicates. Only Free OAuth accounts (isKnownGrokFreeAccount); paid/unknown unchanged. --- .../internal/service/openai_gateway_grok.go | 6 ++++ .../service/openai_gateway_grok_cache.go | 30 +++++++++++++++---- .../internal/service/openai_ws_http_bridge.go | 5 ++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/backend/internal/service/openai_gateway_grok.go b/backend/internal/service/openai_gateway_grok.go index 8c18c9e5eb..c093029332 100644 --- a/backend/internal/service/openai_gateway_grok.go +++ b/backend/internal/service/openai_gateway_grok.go @@ -61,6 +61,12 @@ func (s *OpenAIGatewayService) forwardGrokResponses( if err != nil { return nil, fmt.Errorf("apply grok prompt cache identity: %w", err) } + // Free OAuth + client function tools: reuse Messages mixed-tools cache route + // (append web_search/x_search so xAI does not force non-cacheable build-free). + patchedBody, err = applyGrokFreeMessagesFunctionToolCacheRoute(patchedBody, body, account, cacheIdentity) + if err != nil { + return nil, fmt.Errorf("apply grok Free function-tool cache route: %w", err) + } token, _, err := s.getRequestCredential(ctx, c, account) if err != nil { diff --git a/backend/internal/service/openai_gateway_grok_cache.go b/backend/internal/service/openai_gateway_grok_cache.go index dac9e975e1..9b6436e436 100644 --- a/backend/internal/service/openai_gateway_grok_cache.go +++ b/backend/internal/service/openai_gateway_grok_cache.go @@ -102,8 +102,8 @@ func isGrokRequestContext(c *gin.Context) bool { // Free OAuth requests without native search tools are routed by xAI to the // non-cacheable build-free model. For otherwise tool-free requests, add the // native tools with tool_choice=none: this selects the cache-capable tier -// without allowing an actual search. Explicit client tools are handled by the -// narrower Messages-only mixed-tools policy below. +// without allowing an actual search. Explicit client function tools are handled by +// applyGrokFreeMessagesFunctionToolCacheRoute (Messages bridge and native Responses). func applyGrokResponsesCacheIdentity(body, intentSourceBody []byte, identity string, injectFreeTierTools bool) ([]byte, error) { identity = strings.TrimSpace(identity) if identity == "" { @@ -263,17 +263,35 @@ func appendMissingGrokFreeCacheNativeTools(body []byte) ([]byte, error) { toolType := strings.TrimSpace(tool.Get("type").String()) switch toolType { case "function": - if !tool.IsObject() || strings.TrimSpace(tool.Get("name").String()) == "" || tool.Get("function").Exists() { + name := strings.TrimSpace(tool.Get("name").String()) + if !tool.IsObject() || name == "" || tool.Get("function").Exists() { return body, nil } + // Grok Build may declare search as function tools. Convert to native + // entries so Free OAuth stays cache-capable without duplicate names. + if name == "web_search" || name == "x_search" { + if present[name] { + continue + } + raw, err := json.Marshal(map[string]string{"type": name}) + if err != nil { + return nil, err + } + merged = append(merged, raw) + present[name] = true + continue + } hasFunction = true + merged = append(merged, json.RawMessage(tool.Raw)) case "web_search", "x_search": - // Native tools may already be present when this helper is retried. + if present[toolType] { + continue + } + merged = append(merged, json.RawMessage(tool.Raw)) + present[toolType] = true default: return body, nil } - merged = append(merged, json.RawMessage(tool.Raw)) - present[toolType] = true } if !hasFunction { return body, nil diff --git a/backend/internal/service/openai_ws_http_bridge.go b/backend/internal/service/openai_ws_http_bridge.go index 09b3dc0628..cc7b307982 100644 --- a/backend/internal/service/openai_ws_http_bridge.go +++ b/backend/internal/service/openai_ws_http_bridge.go @@ -201,6 +201,11 @@ func (s *OpenAIGatewayService) proxyOpenAIWSHTTPBridgeTurn( releaseUpstreamCtx() return nil, fmt.Errorf("apply grok prompt cache identity: %w", err) } + body, err = applyGrokFreeMessagesFunctionToolCacheRoute(body, grokIntentSourceBody, account, grokCacheIdentity) + if err != nil { + releaseUpstreamCtx() + return nil, fmt.Errorf("apply grok Free function-tool cache route: %w", err) + } upstreamReq, err = buildGrokResponsesRequest(upstreamCtx, c, account, body, token, grokCacheIdentity, s.cfg) } else { upstreamReq, err = s.buildUpstreamRequestOpenAIPassthrough(upstreamCtx, c, account, body, token) From d2b080e88b9f2e2082058e230b6f0abf14914e12 Mon Sep 17 00:00:00 2001 From: shaw Date: Fri, 17 Jul 2026 08:44:23 +0800 Subject: [PATCH 3/7] fix(openai): restore APIKey account scheduling for alpha/search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #4394 (776f3f0de) 在新增 alpha_search 调度能力时加入了 OAuth-only 门控,把 APIKey 账号从 /alpha/search 候选池整体剔除;但转发层自 52071d391 起就支持 APIKey 走 {base_url}/v1/alpha/search,门控注释中 「API key 会被发往 chatgpt.com 导致 401」与实际路由不符。结果是纯 APIKey 分组自 v0.1.157 起独立搜索在选号阶段即失败(无可用账号)。 修复: - SupportsOpenAIEndpointCapability 的 alpha_search 门控放行 AccountTypeAPIKey(OAuth/APIKey 均可,Grok 等仍拒绝);显式能力集 语义不变,chat_completions 继续隐含放行 alpha_search。 - ForwardAlphaSearch 对 APIKey 账号的 404/405 按端点级 failover 处理 (换号、不写账号错误状态),避免混合分组里请求死在不支持该端点的 APIKey 上游;OAuth 账号 404 透传行为保持不变。 - 更新固化旧门控行为的用例,并在调度层新增 APIKey 放行回归锁。 --- backend/internal/service/account.go | 9 +- .../service/openai_account_scheduler_test.go | 47 ++++++++++ .../internal/service/openai_alpha_search.go | 26 +++++- .../service/openai_alpha_search_test.go | 93 +++++++++++++++++++ .../internal/service/openai_images_test.go | 17 +++- 5 files changed, 182 insertions(+), 10 deletions(-) diff --git a/backend/internal/service/account.go b/backend/internal/service/account.go index 701e14d742..fbf955dcc9 100644 --- a/backend/internal/service/account.go +++ b/backend/internal/service/account.go @@ -1425,10 +1425,11 @@ func (a *Account) SupportsOpenAIEndpointCapability(capability OpenAIEndpointCapa // 配置集校验。 capability = OpenAIEndpointCapabilityChatCompletions case OpenAIEndpointCapabilityAlphaSearch: - // Codex alpha/search 是 ChatGPT/Codex 后端工具端点,必须使用 - // OAuth/PAT/AgentIdentity 这类 ChatGPT 账号凭据;API key 被发往 - // chatgpt.com/backend-api/codex/alpha/search 会稳定 401。 - if a.Type != AccountTypeOAuth { + // alpha/search 的转发按账号类型分流:OAuth/PAT 走 + // chatgpt.com/backend-api/codex/alpha/search,API key 走 + // {base_url}/v1/alpha/search(见 openAIAlphaSearchURL),两类账号 + // 都可承接独立搜索请求。上游不支持该端点时由转发层 failover 兜底。 + if a.Type != AccountTypeOAuth && a.Type != AccountTypeAPIKey { return false } case OpenAIEndpointCapabilityEmbeddings: diff --git a/backend/internal/service/openai_account_scheduler_test.go b/backend/internal/service/openai_account_scheduler_test.go index f952617021..25b563281f 100644 --- a/backend/internal/service/openai_account_scheduler_test.go +++ b/backend/internal/service/openai_account_scheduler_test.go @@ -668,6 +668,53 @@ func TestOpenAIGatewayService_SelectAccountWithScheduler_ResponsesCapabilityExcl }) } +// alpha/search 调度必须同时放行 OAuth 与 APIKey 账号:v0.1.157 曾因 OAuth-only +// 门控把 APIKey 账号从候选池剔除,纯 APIKey 分组的独立搜索请求在选号阶段就 +// 报无可用账号,Codex 网页搜索整体失效(转发层其实一直支持 APIKey 路径)。 +func TestOpenAIGatewayService_SelectAccountWithScheduler_AlphaSearchAllowsAPIKeyAccount(t *testing.T) { + resetOpenAIAdvancedSchedulerSettingCacheForTest() + + ctx := context.Background() + groupID := int64(10125) + accounts := []Account{ + { + ID: 38001, + Platform: PlatformOpenAI, + Type: AccountTypeAPIKey, + Status: StatusActive, + Schedulable: true, + Concurrency: 1, + Priority: 0, + }, + } + cfg := &config.Config{} + cfg.Gateway.Scheduling.LoadBatchEnabled = false + svc := &OpenAIGatewayService{ + accountRepo: schedulerTestOpenAIAccountRepo{accounts: accounts}, + cache: &schedulerTestGatewayCache{}, + cfg: cfg, + concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{}), + } + + selection, _, err := svc.SelectAccountWithSchedulerForCapability( + ctx, + &groupID, + "", + "", + "gpt-5.6-sol", + nil, + OpenAIUpstreamTransportHTTPSSE, + OpenAIEndpointCapabilityAlphaSearch, + false, + false, + false, + ) + require.NoError(t, err) + require.NotNil(t, selection) + require.NotNil(t, selection.Account) + require.Equal(t, int64(38001), selection.Account.ID) +} + func TestOpenAIGatewayService_SelectAccountWithScheduler_DefaultDisabled_AllowsGrokChatAccount(t *testing.T) { resetOpenAIAdvancedSchedulerSettingCacheForTest() diff --git a/backend/internal/service/openai_alpha_search.go b/backend/internal/service/openai_alpha_search.go index d220cb7a1e..f11a21958f 100644 --- a/backend/internal/service/openai_alpha_search.go +++ b/backend/internal/service/openai_alpha_search.go @@ -87,7 +87,8 @@ func (s *OpenAIGatewayService) ForwardAlphaSearch(ctx context.Context, c *gin.Co if resp.StatusCode >= http.StatusBadRequest { upstreamMessage := sanitizeUpstreamErrorMessage(strings.TrimSpace(extractUpstreamErrorMessage(respBody))) - if s.shouldFailoverOpenAIUpstreamResponse(resp.StatusCode, upstreamMessage, respBody) { + if s.shouldFailoverOpenAIUpstreamResponse(resp.StatusCode, upstreamMessage, respBody) || + isOpenAIAlphaSearchEndpointUnsupported(account, resp.StatusCode) { resp.Body = io.NopCloser(bytes.NewReader(respBody)) // alpha/search 是独立的工具端点,单次 401 不能证明账号的模型调用 // 凭据全局失效。若沿用通用 401 逻辑,PAT 会因没有 refresh_token @@ -505,8 +506,29 @@ func (s *OpenAIGatewayService) ensureOpenAIAlphaSearchAuthMetadata(ctx context.C return nil } +// isOpenAIAlphaSearchEndpointUnsupported 识别「API key 上游没有实现 +// /v1/alpha/search 端点」的响应。404/405 不在通用 failover 状态集里(模型 +// 调用中的 404 通常是用户请求问题),但对这个独立工具端点而言,它几乎只 +// 意味着所选上游(官方平台或第三方中转)不提供该端点——应换号重试,而 +// 不是把 404 透传给客户端,否则混合分组里 OAuth 账号明明可以承接搜索, +// 请求却可能死在先被选中的 API key 账号上。 +func isOpenAIAlphaSearchEndpointUnsupported(account *Account, statusCode int) bool { + if account == nil || account.Type != AccountTypeAPIKey { + return false + } + return statusCode == http.StatusNotFound || statusCode == http.StatusMethodNotAllowed +} + func shouldApplyOpenAIAlphaSearchAccountErrorSideEffects(statusCode int) bool { - return statusCode != http.StatusUnauthorized + switch statusCode { + case http.StatusUnauthorized, http.StatusNotFound, http.StatusMethodNotAllowed: + // 401:工具端点的 access enforcement 不代表凭据全局失效; + // 404/405:端点不存在只说明该上游不支持独立搜索,账号本身健康。 + // 两类都只换号,不写账号错误状态。 + return false + default: + return true + } } func openAIAlphaSearchResponseFromResponsesSSE(body []byte) ([]byte, error) { diff --git a/backend/internal/service/openai_alpha_search_test.go b/backend/internal/service/openai_alpha_search_test.go index 6b01356016..e31fd9e01d 100644 --- a/backend/internal/service/openai_alpha_search_test.go +++ b/backend/internal/service/openai_alpha_search_test.go @@ -393,8 +393,101 @@ func TestForwardAlphaSearchPATResponsesFallbackUnauthorizedDoesNotMarkAccountErr require.False(t, c.Writer.Written()) } +// API key 上游(官方平台或第三方中转)不提供 /v1/alpha/search 时返回的 +// 404/405 必须触发换号而不是把错误透传给客户端:混合分组里 OAuth 账号可以 +// 承接搜索,请求不能死在先被选中的 API key 账号上。端点缺失也不能写账号 +// 错误状态——账号本身是健康的。 +func TestForwardAlphaSearchAPIKeyEndpointNotFoundFailsOver(t *testing.T) { + gin.SetMode(gin.TestMode) + body := []byte(`{"id":"search-session","model":"gpt-5.6-sol","commands":{"search_query":[{"q":"news"}]}}`) + recorder := httptest.NewRecorder() + c, _ := gin.CreateTestContext(recorder) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/alpha/search", bytes.NewReader(body)) + + upstream := &httpUpstreamRecorder{resp: &http.Response{ + StatusCode: http.StatusNotFound, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{"error":{"message":"Not Found"}}`)), + }} + repo := &alphaSearchAccountStateRepo{} + cfg := &config.Config{} + service := &OpenAIGatewayService{ + cfg: cfg, + httpUpstream: upstream, + accountRepo: repo, + rateLimitService: NewRateLimitService(repo, nil, cfg, nil, nil), + } + account := &Account{ + ID: 9, + Platform: PlatformOpenAI, + Type: AccountTypeAPIKey, + Credentials: map[string]any{ + "api_key": "sk-test", + "base_url": "https://relay.example", + }, + } + + result, err := service.ForwardAlphaSearch(context.Background(), c, account, body) + + require.Nil(t, result) + var failoverErr *UpstreamFailoverError + require.ErrorAs(t, err, &failoverErr) + require.Equal(t, http.StatusNotFound, failoverErr.StatusCode) + require.Zero(t, repo.setErrorCalls) + require.Empty(t, repo.lastError) + require.False(t, c.Writer.Written()) + require.Empty(t, recorder.Body.String()) +} + +// OAuth 账号的 chatgpt.com 端点固定存在,404 保持原有透传行为不变。 +func TestForwardAlphaSearchOAuthNotFoundPassesThrough(t *testing.T) { + gin.SetMode(gin.TestMode) + body := []byte(`{"id":"search-session","model":"gpt-5.6-sol","commands":{"search_query":[{"q":"news"}]}}`) + recorder := httptest.NewRecorder() + c, _ := gin.CreateTestContext(recorder) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/alpha/search", bytes.NewReader(body)) + + upstreamBody := `{"detail":"Not Found"}` + upstream := &httpUpstreamRecorder{resp: &http.Response{ + StatusCode: http.StatusNotFound, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(upstreamBody)), + }} + service := &OpenAIGatewayService{cfg: &config.Config{}, httpUpstream: upstream} + account := &Account{ + ID: 10, + Platform: PlatformOpenAI, + Type: AccountTypeOAuth, + Concurrency: 1, + Credentials: map[string]any{ + "access_token": "oauth-token", + "chatgpt_account_id": "chatgpt-account", + }, + } + + result, err := service.ForwardAlphaSearch(context.Background(), c, account, body) + + require.NoError(t, err) + require.Nil(t, result) + require.Equal(t, http.StatusNotFound, recorder.Code) + require.JSONEq(t, upstreamBody, recorder.Body.String()) +} + func TestShouldApplyOpenAIAlphaSearchAccountErrorSideEffects(t *testing.T) { require.False(t, shouldApplyOpenAIAlphaSearchAccountErrorSideEffects(http.StatusUnauthorized)) + require.False(t, shouldApplyOpenAIAlphaSearchAccountErrorSideEffects(http.StatusNotFound)) + require.False(t, shouldApplyOpenAIAlphaSearchAccountErrorSideEffects(http.StatusMethodNotAllowed)) require.True(t, shouldApplyOpenAIAlphaSearchAccountErrorSideEffects(http.StatusForbidden)) require.True(t, shouldApplyOpenAIAlphaSearchAccountErrorSideEffects(http.StatusTooManyRequests)) } + +func TestIsOpenAIAlphaSearchEndpointUnsupported(t *testing.T) { + apiKey := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey} + oauth := &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth} + + require.True(t, isOpenAIAlphaSearchEndpointUnsupported(apiKey, http.StatusNotFound)) + require.True(t, isOpenAIAlphaSearchEndpointUnsupported(apiKey, http.StatusMethodNotAllowed)) + require.False(t, isOpenAIAlphaSearchEndpointUnsupported(apiKey, http.StatusBadRequest)) + require.False(t, isOpenAIAlphaSearchEndpointUnsupported(oauth, http.StatusNotFound)) + require.False(t, isOpenAIAlphaSearchEndpointUnsupported(nil, http.StatusNotFound)) +} diff --git a/backend/internal/service/openai_images_test.go b/backend/internal/service/openai_images_test.go index d3579e44de..82f9c3addc 100644 --- a/backend/internal/service/openai_images_test.go +++ b/backend/internal/service/openai_images_test.go @@ -475,7 +475,7 @@ func TestAccountSupportsOpenAIImageCapability_EmptyRequirementDoesNotRejectGrok( } func TestAccountSupportsOpenAIEndpointCapability(t *testing.T) { - t.Run("OpenAI APIKey 默认兼容 chat 和 embeddings", func(t *testing.T) { + t.Run("OpenAI APIKey 默认兼容 chat、embeddings 和 alpha search", func(t *testing.T) { account := &Account{ Platform: PlatformOpenAI, Type: AccountTypeAPIKey, @@ -483,6 +483,7 @@ func TestAccountSupportsOpenAIEndpointCapability(t *testing.T) { require.True(t, account.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityChatCompletions)) require.True(t, account.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityEmbeddings)) + require.True(t, account.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityAlphaSearch)) }) t.Run("OpenAI OAuth 默认仅兼容 chat", func(t *testing.T) { @@ -496,7 +497,9 @@ func TestAccountSupportsOpenAIEndpointCapability(t *testing.T) { require.False(t, account.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityEmbeddings)) }) - t.Run("alpha search 仅允许 OpenAI OAuth/PAT 类账号", func(t *testing.T) { + t.Run("alpha search 允许 OpenAI OAuth/PAT 与 APIKey 账号,拒绝 Grok", func(t *testing.T) { + // OAuth/PAT 走 chatgpt.com Codex 端点,APIKey 走 {base_url}/v1/alpha/search, + // 两类都能承接独立搜索(APIKey 被排除曾导致纯 APIKey 分组搜索失效的回归)。 apiKey := &Account{ Platform: PlatformOpenAI, Type: AccountTypeAPIKey, @@ -505,9 +508,14 @@ func TestAccountSupportsOpenAIEndpointCapability(t *testing.T) { Platform: PlatformOpenAI, Type: AccountTypeOAuth, } + grok := &Account{ + Platform: PlatformGrok, + Type: AccountTypeAPIKey, + } - require.False(t, apiKey.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityAlphaSearch)) + require.True(t, apiKey.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityAlphaSearch)) require.True(t, oauth.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityAlphaSearch)) + require.False(t, grok.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityAlphaSearch)) }) t.Run("显式列表支持同时声明 chat 和 embeddings", func(t *testing.T) { @@ -533,7 +541,8 @@ func TestAccountSupportsOpenAIEndpointCapability(t *testing.T) { } require.True(t, account.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityChatCompletions)) - require.False(t, account.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityAlphaSearch)) + // chat 能力隐含放行 alpha search(OAuth/APIKey 语义一致)。 + require.True(t, account.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityAlphaSearch)) require.False(t, account.SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityEmbeddings)) }) From 824f763a77d4582daa83f84dcb58a13fcde3f3a6 Mon Sep 17 00:00:00 2001 From: yardbirds0 <498756723@qq.com> Date: Fri, 17 Jul 2026 09:15:32 +0800 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E4=BB=8E=20API?= =?UTF-8?q?=20Key=20=E8=B4=A6=E5=8F=B7=E5=90=8D=E7=A7=B0=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=E4=B8=8A=E6=B8=B8=E7=AB=99=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/views/admin/AccountsView.vue | 26 ++++++++++- .../AccountsView.sparkShadow.spec.ts | 46 ++++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index 688ca50027..528bd797c8 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -252,7 +252,24 @@