diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 528b64c4e4..50d9aa248c 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -111,6 +111,7 @@ export enum IpcChannel { File_WriteWithId = 'file:writeWithId', File_SaveImage = 'file:saveImage', File_Base64Image = 'file:base64Image', + File_SaveBase64Image = 'file:saveBase64Image', File_Download = 'file:download', File_Copy = 'file:copy', File_BinaryImage = 'file:binaryImage', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 408af62c33..94de990c5d 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -249,6 +249,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.File_WriteWithId, fileManager.writeFileWithId) ipcMain.handle(IpcChannel.File_SaveImage, fileManager.saveImage) ipcMain.handle(IpcChannel.File_Base64Image, fileManager.base64Image) + ipcMain.handle(IpcChannel.File_SaveBase64Image, fileManager.saveBase64Image) ipcMain.handle(IpcChannel.File_Base64File, fileManager.base64File) ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile) ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile) diff --git a/src/main/services/FileStorage.ts b/src/main/services/FileStorage.ts index 9b097e96ef..0ea36abc09 100644 --- a/src/main/services/FileStorage.ts +++ b/src/main/services/FileStorage.ts @@ -268,6 +268,51 @@ class FileStorage { } } + public saveBase64Image = async (_: Electron.IpcMainInvokeEvent, base64Data: string): Promise => { + try { + if (!base64Data) { + throw new Error('Base64 data is required') + } + + // 移除 base64 头部信息(如果存在) + const base64String = base64Data.replace(/^data:.*;base64,/, '') + const buffer = Buffer.from(base64String, 'base64') + const uuid = uuidv4() + const ext = '.png' + const destPath = path.join(this.storageDir, uuid + ext) + + logger.info('[FileStorage] Saving base64 image:', { + storageDir: this.storageDir, + destPath, + bufferSize: buffer.length + }) + + // 确保目录存在 + if (!fs.existsSync(this.storageDir)) { + fs.mkdirSync(this.storageDir, { recursive: true }) + } + + await fs.promises.writeFile(destPath, buffer) + + const fileMetadata: FileType = { + id: uuid, + origin_name: uuid + ext, + name: uuid + ext, + path: destPath, + created_at: new Date().toISOString(), + size: buffer.length, + ext: ext.slice(1), + type: getFileType(ext), + count: 1 + } + + return fileMetadata + } catch (error) { + logger.error('[FileStorage] Failed to save base64 image:', error) + throw error + } + } + public base64File = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: string; mime: string }> => { const filePath = path.join(this.storageDir, id) const buffer = await fs.promises.readFile(filePath) diff --git a/src/preload/index.ts b/src/preload/index.ts index 2a69ae908e..f5a4a85f12 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -76,7 +76,9 @@ const api = { selectFolder: () => ipcRenderer.invoke(IpcChannel.File_SelectFolder), saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data), base64Image: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64Image, fileId), - download: (url: string, isUseContentType?: boolean) => ipcRenderer.invoke(IpcChannel.File_Download, url, isUseContentType), + saveBase64Image: (data: string) => ipcRenderer.invoke(IpcChannel.File_SaveBase64Image, data), + download: (url: string, isUseContentType?: boolean) => + ipcRenderer.invoke(IpcChannel.File_Download, url, isUseContentType), copy: (fileId: string, destPath: string) => ipcRenderer.invoke(IpcChannel.File_Copy, fileId, destPath), binaryImage: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryImage, fileId), base64File: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64File, fileId), diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 7ec3bdaa02..ec1b3c7cdb 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -820,12 +820,12 @@ "seed_desc_tip": "The same seed and prompt can generate similar images, setting -1 will generate different results each time", "title": "Images", "magic_prompt_option": "Magic Prompt", - "model": "Model Version", + "model": "Model", "aspect_ratio": "Aspect Ratio", "style_type": "Style", "rendering_speed": "Rendering Speed", "learn_more": "Learn More", - "paint_course":"tutorial", + "paint_course": "tutorial", "prompt_placeholder_edit": "Enter your image description, text drawing uses \"double quotes\" to wrap", "proxy_required": "Currently, you need to open a proxy to view the generated images, it will be supported in the future", "image_file_required": "Please upload an image first", @@ -846,6 +846,29 @@ "turbo": "Turbo", "quality": "Quality" }, + "quality_options": { + "auto": "Auto", + "low": "Low", + "medium": "Medium", + "high": "High" + }, + "moderation_options": { + "auto": "Auto", + "low": "Low" + }, + "background_options": { + "auto": "Auto", + "transparent": "Transparent", + "opaque": "Opaque" + }, + "aspect_ratios": { + "square": "Square", + "portrait": "Portrait", + "landscape": "Landscape" + }, + "quality": "Quality", + "moderation": "Moderation", + "background": "Background", "mode": { "generate": "Draw", "edit": "Edit", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index a46c13267e..7535d37709 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -820,12 +820,12 @@ "seed_desc_tip": "同じシードとプロンプトで類似した画像を生成できますが、-1 に設定すると毎回異なる結果が生成されます", "title": "画像", "magic_prompt_option": "プロンプト強化", - "model": "モデルバージョン", + "model": "モデル", "aspect_ratio": "画幅比例", "style_type": "スタイル", "learn_more": "詳しくはこちら", "prompt_placeholder_edit": "画像の説明を入力します。テキスト描画には '二重引用符' を使用します", - "paint_course":"チュートリアル", + "paint_course": "チュートリアル", "proxy_required": "現在、プロキシを開く必要があります。これは、将来サポートされる予定です", "image_file_required": "画像を先にアップロードしてください", "image_file_retry": "画像を先にアップロードしてください", @@ -844,6 +844,29 @@ "turbo": "高速", "quality": "高品質" }, + "quality_options": { + "auto": "自動", + "low": "低", + "medium": "中", + "high": "高" + }, + "moderation_options": { + "auto": "自動", + "low": "低" + }, + "background_options": { + "auto": "自動", + "transparent": "透明", + "opaque": "不透明" + }, + "aspect_ratios": { + "square": "正方形", + "portrait": "縦図", + "landscape": "横図" + }, + "quality": "品質", + "moderation": "敏感度", + "background": "背景", "mode": { "generate": "画像生成", "edit": "部分編集", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index c8959a4a05..5550262b27 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -820,13 +820,13 @@ "seed_desc_tip": "Одинаковые сиды и промпты могут генерировать похожие изображения, установка -1 будет создавать разные результаты каждый раз", "title": "Изображения", "magic_prompt_option": "Улучшение промпта", - "model": "Версия", + "model": "Модель", "aspect_ratio": "Пропорции изображения", "style_type": "Стиль", "rendering_speed": "Скорость рендеринга", "learn_more": "Узнать больше", "prompt_placeholder_edit": "Введите ваше описание изображения, текстовая отрисовка использует двойные кавычки для обертки", - "paint_course":"Руководство / Учебник", + "paint_course": "Руководство / Учебник", "proxy_required": "Сейчас необходимо открыть прокси для просмотра сгенерированных изображений, в будущем будет поддерживаться прямое соединение", "image_file_required": "Пожалуйста, сначала загрузите изображение", "image_file_retry": "Пожалуйста, сначала загрузите изображение", @@ -841,11 +841,34 @@ "3d": "3D", "anime": "Аниме" }, + "quality_options": { + "auto": "Авто", + "low": "Низкое", + "medium": "Среднее", + "high": "Высокое" + }, + "moderation_options": { + "auto": "Авто", + "low": "Низкое" + }, + "background_options": { + "auto": "Авто", + "transparent": "Прозрачный", + "opaque": "Непрозрачный" + }, "rendering_speeds": { "default": "По умолчанию", "turbo": "Быстро", "quality": "Качественно" }, + "aspect_ratios": { + "square": "Квадрат", + "portrait": "Портрет", + "landscape": "Пейзаж" + }, + "quality": "Качество", + "moderation": "Сенсорность", + "background": "Фон", "mode": { "generate": "Рисование", "edit": "Редактирование", @@ -892,7 +915,6 @@ "seed_tip": "Контролирует случайный характер увеличения изображений для воспроизводимых результатов", "magic_prompt_option_tip": "Улучшает увеличение изображений с помощью интеллектуального оптимизирования промптов" }, - "rendering_speed": "Скорость рендеринга", "text_desc_required": "Пожалуйста, сначала введите описание изображения" }, "prompts": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 5d08e7a616..f224e15466 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -820,12 +820,12 @@ "seed_desc_tip": "相同的种子和提示词可以生成相似的图片,设置 -1 每次生成都不一样", "title": "图片", "magic_prompt_option": "提示词增强", - "model": "版本", + "model": "模型", "aspect_ratio": "画幅比例", "style_type": "风格", "rendering_speed": "渲染速度", "learn_more": "了解更多", - "paint_course":"教程", + "paint_course": "教程", "prompt_placeholder_edit": "输入你的图片描述,文本绘制用 \"双引号\" 包裹", "proxy_required": "目前需要打开代理才能查看生成图片,后续会支持国内直连", "image_file_required": "请先上传图片", @@ -846,11 +846,34 @@ "turbo": "快速", "quality": "高质量" }, + "quality_options": { + "auto": "自动", + "low": "低", + "medium": "中", + "high": "高" + }, + "moderation_options": { + "auto": "自动", + "low": "低" + }, + "background_options": { + "auto": "自动", + "transparent": "透明", + "opaque": "不透明" + }, + "aspect_ratios": { + "square": "方形", + "portrait": "竖图", + "landscape": "横图" + }, + "quality": "质量", + "moderation": "敏感度", + "background": "背景", "mode": { "generate": "绘图", "edit": "编辑", "remix": "混合", - "upscale": "放大" + "upscale": "高清增强" }, "generate": { "model_tip": "模型版本:V3 为最新版本,V2 为之前版本,V2A 为快速模型、V_1 为初代模型,_TURBO 为加速版本", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 95b710dab2..4b702dc955 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -821,12 +821,12 @@ "seed_desc_tip": "相同的種子和提示詞可以生成相似的圖片,設置 -1 每次生成都不一樣", "title": "繪圖", "magic_prompt_option": "提示詞增強", - "model": "版本", + "model": "模型", "aspect_ratio": "畫幅比例", "style_type": "風格", "learn_more": "了解更多", "prompt_placeholder_edit": "輸入你的圖片描述,文本繪製用 '雙引號' 包裹", - "paint_course":"教程", + "paint_course": "教程", "proxy_required": "目前需要打開代理才能查看生成圖片,後續會支持國內直連", "image_file_required": "請先上傳圖片", "image_file_retry": "請重新上傳圖片", @@ -846,6 +846,29 @@ "turbo": "快速", "quality": "高品質" }, + "quality_options": { + "auto": "自動", + "low": "低", + "medium": "中", + "high": "高" + }, + "moderation_options": { + "auto": "自動", + "low": "低" + }, + "background_options": { + "auto": "自動", + "transparent": "透明", + "opaque": "不透明" + }, + "aspect_ratios": { + "square": "方形", + "portrait": "豎圖", + "landscape": "橫圖" + }, + "quality": "品質", + "moderation": "敏感度", + "background": "背景", "mode": { "generate": "繪圖", "edit": "編輯", diff --git a/src/renderer/src/pages/paintings/AihubmixPage.tsx b/src/renderer/src/pages/paintings/AihubmixPage.tsx index f8b69e1466..666ed1d72f 100644 --- a/src/renderer/src/pages/paintings/AihubmixPage.tsx +++ b/src/renderer/src/pages/paintings/AihubmixPage.tsx @@ -22,17 +22,16 @@ import { Avatar, Button, Input, InputNumber, Radio, Segmented, Select, Slider, S import TextArea from 'antd/es/input/TextArea' import { Info } from 'lucide-react' import type { FC } from 'react' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocation, useNavigate } from 'react-router-dom' import styled from 'styled-components' import SendMessageButton from '../home/Inputbar/SendMessageButton' import { SettingHelpLink, SettingTitle } from '../settings' -import Artboard from './Artboard' -import { type ConfigItem, createModeConfigs } from './config/aihubmixConfig' -import { DEFAULT_PAINTING } from './config/constants' -import PaintingsList from './PaintingsList' +import Artboard from './components/Artboard' +import PaintingsList from './components/PaintingsList' +import { type ConfigItem, createModeConfigs, DEFAULT_PAINTING } from './config/aihubmixConfig' // 使用函数创建配置项 const modeConfigs = createModeConfigs() @@ -74,12 +73,13 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { { label: t('paintings.mode.upscale'), value: 'upscale' } ] - const getNewPainting = () => { + const getNewPainting = useCallback(() => { return { ...DEFAULT_PAINTING, + model: mode === 'generate' ? 'gpt-image-1' : 'V_3', id: uuid() } - } + }, [mode]) const textareaRef = useRef(null) @@ -89,6 +89,47 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { updatePainting(mode, updatedPainting) } + const handleError = (error: unknown) => { + if (error instanceof Error && error.name !== 'AbortError') { + window.modal.error({ + content: getErrorMessage(error), + centered: true + }) + } + } + + const downloadImages = async (urls: string[]) => { + const downloadedFiles = await Promise.all( + urls.map(async (url) => { + try { + if (!url?.trim()) { + console.error('图像URL为空,可能是提示词违禁') + window.message.warning({ + content: t('message.empty_url'), + key: 'empty-url-warning' + }) + return null + } + return await window.api.file.download(url) + } catch (error) { + console.error('下载图像失败:', error) + if ( + error instanceof Error && + (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) + ) { + window.message.warning({ + content: t('message.empty_url'), + key: 'empty-url-warning' + }) + } + return null + } + }) + ) + + return downloadedFiles.filter((file): file is FileType => file !== null) + } + const onGenerate = async () => { if (painting.files.length > 0) { const confirmed = await window.modal.confirm({ @@ -129,11 +170,11 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { dispatch(setGenerating(true)) let body: string | FormData = '' - const headers: Record = { + let headers: Record = { 'Api-Key': aihubmixProvider.apiKey } + let url = aihubmixProvider.apiHost + `/ideogram/` + mode - // 不使用 AiProvider 的通用规则,而是直接调用自定义接口 try { if (mode === 'generate') { if (painting.model === 'V_3') { @@ -214,67 +255,47 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { console.log('V3 API响应:', data) const urls = data.data.map((item) => item.url) - // Rest of the code for handling image downloads is the same if (urls.length > 0) { - const downloadedFiles = await Promise.all( - urls.map(async (url) => { - try { - // 检查URL是否为空 - if (!url || url.trim() === '') { - console.error('图像URL为空,可能是提示词违禁') - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) - return null - } - return await window.api.file.download(url) - } catch (error) { - console.error('下载图像失败:', error) - // 检查是否是URL解析错误 - if ( - error instanceof Error && - (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) - ) { - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) - } - return null - } - }) - ) - - const validFiles = downloadedFiles.filter((file): file is FileType => file !== null) + const validFiles = await downloadImages(urls) await FileManager.addFiles(validFiles) updatePaintingState({ files: validFiles, urls }) } return } catch (error: unknown) { - if (error instanceof Error && error.name !== 'AbortError') { - window.modal.error({ - content: getErrorMessage(error), - centered: true - }) - } + handleError(error) } finally { setIsLoading(false) dispatch(setGenerating(false)) setAbortController(null) } } else { - // Existing V1/V2 API - const requestData = { - image_request: { + let requestData: any = {} + if (painting.model === 'gpt-image-1') { + requestData = { prompt, model: painting.model, - aspect_ratio: painting.aspectRatio, - num_images: painting.numImages, - style_type: painting.styleType, - seed: painting.seed ? +painting.seed : undefined, - negative_prompt: painting.negativePrompt || undefined, - magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF' + size: painting.size === 'auto' ? undefined : painting.size, + n: painting.n, + quality: painting.quality, + moderation: painting.moderation + } + url = aihubmixProvider.apiHost + `/v1/images/generations` + headers = { + Authorization: `Bearer ${aihubmixProvider.apiKey}` + } + } else { + // Existing V1/V2 API + requestData = { + image_request: { + prompt, + model: painting.model, + aspect_ratio: painting.aspectRatio, + num_images: painting.numImages, + style_type: painting.styleType, + seed: painting.seed ? +painting.seed : undefined, + negative_prompt: painting.negativePrompt || undefined, + magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF' + } } } body = JSON.stringify(requestData) @@ -352,37 +373,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { // Handle the downloaded images if (urls.length > 0) { - const downloadedFiles = await Promise.all( - urls.map(async (url) => { - try { - // 检查URL是否为空 - if (!url || url.trim() === '') { - console.error('图像URL为空,可能是提示词违禁') - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) - return null - } - return await window.api.file.download(url) - } catch (error) { - console.error('下载图像失败:', error) - // 检查是否是URL解析错误 - if ( - error instanceof Error && - (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) - ) { - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) - } - return null - } - }) - ) - - const validFiles = downloadedFiles.filter((file): file is FileType => file !== null) + const validFiles = await downloadImages(urls) await FileManager.addFiles(validFiles) updatePaintingState({ files: validFiles, urls }) } @@ -405,119 +396,6 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { form.append('image_file', fileMap[painting.imageFile] as unknown as Blob) body = form } - } else if (mode === 'edit') { - if (!painting.imageFile) { - window.modal.error({ - content: t('paintings.image_file_required'), - centered: true - }) - return - } - if (!fileMap[painting.imageFile]) { - window.modal.error({ - content: t('paintings.image_file_retry'), - centered: true - }) - return - } - - if (painting.model === 'V_3') { - // V3 Edit API - const formData = new FormData() - formData.append('prompt', prompt) - formData.append('rendering_speed', painting.renderingSpeed || 'DEFAULT') - formData.append('num_images', String(painting.numImages || 1)) - - if (painting.styleType) { - formData.append('style_type', painting.styleType) - } - - if (painting.seed) { - formData.append('seed', painting.seed) - } - - if (painting.magicPromptOption !== undefined) { - formData.append('magic_prompt', painting.magicPromptOption ? 'ON' : 'OFF') - } - - // Add the image file - formData.append('image', fileMap[painting.imageFile] as unknown as Blob) - - // Add the mask if available - if (painting.mask) { - formData.append('mask', painting.mask as unknown as Blob) - } - - body = formData - // For V3 Edit endpoint - const response = await fetch(`${aihubmixProvider.apiHost}/ideogram/v1/ideogram-v3/edit`, { - method: 'POST', - headers: { 'Api-Key': aihubmixProvider.apiKey }, - body - }) - - if (!response.ok) { - const errorData = await response.json() - console.error('V3 Edit API错误:', errorData) - throw new Error(errorData.error?.message || '图像编辑失败') - } - - const data = await response.json() - console.log('V3 Edit API响应:', data) - const urls = data.data.map((item) => item.url) - - // Handle the downloaded images - if (urls.length > 0) { - const downloadedFiles = await Promise.all( - urls.map(async (url) => { - try { - // 检查URL是否为空 - if (!url || url.trim() === '') { - console.error('图像URL为空,可能是提示词违禁') - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) - return null - } - return await window.api.file.download(url) - } catch (error) { - console.error('下载图像失败:', error) - // 检查是否是URL解析错误 - if ( - error instanceof Error && - (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) - ) { - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) - } - return null - } - }) - ) - - const validFiles = downloadedFiles.filter((file): file is FileType => file !== null) - await FileManager.addFiles(validFiles) - updatePaintingState({ files: validFiles, urls }) - } - return - } else { - // Existing V1/V2 API for edit - const form = new FormData() - const imageRequest: Record = { - prompt, - model: painting.model, - style_type: painting.styleType, - num_images: painting.numImages, - seed: painting.seed ? +painting.seed : undefined, - magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF' - } - form.append('image_request', JSON.stringify(imageRequest)) - form.append('image_file', fileMap[painting.imageFile] as unknown as Blob) - body = form - } } else if (mode === 'upscale') { if (!painting.imageFile) { window.modal.error({ @@ -549,9 +427,9 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { } // 只针对非V3模型使用通用接口 - if (!painting.model?.includes('V_3')) { + if (!painting.model?.includes('V_3') || mode === 'upscale') { // 直接调用自定义接口 - const response = await fetch(`${aihubmixProvider.apiHost}/ideogram/${mode}`, { method: 'POST', headers, body }) + const response = await fetch(url, { method: 'POST', headers, body }) if (!response.ok) { const errorData = await response.json() @@ -561,53 +439,27 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { const data = await response.json() console.log('通用API响应:', data) - const urls = data.data.map((item) => item.url) + const urls = data.data.filter((item) => item.url).map((item) => item.url) + const base64s = data.data.filter((item) => item.b64_json).map((item) => item.b64_json) if (urls.length > 0) { - const downloadedFiles = await Promise.all( - urls.map(async (url) => { - try { - // 检查URL是否为空 - if (!url || url.trim() === '') { - console.error('图像URL为空,可能是提示词违禁') - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) - return null - } - return await window.api.file.download(url) - } catch (error) { - console.error('下载图像失败:', error) - // 检查是否是URL解析错误 - if ( - error instanceof Error && - (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) - ) { - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) - } - return null - } + const validFiles = await downloadImages(urls) + await FileManager.addFiles(validFiles) + updatePaintingState({ files: validFiles, urls }) + } + + if (base64s?.length > 0) { + const validFiles = await Promise.all( + base64s.map(async (base64) => { + return await window.api.file.saveBase64Image(base64) }) ) - - const validFiles = downloadedFiles.filter((file): file is FileType => file !== null) - await FileManager.addFiles(validFiles) - - updatePaintingState({ files: validFiles, urls }) + updatePaintingState({ files: validFiles, urls: validFiles.map((file) => file.name) }) } } } catch (error: unknown) { - if (error instanceof Error && error.name !== 'AbortError') { - window.modal.error({ - content: getErrorMessage(error), - centered: true - }) - } + handleError(error) } finally { setIsLoading(false) dispatch(setGenerating(false)) @@ -617,43 +469,15 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { const handleRetry = async (painting: PaintingAction) => { setIsLoading(true) - const downloadedFiles = await Promise.all( - painting.urls.map(async (url) => { - try { - // 检查URL是否为空 - if (!url || url.trim() === '') { - console.error('图像URL为空,可能是提示词违禁') - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) - return null - } - return await window.api.file.download(url) - } catch (error) { - console.error('下载图像失败:', error) - // 检查是否是URL解析错误 - if ( - error instanceof Error && - (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) - ) { - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) - } - setIsLoading(false) - return null - } - }) - ) - - const validFiles = downloadedFiles.filter((file): file is FileType => file !== null) - - await FileManager.addFiles(validFiles) - - updatePaintingState({ files: validFiles, urls: painting.urls }) - setIsLoading(false) + try { + const validFiles = await downloadImages(painting.urls) + await FileManager.addFiles(validFiles) + updatePaintingState({ files: validFiles, urls: painting.urls }) + } catch (error) { + handleError(error) + } finally { + setIsLoading(false) + } } const onCancel = () => { @@ -754,20 +578,8 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { } // 渲染配置项的函数 - const renderConfigItem = (item: ConfigItem, index: number) => { + const renderConfigForm = (item: ConfigItem) => { switch (item.type) { - case 'title': { - return ( - - {t(item.title!)} - {item.tooltip && ( - - - - )} - - ) - } case 'select': { // 处理函数类型的disabled属性 const isDisabled = typeof item.disabled === 'function' ? item.disabled(item, painting) : item.disabled @@ -786,10 +598,11 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { return ( updatePaintingState({ [item.key!]: e.target.value })} - suffix={ - - } - /> - ) - } + case 'input': return ( updatePaintingState({ [item.key!]: e.target.value })} - suffix={item.suffix} + suffix={ + item.key === 'seed' ? ( + + ) : ( + item.suffix + ) + } /> ) - } - case 'inputNumber': { + case 'inputNumber': return ( updatePaintingState({ [item.key!]: v })} /> ) - } - case 'textarea': { + case 'textarea': return (