feat(grok): 支持上游端点手动切换与快捷端点,修复 SSO 建号自定义地址被覆盖

官方端点(api.x.ai / cli-chat-proxy.grok.com)偶发不可用,运营方需要在
端点间手动切换。旧语义把 OAuth 账号存储的官方 host 一律视同"未定制"并
回落默认 CLI 网关:填了官方地址保存成功却不生效、重新编辑开关回到关闭、
再次保存直接删值,形成"修改不生效"的静默循环。

后端:
- GetGrokBaseURL OAuth 分支改为"存了什么用什么":官方 API / 区域 API /
  第三方转发地址一律按填写值转发与探测,仅空值或无法解析的脏数据回落
  默认 CLI 网关;删除官方变体运行时迁移逻辑
- *.api.x.ai 区域端点(us-east-1/us-west-2/eu-west-1 等)纳入可信 host,
  OAuth 使用时不受运营方 URL 白名单限制,官方 host 仍强制 /v1 path
- 修复 SSO 批量建号 MergeCredentials 方向缺陷:BuildAccountCredentials
  恒写官方 base_url,会覆盖导入请求指定的自定义转发地址;抽出
  grokSSOImportCredentials 显式保留请求值(与 RefreshAccountToken 对齐)

前端:
- isCustomGrokBaseUrl 仅默认 CLI 网关 host 视同未定制:api.x.ai 与区域
  端点保存后正常回显(开关开启 + 显示地址),不再被静默吞掉
- 新增 GrokBaseUrlPresets 快捷端点组件(Grok Build CLI / 官方 API /
  us-east-1 / us-west-2 / eu-west-1),接入编辑(OAuth 自定义区 + apikey
  Base URL)、新增(同前)与批量编辑(所选平台全为 grok 时显示,点击
  自动勾选 base_url);仅快速填充,输入框仍可自由填写任意第三方地址
This commit is contained in:
shaw
2026-07-16 19:10:21 +08:00
parent b960ec1980
commit 7f5d067af2
18 changed files with 591 additions and 104 deletions
@@ -373,8 +373,7 @@ func (h *GrokOAuthHandler) createAccountFromSSOToken(ctx context.Context, req Gr
return grokSSOImportWorkerResult{item: GrokSSOToOAuthItemResult{Index: index, Error: grokSSOImportErrorMessage(err)}}
}
credentials := h.grokOAuthService.BuildAccountCredentials(tokenInfo)
credentials = service.MergeCredentials(cloneGrokSSOMap(req.Credentials), credentials)
credentials := grokSSOImportCredentials(h.grokOAuthService.BuildAccountCredentials(tokenInfo), req.Credentials)
name := grokSSOImportAccountName(req.Name, tokenInfo, index, total)
expiresAt, autoPauseOnExpired := grokSSOImportExpiry(req.ExpiresAt, req.AutoPauseOnExpired, tokenInfo)
account, err := h.adminService.CreateAccount(ctx, &service.CreateAccountInput{
@@ -408,6 +407,18 @@ func (h *GrokOAuthHandler) createAccountFromSSOToken(ctx context.Context, req Gr
}
}
// grokSSOImportCredentials 合并 SSO 兑换出的凭据与导入请求携带的运营侧配置。
// token 字段以 BuildAccountCredentials 为准(请求不可覆盖);但 base_url 是运营侧
// 配置且 Build 恒写官方地址,会吞掉导入时指定的自定义转发地址——与
// RefreshAccountToken 的保留逻辑对齐,请求显式提供时以请求为准。
func grokSSOImportCredentials(built map[string]any, reqCredentials map[string]any) map[string]any {
credentials := service.MergeCredentials(cloneGrokSSOMap(reqCredentials), built)
if reqBaseURL, ok := reqCredentials["base_url"].(string); ok && strings.TrimSpace(reqBaseURL) != "" {
credentials["base_url"] = strings.TrimSpace(reqBaseURL)
}
return credentials
}
func grokSSOImportExpiry(requestExpiresAt *int64, requestAutoPause *bool, tokenInfo *service.GrokTokenInfo) (*int64, *bool) {
if tokenInfo == nil || strings.TrimSpace(tokenInfo.RefreshToken) != "" || tokenInfo.ExpiresAt <= 0 {
return requestExpiresAt, requestAutoPause
@@ -223,6 +223,45 @@ func TestGrokSSOImportExpiryPreservesRequestSettingsWithRefreshToken(t *testing.
require.Same(t, &requestedAutoPause, autoPause)
}
func TestGrokSSOImportCredentialsPreservesRequestedBaseURL(t *testing.T) {
built := map[string]any{
"access_token": "at-1",
"base_url": xai.DefaultCLIBaseURL,
}
reqCredentials := map[string]any{
"base_url": "https://relay.example.com/v1",
"header_override_enabled": true,
"header_overrides": map[string]any{"x-relay-key": "k"},
}
credentials := grokSSOImportCredentials(built, reqCredentials)
// token 字段以兑换结果为准;base_url 是运营侧配置,必须保留请求里的自定义地址
require.Equal(t, "at-1", credentials["access_token"])
require.Equal(t, "https://relay.example.com/v1", credentials["base_url"])
require.Equal(t, true, credentials["header_override_enabled"])
require.Equal(t, map[string]any{"x-relay-key": "k"}, credentials["header_overrides"])
// 入参不被污染(req.Credentials 会被多个 worker 并发读取)
require.Equal(t, "https://relay.example.com/v1", reqCredentials["base_url"])
}
func TestGrokSSOImportCredentialsDefaultsToOfficialBaseURL(t *testing.T) {
built := map[string]any{
"access_token": "at-1",
"base_url": xai.DefaultCLIBaseURL,
}
credentials := grokSSOImportCredentials(built, nil)
require.Equal(t, xai.DefaultCLIBaseURL, credentials["base_url"])
credentials = grokSSOImportCredentials(map[string]any{
"access_token": "at-2",
"base_url": xai.DefaultCLIBaseURL,
}, map[string]any{"base_url": " "})
require.Equal(t, xai.DefaultCLIBaseURL, credentials["base_url"])
require.Equal(t, "at-2", credentials["access_token"])
}
func TestGrokSSOImportWorkerRecoversPanic(t *testing.T) {
h := &GrokOAuthHandler{}
result := h.safeCreateAccountFromSSOToken(context.Background(), GrokSSOToOAuthRequest{}, "token", 2, 3)