From 6b5f176baac89c1316adf7e40e0419b6c5df61da Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Fri, 7 Aug 2026 14:58:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(*):=20=E5=9B=BE=E7=89=87=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=B8=A0=E9=81=93=20-=20=E6=96=B0=E5=A2=9E=20Grok=20JSON=20?= =?UTF-8?q?=E7=94=9F=E5=9B=BE=E4=B8=8E=E5=8D=95=E5=9B=BE=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/security.go | 4 +- backend/internal/handler/security_test.go | 9 +++ backend/internal/model/models.go | 1 + backend/internal/service/admin.go | 2 +- backend/internal/service/channel_models.go | 2 +- backend/internal/service/model_capability.go | 11 +++- backend/internal/service/provider.go | 47 +++++++++++++- .../service/provider_request_types.go | 12 ++++ backend/internal/service/provider_test.go | 63 +++++++++++++++++++ web/src/components/model-protocol-picker.tsx | 2 +- web/src/lib/model-capabilities.ts | 13 +++- web/src/lib/model-protocols.ts | 2 + .../pages/settings/channel-video-pricing.tsx | 1 + web/src/services/api/image.ts | 46 ++++++++++++++ web/src/stores/use-config-store.ts | 2 +- 15 files changed, 207 insertions(+), 10 deletions(-) diff --git a/backend/internal/handler/security.go b/backend/internal/handler/security.go index f86b805b9..b320bac05 100644 --- a/backend/internal/handler/security.go +++ b/backend/internal/handler/security.go @@ -82,7 +82,7 @@ func authorizeCustomRelay(method string, target *url.URL, apiFormat string, cont } if apiFormat == "openai" { multipartAllowed := mediaType == "multipart/form-data" && (strings.HasSuffix(requestPath, "/images/edits") || strings.HasSuffix(requestPath, "/videos")) - jsonAllowed := mediaType == "application/json" && (strings.HasSuffix(requestPath, "/responses") || strings.HasSuffix(requestPath, "/chat/completions") || strings.HasSuffix(requestPath, "/images/generations") || strings.HasSuffix(requestPath, "/audio/speech") || strings.HasSuffix(requestPath, "/video/generations") || strings.HasSuffix(requestPath, "/videos/generations") || strings.HasSuffix(requestPath, "/videos") || strings.HasSuffix(requestPath, "/contents/generations/tasks")) + jsonAllowed := mediaType == "application/json" && (strings.HasSuffix(requestPath, "/responses") || strings.HasSuffix(requestPath, "/chat/completions") || strings.HasSuffix(requestPath, "/images/generations") || strings.HasSuffix(requestPath, "/images/edits") || strings.HasSuffix(requestPath, "/audio/speech") || strings.HasSuffix(requestPath, "/video/generations") || strings.HasSuffix(requestPath, "/videos/generations") || strings.HasSuffix(requestPath, "/videos") || strings.HasSuffix(requestPath, "/contents/generations/tasks")) if len(query) != 0 || (!multipartAllowed && !jsonAllowed) { return errors.New("自定义渠道不允许访问该上游接口") } @@ -183,7 +183,7 @@ func interfaceAllowsProxyPath(interfaceType model.ChannelInterfaceType, requestP return requestPath == "/chat/completions" case model.ChannelInterfaceOpenAIResponse: return requestPath == "/responses" - case model.ChannelInterfaceOpenAIImage: + case model.ChannelInterfaceOpenAIImage, model.ChannelInterfaceGrokImage: return requestPath == "/images/generations" || requestPath == "/images/edits" case model.ChannelInterfaceVolcengineArkImage: return requestPath == "/images/generations" diff --git a/backend/internal/handler/security_test.go b/backend/internal/handler/security_test.go index 89adb5cdc..db48d7462 100644 --- a/backend/internal/handler/security_test.go +++ b/backend/internal/handler/security_test.go @@ -18,6 +18,14 @@ func TestAuthorizeSystemProxyAllowsConfiguredGenerationModel(t *testing.T) { } } +func TestAuthorizeSystemProxyAllowsGrokImageJSONEdits(t *testing.T) { + channel := &model.ModelChannel{APIFormat: "openai", ModelsJSON: `["grok-imagine-image"]`} + body := []byte(`{"model":"grok-imagine-image","prompt":"edit"}`) + if err := authorizeSystemProxy(channel, model.ChannelInterfaceGrokImage, http.MethodPost, "/images/edits", "application/json", body); err != nil { + t.Fatalf("authorizeSystemProxy() error = %v", err) + } +} + func TestAuthorizeCustomRelayAllowsModelsAndAgentEndpoints(t *testing.T) { tests := []struct { method string @@ -30,6 +38,7 @@ func TestAuthorizeCustomRelayAllowsModelsAndAgentEndpoints(t *testing.T) { {method: http.MethodPost, target: "https://api.example.com/v1/chat/completions", apiFormat: "openai", contentType: "application/json; charset=utf-8"}, {method: http.MethodPost, target: "https://api.example.com/v1/audio/speech", apiFormat: "openai", contentType: "application/json"}, {method: http.MethodPost, target: "https://api.example.com/v1/images/edits", apiFormat: "openai", contentType: "multipart/form-data; boundary=test"}, + {method: http.MethodPost, target: "https://api.example.com/v1/images/edits", apiFormat: "openai", contentType: "application/json"}, {method: http.MethodPost, target: "https://api.example.com/v1/videos", apiFormat: "openai", contentType: "application/json"}, {method: http.MethodPost, target: "https://api.example.com/v1/videos", apiFormat: "openai", contentType: "multipart/form-data; boundary=test"}, {method: http.MethodGet, target: "https://api.example.com/v1/videos/task-1/content", apiFormat: "openai"}, diff --git a/backend/internal/model/models.go b/backend/internal/model/models.go index 3aade4cc1..126d67cfc 100644 --- a/backend/internal/model/models.go +++ b/backend/internal/model/models.go @@ -61,6 +61,7 @@ const ( ChannelInterfaceChatCompletion ChannelInterfaceType = "chat-completion" ChannelInterfaceOpenAIResponse ChannelInterfaceType = "openai-response" ChannelInterfaceOpenAIImage ChannelInterfaceType = "openai-image" + ChannelInterfaceGrokImage ChannelInterfaceType = "grok-image" ChannelInterfaceVolcengineArkImage ChannelInterfaceType = "volcengine-ark-image" ChannelInterfaceVolcengineJiMengImage ChannelInterfaceType = "volcengine-jimeng-image" ChannelInterfaceOpenAIAudio ChannelInterfaceType = "openai-audio" diff --git a/backend/internal/service/admin.go b/backend/internal/service/admin.go index c34565834..7313f4a80 100644 --- a/backend/internal/service/admin.go +++ b/backend/internal/service/admin.go @@ -734,7 +734,7 @@ func mergeChannelRequest(req ChannelRequest, channel model.ModelChannel) Channel func validChannelInterfaceType(value model.ChannelInterfaceType) bool { switch value { - case model.ChannelInterfaceChatCompletion, model.ChannelInterfaceOpenAIResponse, model.ChannelInterfaceOpenAIImage, model.ChannelInterfaceVolcengineArkImage, model.ChannelInterfaceVolcengineJiMengImage, model.ChannelInterfaceOpenAIAudio, model.ChannelInterfaceAsyncAudio, model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo, model.ChannelInterfaceVolcengineArkVideo, model.ChannelInterfaceVolcengineJiMengVideo, model.ChannelInterfaceGeminiVeo: + case model.ChannelInterfaceChatCompletion, model.ChannelInterfaceOpenAIResponse, model.ChannelInterfaceOpenAIImage, model.ChannelInterfaceGrokImage, model.ChannelInterfaceVolcengineArkImage, model.ChannelInterfaceVolcengineJiMengImage, model.ChannelInterfaceOpenAIAudio, model.ChannelInterfaceAsyncAudio, model.ChannelInterfaceNewAPIVideo, model.ChannelInterfaceNewAPIChannel1, model.ChannelInterfaceNewAPIChannel2, model.ChannelInterfaceXAIVideo, model.ChannelInterfaceVolcengineArkVideo, model.ChannelInterfaceVolcengineJiMengVideo, model.ChannelInterfaceGeminiVeo: return true default: return false diff --git a/backend/internal/service/channel_models.go b/backend/internal/service/channel_models.go index fc8bc68c9..555108a6c 100644 --- a/backend/internal/service/channel_models.go +++ b/backend/internal/service/channel_models.go @@ -454,7 +454,7 @@ func (s *Service) syncChannelModelNames(channel *model.ModelChannel) error { func capabilityForProtocol(protocol model.ChannelInterfaceType) string { switch protocol { - case model.ChannelInterfaceOpenAIImage, model.ChannelInterfaceVolcengineArkImage, model.ChannelInterfaceVolcengineJiMengImage: + case model.ChannelInterfaceOpenAIImage, model.ChannelInterfaceGrokImage, model.ChannelInterfaceVolcengineArkImage, model.ChannelInterfaceVolcengineJiMengImage: return "image" case model.ChannelInterfaceOpenAIAudio, model.ChannelInterfaceAsyncAudio: return "audio" diff --git a/backend/internal/service/model_capability.go b/backend/internal/service/model_capability.go index ac85d57af..bc7ae8e20 100644 --- a/backend/internal/service/model_capability.go +++ b/backend/internal/service/model_capability.go @@ -106,6 +106,15 @@ func DefaultImageCapabilityConfig(protocol string, modelName string) *ImageCapab MaxOutputs: 15, } switch model.ChannelInterfaceType(protocol) { + case model.ChannelInterfaceGrokImage: + image.References.MaxImages = 1 + image.References.MaskSupported = false + image.Size = ImageSizeConfig{Parameter: "none", Values: []string{}, Default: "auto", AllowCustom: false} + image.Quality = ImageQualityConfig{Supported: false, Values: []string{}, Default: "auto"} + image.TransparentBackground = VideoBooleanConfig{Supported: false, Default: false} + image.ResponseFormat = ParameterSupport{Supported: true} + image.OutputFormat = ParameterSupport{Supported: false} + image.MaxOutputs = 1 case model.ChannelInterfaceVolcengineArkImage: image.References.MaskSupported = false image.Quality.Supported = false @@ -120,7 +129,7 @@ func DefaultImageCapabilityConfig(protocol string, modelName string) *ImageCapab image.ResponseFormat.Supported = false image.OutputFormat.Supported = false } - if strings.HasPrefix(strings.ToLower(strings.TrimSpace(modelName)), "grok-imagine-image") { + if model.ChannelInterfaceType(protocol) != model.ChannelInterfaceGrokImage && strings.HasPrefix(strings.ToLower(strings.TrimSpace(modelName)), "grok-imagine-image") { image.References.MaxImages = 0 image.References.MaskSupported = false image.Size = ImageSizeConfig{Parameter: "none", Values: []string{}, Default: "auto", AllowCustom: false} diff --git a/backend/internal/service/provider.go b/backend/internal/service/provider.go index ad68418b6..0dd10840c 100644 --- a/backend/internal/service/provider.go +++ b/backend/internal/service/provider.go @@ -635,6 +635,9 @@ func systemChannelIDFromBaseURL(baseURL string) string { } func runImageTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) { + if input.Config.InterfaceType == string(model.ChannelInterfaceGrokImage) { + return runGrokImageTask(ctx, input) + } if input.Config.InterfaceType == string(model.ChannelInterfaceVolcengineJiMengImage) { return runVolcengineJiMengImageTask(ctx, input) } @@ -720,6 +723,48 @@ func runImageTask(ctx context.Context, input canvasGenerationInput) (map[string] return map[string]interface{}{"mode": "image", "images": images}, nil } +func runGrokImageTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) { + body, path, err := grokImageRequestBody(input) + if err != nil { + return nil, err + } + var payload imageResponse + if err := postJSON(ctx, input.Config, path, body, &payload); err != nil { + return nil, err + } + images, err := imageDataURLs(payload) + if err != nil { + return nil, err + } + return map[string]interface{}{"mode": "image", "images": images}, nil +} + +func grokImageRequestBody(input canvasGenerationInput) (grokImageRequest, string, error) { + if input.Mask != nil { + return grokImageRequest{}, "", errors.New("Grok 图片协议不支持蒙版编辑,请移除蒙版后重试") + } + body := grokImageRequest{Model: input.Config.Model, Prompt: withSystemPrompt(input.Config, input.Prompt), N: 1, ResponseFormat: "url"} + if len(input.ReferenceImages) == 0 { + return body, "/images/generations", nil + } + if len(input.ReferenceImages) != 1 { + return grokImageRequest{}, "", fmt.Errorf("Grok 图片编辑只支持 1 张参考图,当前连接了 %d 张", len(input.ReferenceImages)) + } + imageURL, err := grokImageInputURL(input.ReferenceImages[0]) + if err != nil { + return grokImageRequest{}, "", err + } + body.Image = &grokImageInput{URL: imageURL} + return body, "/images/edits", nil +} + +func grokImageInputURL(media providerMedia) (string, error) { + if isPublicMediaURL(strings.TrimSpace(media.URL)) { + return strings.TrimSpace(media.URL), nil + } + return openAIImageInputURL(media) +} + const volcengineArkImageMaxPixels = 4624220 func runVolcengineArkImageTask(ctx context.Context, input canvasGenerationInput) (map[string]interface{}, error) { @@ -1909,7 +1954,7 @@ func validateGenerationInterface(mode string, interfaceType string) error { } allowed := map[string]map[string]bool{ "text": {"chat-completion": true, "openai-response": true}, - "image": {"openai-image": true, "volcengine-ark-image": true, "volcengine-jimeng-image": true}, + "image": {"openai-image": true, "grok-image": true, "volcengine-ark-image": true, "volcengine-jimeng-image": true}, "video": {"newapi": true, "newapi-channel-1": true, "newapi-channel-2": true, "xai-video": true, "volcengine-ark-video": true, "volcengine-jimeng-video": true, "gemini-veo": true}, "audio": {"openai-audio": true, "async-audio": true}, } diff --git a/backend/internal/service/provider_request_types.go b/backend/internal/service/provider_request_types.go index 50a955a85..9956e95b9 100644 --- a/backend/internal/service/provider_request_types.go +++ b/backend/internal/service/provider_request_types.go @@ -52,6 +52,18 @@ type xaiVideoImage struct { URL string `json:"url"` } +type grokImageRequest struct { + Model string `json:"model"` + Prompt string `json:"prompt"` + Image *grokImageInput `json:"image,omitempty"` + N int `json:"n"` + ResponseFormat string `json:"response_format"` +} + +type grokImageInput struct { + URL string `json:"url"` +} + type geminiVeoRequest struct { Instances []geminiVeoInstance `json:"instances"` Parameters geminiVeoParameters `json:"parameters"` diff --git a/backend/internal/service/provider_test.go b/backend/internal/service/provider_test.go index 4b71b9ed0..4687ffea8 100644 --- a/backend/internal/service/provider_test.go +++ b/backend/internal/service/provider_test.go @@ -104,6 +104,66 @@ func TestVolcengineArkImageRejectsMaskBeforeRequest(t *testing.T) { } } +func TestRunGrokImageTaskUsesJSONEditContract(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/images/edits" { + t.Errorf("path = %q, want /v1/images/edits", r.URL.Path) + } + if contentType := r.Header.Get("Content-Type"); !strings.HasPrefix(contentType, "application/json") { + t.Errorf("Content-Type = %q, want application/json", contentType) + } + var body grokImageRequest + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("decode request body: %v", err) + } + if body.Model != "grok-imagine-image-quality" || body.N != 1 || body.ResponseFormat != "url" { + t.Fatalf("request body = %#v", body) + } + if body.Image == nil || body.Image.URL != testReferenceImageDataURL { + t.Fatalf("image = %#v", body.Image) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":[{"url":"https://example.com/result.png"}]}`)) + })) + defer server.Close() + + result, err := runImageTask(context.Background(), canvasGenerationInput{ + Mode: "image", + Prompt: "edit the reference", + Config: providerConfig{BaseURL: server.URL, APIKey: "key", Model: "grok-imagine-image-quality", InterfaceType: "grok-image"}, + ReferenceImages: []providerMedia{{DataURL: testReferenceImageDataURL}}, + }) + if err != nil { + t.Fatalf("runImageTask() error = %v", err) + } + images, _ := result["images"].([]map[string]string) + if len(images) != 1 || images[0]["dataUrl"] != "https://example.com/result.png" { + t.Fatalf("images = %#v", result["images"]) + } +} + +func TestGrokImageRequestBodyRejectsMaskAndMultipleReferences(t *testing.T) { + if _, _, err := grokImageRequestBody(canvasGenerationInput{Config: providerConfig{InterfaceType: "grok-image"}, Mask: &providerMedia{DataURL: testReferenceImageDataURL}}); err == nil || !strings.Contains(err.Error(), "不支持蒙版") { + t.Fatalf("mask error = %v", err) + } + if _, _, err := grokImageRequestBody(canvasGenerationInput{Config: providerConfig{InterfaceType: "grok-image"}, ReferenceImages: []providerMedia{{DataURL: testReferenceImageDataURL}, {DataURL: testReferenceImageDataURL}}}); err == nil || !strings.Contains(err.Error(), "只支持 1 张") { + t.Fatalf("multiple reference error = %v", err) + } +} + +func TestGrokImageRequestBodyPrefersPublicURL(t *testing.T) { + body, path, err := grokImageRequestBody(canvasGenerationInput{ + Config: providerConfig{Model: "grok-imagine-image", InterfaceType: "grok-image"}, + ReferenceImages: []providerMedia{{URL: "https://example.com/reference.png", DataURL: testReferenceImageDataURL}}, + }) + if err != nil { + t.Fatalf("grokImageRequestBody() error = %v", err) + } + if path != "/images/edits" || body.Image == nil || body.Image.URL != "https://example.com/reference.png" { + t.Fatalf("path = %q, image = %#v", path, body.Image) + } +} + func TestNormalizePixelSizeConvertsCanvasAspectRatios(t *testing.T) { tests := map[string]string{ "1:1": "1024x1024", @@ -974,6 +1034,9 @@ func TestValidateGenerationInterfaceRejectsMismatchedType(t *testing.T) { if err := validateGenerationInterface("video", "xai-video"); err != nil { t.Fatalf("validateGenerationInterface() error = %v", err) } + if err := validateGenerationInterface("image", "grok-image"); err != nil { + t.Fatalf("validateGenerationInterface() error = %v", err) + } } func TestProcessTaskValidatesInterfaceBeforeHydratingMedia(t *testing.T) { diff --git a/web/src/components/model-protocol-picker.tsx b/web/src/components/model-protocol-picker.tsx index 3a0da01ba..c6f9dea28 100644 --- a/web/src/components/model-protocol-picker.tsx +++ b/web/src/components/model-protocol-picker.tsx @@ -146,7 +146,7 @@ function ProtocolBrandMark({ protocol, compact = false }: { protocol: ModelProto if (protocol.value.startsWith("volcengine-jimeng-")) return ; if (protocol.value.startsWith("volcengine-")) return ; if (protocol.value.startsWith("newapi-channel-")) return ; - const brand = protocol.value === "gemini-veo" ? "gemini" : protocol.value === "xai-video" ? "grok" : "openai"; + const brand = protocol.value === "gemini-veo" ? "gemini" : protocol.value === "grok-image" || protocol.value === "xai-video" ? "grok" : "openai"; return ; } diff --git a/web/src/lib/model-capabilities.ts b/web/src/lib/model-capabilities.ts index ed739cf0e..090bb41b2 100644 --- a/web/src/lib/model-capabilities.ts +++ b/web/src/lib/model-capabilities.ts @@ -74,7 +74,16 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = " outputFormat: { supported: true }, maxOutputs: 15, }; - if (protocol === "volcengine-ark-image") { + if (protocol === "grok-image") { + image.references.maxImages = 1; + image.references.maskSupported = false; + image.size = { parameter: "none", values: [], default: "auto", allowCustom: false }; + image.quality = { supported: false, values: [], default: "auto" }; + image.transparentBackground = { supported: false, default: false }; + image.responseFormat = { supported: true }; + image.outputFormat = { supported: false }; + image.maxOutputs = 1; + } else if (protocol === "volcengine-ark-image") { image.references.maskSupported = false; image.quality.supported = false; image.transparentBackground.supported = false; @@ -89,7 +98,7 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = " image.responseFormat.supported = false; image.outputFormat.supported = false; } - if (model.trim().toLowerCase().startsWith("grok-imagine-image")) { + if (protocol !== "grok-image" && model.trim().toLowerCase().startsWith("grok-imagine-image")) { image.references.maxImages = 0; image.references.maskSupported = false; image.size = { parameter: "none", values: [], default: "auto", allowCustom: false }; diff --git a/web/src/lib/model-protocols.ts b/web/src/lib/model-protocols.ts index e2ccdb403..2db4d5c19 100644 --- a/web/src/lib/model-protocols.ts +++ b/web/src/lib/model-protocols.ts @@ -2,6 +2,7 @@ export type ModelProtocol = | "chat-completion" | "openai-response" | "openai-image" + | "grok-image" | "volcengine-ark-image" | "volcengine-jimeng-image" | "openai-audio" @@ -30,6 +31,7 @@ export const MODEL_PROTOCOLS: ModelProtocolDefinition[] = [ { value: "chat-completion", label: "OpenAI Chat Completions", capability: "text", create: "POST /v1/chat/completions", contentType: "application/json", media: "文本与多模态消息" }, { value: "openai-response", label: "OpenAI Responses", capability: "text", create: "POST /v1/responses", contentType: "application/json", media: "文本与多模态输入" }, { value: "openai-image", label: "OpenAI Images", capability: "image", create: "POST /v1/images/generations", contentType: "application/json / multipart", media: "生成、编辑与参考图" }, + { value: "grok-image", label: "Grok Images", capability: "image", create: "POST /v1/images/generations / edits", contentType: "application/json", media: "文生图与单张 URL 参考图,不支持蒙版" }, { value: "volcengine-ark-image", label: "火山方舟图片", capability: "image", create: "POST /api/v3/images/generations", contentType: "application/json", media: "文生图与 image 参考图,不支持蒙版" }, { value: "volcengine-jimeng-image", label: "即梦官方图片", capability: "image", create: "POST CVSync2AsyncSubmitTask", poll: "POST CVSync2AsyncGetResult", contentType: "application/json + AK/SK 签名", media: "0-14 张参考图,模型标识填写 req_key" }, { value: "openai-audio", label: "OpenAI Audio", capability: "audio", create: "POST /v1/audio/speech", contentType: "application/json", media: "文本转语音" }, diff --git a/web/src/pages/settings/channel-video-pricing.tsx b/web/src/pages/settings/channel-video-pricing.tsx index 3a5811629..1fad37972 100644 --- a/web/src/pages/settings/channel-video-pricing.tsx +++ b/web/src/pages/settings/channel-video-pricing.tsx @@ -181,6 +181,7 @@ function defaultProtocolForModel(channel: ModelChannel, model: string): ModelPro if (channel.interfaceType) return channel.interfaceType; if (channel.apiFormat === "gemini" && modelMatchesCapability(model, "video")) return "gemini-veo"; if (modelMatchesCapability(model, "video")) return "newapi"; + if (modelOptionName(model).trim().toLowerCase().startsWith("grok-imagine-image")) return "grok-image"; if (modelMatchesCapability(model, "image")) return "openai-image"; return "chat-completion"; } diff --git a/web/src/services/api/image.ts b/web/src/services/api/image.ts index 5efa24927..fe08b5fbd 100644 --- a/web/src/services/api/image.ts +++ b/web/src/services/api/image.ts @@ -812,6 +812,24 @@ export async function requestGeneration(config: AiConfig, prompt: string, option throw new Error(readAxiosError(error, "请求失败")); } } + if (requestConfig.interfaceType === "grok-image") { + try { + const responseData = await postChannelJSON( + requestConfig, + aiApiUrl(requestConfig, "/images/generations"), + { + model: requestConfig.model, + prompt: withSystemPrompt(requestConfig, prompt), + n, + response_format: "url", + }, + options, + ); + return parseImagePayload(responseData); + } catch (error) { + throw new Error(readAxiosError(error, "Grok 图片生成失败")); + } + } const quality = imageProfile.quality.supported && normalizedImage.quality !== "auto" ? normalizeQuality(normalizedImage.quality) || normalizedImage.quality : undefined; const requestSize = resolveImageRequestSize(imageProfile, quality, normalizedImage.size); const isVolcengineArk = requestConfig.interfaceType === "volcengine-ark-image"; @@ -853,6 +871,12 @@ async function postChannelJSON(config: ReturnType( + requestConfig, + aiApiUrl(requestConfig, "/images/edits"), + { + model: requestConfig.model, + prompt: withSystemPrompt(requestConfig, requestPrompt), + image: { url: imageUrl }, + n, + response_format: "url", + }, + options, + ); + return parseImagePayload(response); + } catch (error) { + throw new Error(readAxiosError(error, "Grok 图片编辑失败")); + } + } if (requestConfig.interfaceType === "volcengine-ark-image") { if (mask) throw new Error("火山方舟图片协议不支持蒙版编辑,请移除蒙版后重试"); const quality = imageProfile.quality.supported && normalizedImage.quality !== "auto" ? normalizeQuality(normalizedImage.quality) || normalizedImage.quality : undefined; diff --git a/web/src/stores/use-config-store.ts b/web/src/stores/use-config-store.ts index 4e85da2c3..c2d166ec1 100644 --- a/web/src/stores/use-config-store.ts +++ b/web/src/stores/use-config-store.ts @@ -491,7 +491,7 @@ export function defaultBaseUrlForChannelInterface(interfaceType?: ChannelInterfa if (interfaceType === "gemini-veo") return GEMINI_BASE_URL; if (interfaceType === "volcengine-ark-image" || interfaceType === "volcengine-ark-video") return "https://ark.cn-beijing.volces.com/api/v3"; if (interfaceType === "volcengine-jimeng-image" || interfaceType === "volcengine-jimeng-video") return "https://visual.volcengineapi.com"; - if (interfaceType === "newapi" || interfaceType === "newapi-channel-1" || interfaceType === "newapi-channel-2" || interfaceType === "xai-video") return ""; + if (interfaceType === "grok-image" || interfaceType === "newapi" || interfaceType === "newapi-channel-1" || interfaceType === "newapi-channel-2" || interfaceType === "xai-video") return ""; return OPENAI_BASE_URL; }