diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 5f43152c5..d156c6c03 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -455,6 +455,7 @@ type AiProvider = { | "custom" | "local" | "gemini" + | "grsai-gpt-image" | "huanke-gemini" | "qianwen-video" | "doubao-video" @@ -1186,6 +1187,7 @@ const PROVIDER_TYPES = [ { value: "ruxa-image", label: "RUXA生图" }, { value: "fal-image", label: "FAL 生图模型" }, { value: "openai-image", label: "OpenAI 图片兼容" }, + { value: "grsai-gpt-image", label: "Grsai GPT Image" }, { value: "midjourney", label: "Midjourney" }, { value: "tripo-3d", label: "Tripo3D 模型生成" }, { value: "worldlabs-3d", label: "World Labs 3D 世界生成" }, @@ -1215,6 +1217,7 @@ const PROVIDER_DEFAULT_BASE_URL: Record = { "ruxa-image": "https://api.ruxa.ai", "fal-image": "https://fal.run", "openai-image": "https://api.openai.com/v1", + "grsai-gpt-image": "https://grsai.dakka.com.cn", midjourney: "https://your-midjourney-api.example.com", "fal-video": "https://fal.run", "tripo-3d": "https://api.tripo3d.ai/v2/openapi", @@ -3008,6 +3011,7 @@ export function AdminSettingsClient() { | "custom" | "local" | "gemini" + | "grsai-gpt-image" | "huanke-gemini" | "qianwen-video" | "doubao-video" diff --git a/app/api/admin/ai-providers/route.ts b/app/api/admin/ai-providers/route.ts index 7f9d35a25..bd474378e 100644 --- a/app/api/admin/ai-providers/route.ts +++ b/app/api/admin/ai-providers/route.ts @@ -15,6 +15,7 @@ const providerTypes = [ "custom", "local", "gemini", + "grsai-gpt-image", "huanke-gemini", "qianwen-video", "doubao-video", @@ -69,6 +70,7 @@ const WORLDLABS_3D_BASE_URL = "https://api.worldlabs.ai"; const KITTYCAD_CAD_BASE_URL = "https://api.zoo.dev"; const RUNWAY_BASE_URL = "https://api.dev.runwayml.com"; const KREA_BASE_URL = "https://api.krea.ai"; +const GRSAI_GPT_IMAGE_BASE_URL = "https://grsai.dakka.com.cn"; const RUNWAY_API_KEY_PATTERN = /^key_[0-9a-f]{128}$/; function normalizeProviderTypeForStorage( @@ -222,6 +224,8 @@ export async function POST(request: Request) { ? data.baseUrl || RUNWAY_BASE_URL : data.type === "krea" ? data.baseUrl || KREA_BASE_URL + : data.type === "grsai-gpt-image" + ? data.baseUrl || GRSAI_GPT_IMAGE_BASE_URL : data.baseUrl, updatedBy: session.user.id, }); diff --git a/components/chat.tsx b/components/chat.tsx index e7d099dad..4ca5a3b97 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -1132,7 +1132,12 @@ export function Chat({ let modelType: PendingGeneration["modelType"] = "other"; // 🆕 gemini-image 和 huanke-gemini 都视为 gemini 类型 - if (model?.providerType === "gemini" || model?.providerType === "gemini-image" || model?.providerType === "huanke-gemini") modelType = "gemini"; + if ( + model?.providerType === "gemini" || + model?.providerType === "gemini-image" || + model?.providerType === "grsai-gpt-image" || + model?.providerType === "huanke-gemini" + ) modelType = "gemini"; else if (model?.providerType === "volcano-vision") modelType = "volcano-vision"; else if ( isVideoModelDescriptor({ diff --git a/components/messages.tsx b/components/messages.tsx index 806ffaa25..dc2b02669 100644 --- a/components/messages.tsx +++ b/components/messages.tsx @@ -108,9 +108,14 @@ function PureMessages({ ); // 判断当前选中的模型类型 - // 🆕 包含 gemini 和 huanke-gemini 两种 Gemini 图片生成 provider + // 🆕 包含 Gemini 和 Grsai GPT Image 图片生成 provider const isGeminiModel = modelsData?.models?.some( - (m: DynamicModel) => m.id === selectedModelId && (m.providerType === "gemini" || m.providerType === "gemini-image" || m.providerType === "huanke-gemini") + (m: DynamicModel) => m.id === selectedModelId && ( + m.providerType === "gemini" || + m.providerType === "gemini-image" || + m.providerType === "grsai-gpt-image" || + m.providerType === "huanke-gemini" + ) ) ?? false; const isQianwenVideoModel = modelsData?.models?.some( diff --git a/lib/ai/dynamic-provider.ts b/lib/ai/dynamic-provider.ts index a07014d27..63e52e6e1 100644 --- a/lib/ai/dynamic-provider.ts +++ b/lib/ai/dynamic-provider.ts @@ -128,6 +128,15 @@ function getOrCreateProvider(provider: Pick> = { alibaba: "https://dashscope.aliyuncs.com/compatible-mode/v1", openai: "https://api.openai.com/v1", + "grsai-gpt-image": "https://grsai.dakka.com.cn", "huanke-gemini": "https://api.huandutech.com/v1", "qianwen-video": "https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation", "doubao-video": "https://ark.cn-beijing.volces.com/api/v3", diff --git a/lib/ai/providers/gemini-image.ts b/lib/ai/providers/gemini-image.ts index babb0b006..8b2e87978 100644 --- a/lib/ai/providers/gemini-image.ts +++ b/lib/ai/providers/gemini-image.ts @@ -418,8 +418,12 @@ async function sendGeminiRequest( imageSize?: string; } ): Promise { - // 使用 Nano Banana 原生端点(支持完整的图片生成参数) - const url = `${baseURL}/v1/draw/nano-banana`; + const isGptImageModel = /^gpt-image/i.test(modelId); + const apiLogPrefix = isGptImageModel ? "[Grsai GPT Image]" : "[Gemini Image]"; + // GPT Image 和 Nano Banana 在 Grsai 上是两套接口,不能混用同一个 endpoint。 + const url = isGptImageModel + ? `${baseURL}/v1/draw/completions` + : `${baseURL}/v1/draw/nano-banana`; // 提取最后一条消息的文本作为prompt const lastMessage = messages[messages.length - 1]; @@ -453,20 +457,20 @@ async function sendGeminiRequest( // 🆕 处理参考图:上传 Base64 到 S3 const processedUrls: string[] = []; if (urls.length > 0) { - console.log(`[Gemini Image] Processing ${urls.length} reference images...`); + console.log(`${apiLogPrefix} Processing ${urls.length} reference images...`); for (const url of urls) { const s3Url = await ensureImageUrl(url); processedUrls.push(s3Url); } } - console.log("[Gemini Image] Sending request to:", url); - console.log("[Gemini Image] Using model:", modelId); - console.log("[Gemini Image] Prompt:", prompt.substring(0, 100)); - console.log("[Gemini Image] Reference image count:", urls.length); - console.log("[Gemini Image] Image params:", imageParams); + console.log(`${apiLogPrefix} Sending request to:`, url); + console.log(`${apiLogPrefix} Using model:`, modelId); + console.log(`${apiLogPrefix} Prompt:`, prompt.substring(0, 100)); + console.log(`${apiLogPrefix} Reference image count:`, urls.length); + console.log(`${apiLogPrefix} Image params:`, imageParams); - // 使用 Nano Banana 原生格式 + // 使用对应模型的原生格式 const requestBody: any = { model: modelId, prompt: prompt, @@ -477,19 +481,24 @@ async function sendGeminiRequest( requestBody.urls = processedUrls; } - // 添加 Nano Banana 特定参数 - if (imageParams?.aspectRatio) { - requestBody.aspectRatio = imageParams.aspectRatio; - } - if (imageParams?.imageSize) { - requestBody.imageSize = imageParams.imageSize; + if (isGptImageModel) { + requestBody.size = imageParams?.aspectRatio || "auto"; + requestBody.shutProgress = false; + } else { + // 添加 Nano Banana 特定参数 + if (imageParams?.aspectRatio) { + requestBody.aspectRatio = imageParams.aspectRatio; + } + if (imageParams?.imageSize) { + requestBody.imageSize = imageParams.imageSize; + } } - console.log("[Gemini Image] Request body (truncated):", JSON.stringify(requestBody).substring(0, 500) + "..."); + console.log(`${apiLogPrefix} Request body (truncated):`, JSON.stringify(requestBody).substring(0, 500) + "..."); // 根据分辨率动态设置超时时间 const timeout = imageParams?.imageSize === "4K" ? 300000 : 120000; // 4K: 5分钟, 其他: 2分钟 - console.log(`[Gemini Image] Timeout set to ${timeout/1000} seconds for ${imageParams?.imageSize || "default"}`); + console.log(`${apiLogPrefix} Timeout set to ${timeout/1000} seconds for ${imageParams?.imageSize || "default"}`); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); @@ -509,14 +518,14 @@ async function sendGeminiRequest( if (!response.ok) { const errorText = await response.text(); - console.error("[Gemini Image] Request failed:", response.status, errorText); + console.error(`${apiLogPrefix} Request failed:`, response.status, errorText); throw new Error( - `Gemini API error: ${response.status} ${response.statusText}` + `${isGptImageModel ? "Grsai GPT Image" : "Gemini"} API error: ${response.status} ${response.statusText}` ); } // 处理流式响应(SSE格式) - console.log("[Gemini Image] Reading stream response..."); + console.log(`${apiLogPrefix} Reading stream response...`); const reader = response.body?.getReader(); const decoder = new TextDecoder(); let buffer = ""; diff --git a/lib/canvas/generate-image.ts b/lib/canvas/generate-image.ts index 70a401dc0..3b8e76c8d 100644 --- a/lib/canvas/generate-image.ts +++ b/lib/canvas/generate-image.ts @@ -41,6 +41,7 @@ export interface CanvasGenerateImageResult { const SUPPORTED_CANVAS_PROVIDER_TYPES = new Set([ "gemini", + "grsai-gpt-image", "alibaba-image", "bfl-image", "ruxa-image", @@ -92,6 +93,7 @@ function getProviderBatchLimit(providerType: string) { case "krea": return 4; case "gemini": + case "grsai-gpt-image": return 2; case "alibaba-image": case "fal-image": @@ -173,7 +175,7 @@ export async function generateCanvasImage( } if (!SUPPORTED_CANVAS_PROVIDER_TYPES.has(fallbackProvider.type)) { throw new Error( - "当前图片模型类型暂不支持画布生成,请在后台选择 Gemini/阿里/BFL/RUXA/FAL/OpenAI/Runway/Krea 图片模型。" + "当前图片模型类型暂不支持画布生成,请在后台选择 Gemini/Grsai GPT Image/阿里/BFL/RUXA/FAL/OpenAI/Runway/Krea 图片模型。" ); } if (fallbackModel.model.id !== selectedModel.model.id) { @@ -252,7 +254,7 @@ export async function generateCanvasImage( | ((batchImageCount: number) => Promise) | null = null; - if (provider.type === "gemini") { + if (provider.type === "gemini" || provider.type === "grsai-gpt-image") { const geminiProvider = createGeminiImage({ apiKey: provider.apiKey, baseURL: provider.baseUrl, @@ -392,7 +394,7 @@ export async function generateCanvasImage( }; } else { throw new Error( - `不支持的图片生成模型类型: ${provider.type || "unknown"},请改用 Gemini/阿里/BFL/RUXA/FAL/OpenAI/Midjourney/Runway/Krea 图片模型` + `不支持的图片生成模型类型: ${provider.type || "unknown"},请改用 Gemini/Grsai GPT Image/阿里/BFL/RUXA/FAL/OpenAI/Midjourney/Runway/Krea 图片模型` ); } diff --git a/lib/db/migrate.ts b/lib/db/migrate.ts index 8545bd213..e0f26efed 100644 --- a/lib/db/migrate.ts +++ b/lib/db/migrate.ts @@ -13,6 +13,7 @@ config({ const providerBaseUrlDefaults = [ { type: "alibaba", baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1" }, { type: "openai", baseUrl: "https://api.openai.com/v1" }, + { type: "grsai-gpt-image", baseUrl: "https://grsai.dakka.com.cn" }, { type: "huanke-gemini", baseUrl: "https://api.huandutech.com/v1" }, { type: "qianwen-video", @@ -96,7 +97,7 @@ const runMigrate = async () => { ADD CONSTRAINT "AiProvider_type_check" CHECK (type IN ( 'alibaba', 'openai', 'custom', 'local', 'gemini', - 'huanke-gemini', 'qianwen-video', 'doubao-video', 'ruxa-video', 'volcano-vision', 'volcano-video', 'alibaba-image', 'bfl-image', 'ruxa-image', 'fal-image', 'openai-image', 'midjourney', 'fal-video', 'zhipu', 'tripo-3d', 'worldlabs-3d', 'kittycad-cad', 'runway', 'krea' + 'grsai-gpt-image', 'huanke-gemini', 'qianwen-video', 'doubao-video', 'ruxa-video', 'volcano-vision', 'volcano-video', 'alibaba-image', 'bfl-image', 'ruxa-image', 'fal-image', 'openai-image', 'midjourney', 'fal-video', 'zhipu', 'tripo-3d', 'worldlabs-3d', 'kittycad-cad', 'runway', 'krea' )); `); await db.execute(sql` diff --git a/lib/db/queries.ts b/lib/db/queries.ts index 810e5dff5..d96b0467f 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -3750,7 +3750,7 @@ export async function createAiProvider({ updatedBy, }: { name: string; - type: "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "kittycad-cad" | "runway" | "krea"; + type: "alibaba" | "openai" | "custom" | "local" | "gemini" | "grsai-gpt-image" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "kittycad-cad" | "runway" | "krea"; baseUrl: string; apiKey: string; isEnabled?: boolean; @@ -3788,7 +3788,7 @@ export async function updateAiProvider({ }: { id: string; name?: string; - type?: "alibaba" | "openai" | "custom" | "local" | "gemini" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "kittycad-cad" | "runway" | "krea"; + type?: "alibaba" | "openai" | "custom" | "local" | "gemini" | "grsai-gpt-image" | "huanke-gemini" | "qianwen-video" | "doubao-video" | "ruxa-video" | "volcano-vision" | "volcano-video" | "alibaba-image" | "bfl-image" | "ruxa-image" | "fal-image" | "openai-image" | "midjourney" | "fal-video" | "zhipu" | "tripo-3d" | "worldlabs-3d" | "kittycad-cad" | "runway" | "krea"; baseUrl?: string; apiKey?: string; isEnabled?: boolean; diff --git a/lib/db/schema.ts b/lib/db/schema.ts index bb187f203..bf60b5efb 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -276,7 +276,7 @@ export type ToolConfig = InferSelectModel; export const aiProvider = pgTable("AiProvider", { id: uuid("id").primaryKey().notNull().defaultRandom(), name: varchar("name", { length: 128 }).notNull(), // 提供商名称,如 "阿里云通义千问" - type: varchar("type", { enum: ["alibaba", "openai", "custom", "local", "gemini", "huanke-gemini", "qianwen-video", "doubao-video", "ruxa-video", "volcano-vision", "volcano-video", "alibaba-image", "ruxa-image", "fal-image", "openai-image", "midjourney", "fal-video", "zhipu", "tripo-3d", "worldlabs-3d", "kittycad-cad", "runway", "krea", "bfl-image"] }) + type: varchar("type", { enum: ["alibaba", "openai", "custom", "local", "gemini", "grsai-gpt-image", "huanke-gemini", "qianwen-video", "doubao-video", "ruxa-video", "volcano-vision", "volcano-video", "alibaba-image", "ruxa-image", "fal-image", "openai-image", "midjourney", "fal-video", "zhipu", "tripo-3d", "worldlabs-3d", "kittycad-cad", "runway", "krea", "bfl-image"] }) .notNull() .default("custom"), // 提供商类型 baseUrl: text("baseUrl").notNull(), // API 基础 URL