diff --git a/.yarn/patches/@langchain-core-npm-0.3.44-41d5c3cb0a.patch b/.yarn/patches/@langchain-core-npm-0.3.44-41d5c3cb0a.patch new file mode 100644 index 000000000..e1be29475 --- /dev/null +++ b/.yarn/patches/@langchain-core-npm-0.3.44-41d5c3cb0a.patch @@ -0,0 +1,37 @@ +diff --git a/dist/utils/tiktoken.js b/dist/utils/tiktoken.js +index 8e41ee6f00f2f9c7fa2c59fa2b2f4297634b97aa..6708abfaabee108b8967c3fbd7fccc8dfb95e693 100644 +--- a/dist/utils/tiktoken.js ++++ b/dist/utils/tiktoken.js +@@ -3,18 +3,20 @@ import { AsyncCaller } from "./async_caller.js"; + const cache = {}; + const caller = /* #__PURE__ */ new AsyncCaller({}); + export async function getEncoding(encoding) { +- if (!(encoding in cache)) { +- cache[encoding] = caller +- .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`) +- .then((res) => res.json()) +- .then((data) => new Tiktoken(data)) +- .catch((e) => { +- delete cache[encoding]; +- throw e; +- }); +- } +- return await cache[encoding]; ++ // if (!(encoding in cache)) { ++ // cache[encoding] = caller ++ // .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`) ++ // .then((res) => res.json()) ++ // .then((data) => new Tiktoken(data)) ++ // .catch((e) => { ++ // delete cache[encoding]; ++ // throw e; ++ // }); ++ // } ++ // return await cache[encoding]; ++ throw new Error("TikToken Not implemented"); + } + export async function encodingForModel(model) { +- return getEncoding(getEncodingNameForModel(model)); ++ // return getEncoding(getEncodingNameForModel(model)); ++ throw new Error("TikToken Not implemented"); + } diff --git a/electron-builder.yml b/electron-builder.yml index 5e1f97f00..631b483cd 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -36,8 +36,11 @@ files: - '!**/*.{spec,test}.{js,jsx,ts,tsx}' - '!**/*.min.*.map' - '!**/*.d.ts' + - '!**/dist/es6/**' + - '!**/dist/demo/**' + - '!**/amd/**' - '!**/{.DS_Store,Thumbs.db,thumbs.db,__pycache__}' - - '!**/{LICENSE,LICENSE.txt,LICENSE-MIT.txt,*.LICENSE.txt,NOTICE.txt,README.md,readme.md,CHANGELOG.md}' + - '!**/{LICENSE,license,LICENSE.*,*.LICENSE.txt,NOTICE.txt,README.md,readme.md,CHANGELOG.md}' - '!node_modules/rollup-plugin-visualizer' - '!node_modules/js-tiktoken' - '!node_modules/@tavily/core/node_modules/js-tiktoken' diff --git a/electron.vite.config.ts b/electron.vite.config.ts index b6e0148e8..7f4a4e3a6 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -9,25 +9,7 @@ const visualizerPlugin = (type: 'renderer' | 'main') => { export default defineConfig({ main: { - plugins: [ - externalizeDepsPlugin({ - exclude: [ - '@cherrystudio/embedjs', - '@cherrystudio/embedjs-openai', - '@cherrystudio/embedjs-loader-web', - '@cherrystudio/embedjs-loader-markdown', - '@cherrystudio/embedjs-loader-msoffice', - '@cherrystudio/embedjs-loader-xml', - '@cherrystudio/embedjs-loader-pdf', - '@cherrystudio/embedjs-loader-sitemap', - '@cherrystudio/embedjs-libsql', - '@cherrystudio/embedjs-loader-image', - 'p-queue', - 'webdav' - ] - }), - ...visualizerPlugin('main') - ], + plugins: [externalizeDepsPlugin(), ...visualizerPlugin('main')], resolve: { alias: { '@main': resolve('src/main'), diff --git a/package.json b/package.json index a22decabb..08683e0f8 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "publish": "yarn build:check && yarn release patch push", "pulish:artifacts": "cd packages/artifacts && npm publish && cd -", "generate:agents": "yarn workspace @cherry-studio/database agents", - "generate:icons": "electron-icon-builder --input=./build/logo.png --output=build", "analyze:renderer": "VISUALIZER_RENDERER=true yarn build", "analyze:main": "VISUALIZER_MAIN=true yarn build", "typecheck": "npm run typecheck:node && npm run typecheck:web", @@ -112,7 +111,7 @@ "@emotion/is-prop-valid": "^1.3.1", "@eslint-react/eslint-plugin": "^1.36.1", "@eslint/js": "^9.22.0", - "@google/genai": "^0.13.0", + "@google/genai": "^1.0.1", "@hello-pangea/dnd": "^16.6.0", "@kangfenmao/keyv-storage": "^0.1.0", "@modelcontextprotocol/sdk": "^1.11.4", @@ -138,7 +137,6 @@ "@types/react-infinite-scroll-component": "^5.0.0", "@types/react-window": "^1", "@types/tinycolor2": "^1", - "@types/ws": "^8", "@uiw/codemirror-extensions-langs": "^4.23.12", "@uiw/codemirror-themes-all": "^4.23.12", "@uiw/react-codemirror": "^4.23.12", @@ -159,7 +157,6 @@ "electron": "35.4.0", "electron-builder": "26.0.15", "electron-devtools-installer": "^3.2.0", - "electron-icon-builder": "^2.0.1", "electron-vite": "^3.1.0", "emittery": "^1.0.3", "emoji-picker-element": "^1.22.1", @@ -220,13 +217,13 @@ "pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch", "@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch", "@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch", - "node-gyp": "^9.1.0", "libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch", "openai@npm:^4.77.0": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch", "pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch", "app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch", "openai@npm:^4.87.3": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch", - "app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch" + "app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch", + "@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A0.3.44#~/.yarn/patches/@langchain-core-npm-0.3.44-41d5c3cb0a.patch" }, "packageManager": "yarn@4.9.1", "lint-staged": { diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 528b64c4e..50d9aa248 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 9c75b514c..94de990c5 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -18,7 +18,6 @@ import CopilotService from './services/CopilotService' import { ExportService } from './services/ExportService' import FileService from './services/FileService' import FileStorage from './services/FileStorage' -import { GeminiService } from './services/GeminiService' import KnowledgeService from './services/KnowledgeService' import mcpService from './services/MCPService' import NotificationService from './services/NotificationService' @@ -250,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) @@ -298,13 +298,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { } }) - // gemini - ipcMain.handle(IpcChannel.Gemini_UploadFile, GeminiService.uploadFile) - ipcMain.handle(IpcChannel.Gemini_Base64File, GeminiService.base64File) - ipcMain.handle(IpcChannel.Gemini_RetrieveFile, GeminiService.retrieveFile) - ipcMain.handle(IpcChannel.Gemini_ListFiles, GeminiService.listFiles) - ipcMain.handle(IpcChannel.Gemini_DeleteFile, GeminiService.deleteFile) - // mini window ipcMain.handle(IpcChannel.MiniWindow_Show, () => windowService.showMiniWindow()) ipcMain.handle(IpcChannel.MiniWindow_Hide, () => windowService.hideMiniWindow()) diff --git a/src/main/services/FileStorage.ts b/src/main/services/FileStorage.ts index 9b097e96e..0ea36abc0 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/main/services/GeminiService.ts b/src/main/services/GeminiService.ts deleted file mode 100644 index e7b831066..000000000 --- a/src/main/services/GeminiService.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { File, FileState, GoogleGenAI, Pager } from '@google/genai' -import { FileType } from '@types' -import fs from 'fs' - -import { CacheService } from './CacheService' - -export class GeminiService { - private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list' - private static readonly CACHE_DURATION = 3000 - - static async uploadFile( - _: Electron.IpcMainInvokeEvent, - file: FileType, - { apiKey, baseURL }: { apiKey: string; baseURL: string } - ): Promise { - const sdk = new GoogleGenAI({ - vertexai: false, - apiKey, - httpOptions: { - baseUrl: baseURL - } - }) - - return await sdk.files.upload({ - file: file.path, - config: { - mimeType: 'application/pdf', - name: file.id, - displayName: file.origin_name - } - }) - } - - static async base64File(_: Electron.IpcMainInvokeEvent, file: FileType) { - return { - data: Buffer.from(fs.readFileSync(file.path)).toString('base64'), - mimeType: 'application/pdf' - } - } - - static async retrieveFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string): Promise { - const sdk = new GoogleGenAI({ vertexai: false, apiKey }) - const cachedResponse = CacheService.get(GeminiService.FILE_LIST_CACHE_KEY) - if (cachedResponse) { - return GeminiService.processResponse(cachedResponse, file) - } - - const response = await sdk.files.list() - CacheService.set(GeminiService.FILE_LIST_CACHE_KEY, response, GeminiService.CACHE_DURATION) - - return GeminiService.processResponse(response, file) - } - - private static async processResponse(response: Pager, file: FileType) { - for await (const f of response) { - if (f.state === FileState.ACTIVE) { - if (f.displayName === file.origin_name && Number(f.sizeBytes) === file.size) { - return f - } - } - } - - return undefined - } - - static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string): Promise { - const sdk = new GoogleGenAI({ vertexai: false, apiKey }) - const files: File[] = [] - for await (const f of await sdk.files.list()) { - files.push(f) - } - return files - } - - static async deleteFile(_: Electron.IpcMainInvokeEvent, fileId: string, apiKey: string) { - const sdk = new GoogleGenAI({ vertexai: false, apiKey }) - await sdk.files.delete({ name: fileId }) - } -} diff --git a/src/preload/index.ts b/src/preload/index.ts index 51c8dee7a..f5a4a85f1 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -76,6 +76,7 @@ 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), + 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), diff --git a/src/renderer/src/components/CodeToolbar/constants.ts b/src/renderer/src/components/CodeToolbar/constants.ts index 00e7fa795..4eeec0fa1 100644 --- a/src/renderer/src/components/CodeToolbar/constants.ts +++ b/src/renderer/src/components/CodeToolbar/constants.ts @@ -5,12 +5,12 @@ export const TOOL_SPECS: Record = { copy: { id: 'copy', type: 'core', - order: 10 + order: 11 }, download: { id: 'download', type: 'core', - order: 11 + order: 10 }, edit: { id: 'edit', diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx index 08f4c1fa2..1474b80c0 100644 --- a/src/renderer/src/components/app/Sidebar.tsx +++ b/src/renderer/src/components/app/Sidebar.tsx @@ -137,7 +137,7 @@ const MainMenus: FC = () => { const { hideMinappPopup } = useMinappPopup() const { t } = useTranslation() const { pathname } = useLocation() - const { sidebarIcons } = useSettings() + const { sidebarIcons, defaultPaintingProvider } = useSettings() const { minappShow } = useRuntime() const navigate = useNavigate() const { theme } = useTheme() @@ -158,7 +158,7 @@ const MainMenus: FC = () => { const pathMap = { assistants: '/', agents: '/agents', - paintings: '/paintings', + paintings: `/paintings/${defaultPaintingProvider}`, translate: '/translate', minapp: '/apps', knowledge: '/knowledge', diff --git a/src/renderer/src/hooks/useSettings.ts b/src/renderer/src/hooks/useSettings.ts index 5eea046d0..66d9615ee 100644 --- a/src/renderer/src/hooks/useSettings.ts +++ b/src/renderer/src/hooks/useSettings.ts @@ -6,13 +6,13 @@ import { setAutoCheckUpdate as _setAutoCheckUpdate, setLaunchOnBoot, setLaunchToTray, + setPinTopicsToTop, setSendMessageShortcut as _setSendMessageShortcut, setSidebarIcons, setTargetLanguage, setTheme, SettingsState, setTopicPosition, - setPinTopicsToTop, setTray as _setTray, setTrayOnClose, setWindowStyle diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index d9d124d0c..911102a79 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 6cb36f8af..73b96739a 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 85d2061e5..4b29123b0 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 a9d128643..3364eb5e0 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 70b34c7d1..0563b68e9 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/files/ContentView.tsx b/src/renderer/src/pages/files/ContentView.tsx index 6e3d34283..663096292 100644 --- a/src/renderer/src/pages/files/ContentView.tsx +++ b/src/renderer/src/pages/files/ContentView.tsx @@ -5,8 +5,6 @@ import { Col, Image, Row, Spin, Table } from 'antd' import React, { memo } from 'react' import styled from 'styled-components' -import GeminiFiles from './GeminiFiles' - interface ContentViewProps { id: FileTypes | 'all' | string files?: FileType[] @@ -45,10 +43,6 @@ const ContentView: React.FC = ({ id, files, dataSource, column ) } - if (id.startsWith('gemini_')) { - return - } - return ( = ({ id, list, files }) => { ) } - if (id.startsWith('gemini_')) { - return - } - return ( = ({ id }) => { - const { provider } = useProvider(id) - const [files, setFiles] = useState([]) - const [loading, setLoading] = useState(false) - - const fetchFiles = useCallback(async () => { - const files = await window.api.gemini.listFiles(provider.apiKey) - files && setFiles(files.filter((file) => file.state === 'ACTIVE')) - }, [provider]) - - useEffect(() => { - runAsyncFunction(async () => { - try { - setLoading(true) - await fetchFiles() - setLoading(false) - } catch (error: any) { - console.error('Failed to fetch files:', error) - window.message.error(error.message) - setLoading(false) - } - }) - }, [fetchFiles]) - - useEffect(() => { - setFiles([]) - }, [id]) - - if (loading) { - return ( - - - - - - ) - } - - return ( - - - {files.map((file) => ( - { - setFiles(files.filter((f) => f.name !== file.name)) - window.api.gemini.deleteFile(file.name!, provider.apiKey).catch((error) => { - console.error('Failed to delete file:', error) - setFiles((prev) => [...prev, file]) - }) - }} - /> - ) - }} - /> - ))} - - - ) -} - -const Container = styled.div` - width: 100%; -` - -const FileListContainer = styled.div` - display: flex; - flex-direction: column; - gap: 8px; -` - -const LoadingWrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; - min-height: 200px; -` - -export default GeminiFiles diff --git a/src/renderer/src/pages/home/Messages/Blocks/MainTextBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/MainTextBlock.tsx index 3e12940c2..403e59fa6 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/MainTextBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/MainTextBlock.tsx @@ -163,7 +163,7 @@ const MainTextBlock: React.FC = ({ block, citationBlockId, role, mentions )} {role === 'user' && !renderInputMessageAsMarkdown ? ( -

+

{block.content}

) : ( diff --git a/src/renderer/src/pages/paintings/AihubmixPage.tsx b/src/renderer/src/pages/paintings/AihubmixPage.tsx index f8b69e146..666ed1d72 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 (