From 4dabc214f2cbbc0818ef0ec14a4c3ba86d39a8ff Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Thu, 21 Aug 2025 14:19:51 +0800 Subject: [PATCH] feat: enhance file extension handling in Inputbar (#9269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add isTextFile functionality and improve file selection handling - Introduced a new IPC channel for checking if a file is a text file. - Implemented isTextFile method in FileStorage service to determine file type based on content. - Enhanced AttachmentButton to filter selected files based on text file validation. - Updated translations to include support for displaying unsupported file counts across multiple languages. - Added utility functions for text file validation and filtering in file utilities. * refactor(FileStorage): replace hardcoded buffer size with constant for improved readability * restore yarn lock * add isbinaryfile dep * refactor: 整理导入顺序 * fix(preload): 为isTextFile方法添加返回类型Promise * refactor(FileManager): update getSafePath to use file metadata for path retrieval - Modified getSafePath method to utilize the path from file metadata instead of a hardcoded file path. - Enhanced handling for files not stored in the file storage system. * refactor(FileUtilities): rename text file functions for clarity - Updated function names from isTextFile to isSupportedFile and filterTextFiles to filterSupportedFiles to better reflect their purpose. - Adjusted related imports and usages in AttachmentButton and PasteService components to align with the new naming conventions. * fix drop files * refactor(MarkdownStyles): remove last-child margin override; adjust MessageFooter margin and clean up unused code in MessageAttachments * feat(Sidebar): add 'code_tools' icon and route; enhance CodeToolsPage layout with Navbar and improved provider filtering * feat(CodeTools): add environment variable support for CLI tools; update UI to manage environment variables and enhance localization for related strings * refactor(Sidebar): remove unused imports and code related to documentation; streamline sidebar functionality * refactor(SvgPreview): use transparent container for SVG (#9294) * refactor(SvgPreview): use transparent container for SVG * test: fix snapshot * refactor(CodeToolsService): replace npm package version fetching with direct API call; simplify command construction for installation * chore: release v1.5.7-rc.1 * refactor(CodeToolsService): adjust command construction for Windows compatibility; streamline installation command handling * refactor(Markdown): update disallowed elements to include 'script' for enhanced security * feat: quick model (#9290) * refactor(i18n): 将话题命名模型相关文案更新为摘要模型 更新所有语言文件中关于话题命名模型的文案,统一改为摘要模型,以反映功能的扩展和更通用的用途 * refactor(设置页面): 优化主题命名弹窗组件性能 使用useCallback和useMemo优化回调函数和渲染性能 将重复的JSX代码提取为独立组件 * feat(设置): 在模型设置中添加话题命名折叠面板 将话题命名设置从直接显示改为折叠面板形式,提升界面整洁度 * refactor(i18n): 重构话题命名相关翻译字段结构 * docs(i18n): 添加生成图像的高度、宽度和安全容忍度翻译占位符 * fix(settings): 修正主题命名弹窗中的翻译键名 * style(ui): 调整主题命名弹窗的间距和文本区域高度 移除多余的上下边距,并使用自适应高度的文本区域 * refactor(llm): 将 topicNamingModel 重命名为 summaryModel 更新相关函数、状态和测试用例以反映命名变更 增加迁移逻辑处理旧状态数据 更新持久化版本号至133 * fix(ApiService): 优先使用摘要模型替代默认模型 当获取摘要时,优先使用getSummaryModel()返回的模型,其次才是助手指定的模型或默认模型,以确保摘要生成的一致性 * docs(i18n): 更新摘要模型描述中的搜索关键词提炼 将"搜索结果摘要"修改为"搜索关键字提炼"以更准确描述功能 * fix(i18n): 更新多语言翻译文件中的摘要模型相关文本 * feat(i18n): 为摘要模型设置添加工具提示说明 添加摘要模型设置的工具提示,建议用户选择轻量模型而非思考模型 * refactor(i18n): 将摘要模型相关文案更新为快速模型 更新国际化文案和组件引用,将"摘要模型"统一改为"快速模型"以更准确描述功能用途 * feat(i18n): 将摘要模型重命名为快速模型并更新相关描述 * refactor(llm): 将summaryModel重命名为quickModel以提升语义清晰度 * test(api): 在ApiService测试中添加LlmState类型和awsBedrock配置 添加LlmState类型以满足类型检查要求,并补充awsBedrock的mock配置以完善测试覆盖 * Revert "feat(设置): 在模型设置中添加话题命名折叠面板" This reverts commit 4d58c053dafe77acbdefc2e73a891c96d248e188. * refactor(settings): 重命名并移动 TopicNamingModalPopup 组件文件 将 TopicNamingModalPopup.tsx 重命名为 QuickModelPopup.tsx 并移动到相应目录 * refactor(QuickModelPopup): 优化主题命名设置布局和样式 移除 TopicNamingSettings 组件内联实现,直接整合到 Modal 中 调整间距和样式,提升视觉一致性 修复文本区域 onChange 去除换行的逻辑 * feat(模型设置): 在快速模型弹窗中添加重置按钮图标并调整布局 将重置按钮改为图标形式并内联显示,同时调整输入区域的高度样式 * docs(i18n): 更新快速模型相关翻译文本 * fix: 将迁移错误日志从133更新为134 * style(settings): 替换模型设置中的图标为Rocket图标以提升视觉一致性 * fix: unexpected quitting full screen mode (#9200) * fix(Inputbar): 修正拼写错误,将expend改为expand * fix: 修复Escape键事件冒泡问题并改进全屏处理 修复多个组件中Escape键事件未阻止冒泡的问题 添加全屏控制IPC通道 将全屏退出逻辑移至渲染进程处理 移除主进程中冗余的全屏退出处理代码 * fix(SelectModelPopup): 修复键盘事件处理并移除无效的useEffect 将键盘事件监听从window移动到Modal容器,避免事件冒泡问题 移除无效的useEffect并更新键盘事件类型定义 * fix(QuickPanel): 拦截window上的keydown事件 * fix(QuickPanel): 修复事件监听器移除时未使用相同参数的问题 * fix(TopView): 修复左侧导航栏布局崩坏问题 * fix: 修正变量名拼写错误,将expended改为expanded * Revert "fix(SelectModelPopup): 修复键盘事件处理并移除无效的useEffect" This reverts commit 4211780b95c737989a62e6598728ff6e17b6b61b. * feat: use quick model to detect translate language (#9315) * refactor(语言检测): 移除翻译模型依赖,改用快速或默认模型 * feat(i18n): 添加希腊语翻译支持 * fix(i18n): 更新i18n 统一将翻译模型提示改为快速模型提示,优化多语言文件中的描述 * Revert "feat(i18n): 添加希腊语翻译支持" This reverts commit 42613cb2e21da2a6cb3e920b09cfa57f7784ac58. * feat: add 'code_tools' to sidebar icons and update related components * fix: KaTeX math engine render * feat: 同步百炼服务器功能 (#9205) * 同步百炼服务器功能 * cr修改 --------- Co-authored-by: yunze * fix(SelectionHook): improve validation for selected text range to handle empty strings and ensure valid extraction (#9329) chore: update selection-hook dependency to version 1.0.10 in package.json and yarn.lock * fix: web search references missing caused by early reset (#9328) * feat(openai): handle special tokens for zhipu api (#9323) * feat(openai): 添加对智谱特殊token的过滤处理 在OpenAIAPIClient中添加对智谱AI特殊token的过滤逻辑,避免不需要的token被输出 * docs(OpenAIApiClient): 添加注释 * refactor(zhipu): 重命名并更新智谱特殊token处理逻辑 将 ZHIPU_SPECIAL_TOKENS_TO_FILTER 重命名为 ZHIPU_RESULT_TOKENS 以更准确描述用途 修改智谱API特殊token处理逻辑,不再过滤而是用**标记结果token * feat: support openai codex (#9332) * support openai codex * lint * refactor: remove unused codeTools enum from constant.ts * fix build * fix lin * fix: add support for qwenCode CLI tool and improve error handling in CodeToolsService * fix: timeout memory leak (#9312) * fix(MinappPopupContainer): 修复内存泄漏问题,清理未使用的定时器 在组件卸载时清理setTimeout定时器,避免潜在的内存泄漏 * fix(SelectModelButton): 修复模型选择后更新导致的卡顿问题 使用useRef存储定时器并在组件卸载时清理,避免内存泄漏 * fix(QuickPanel): 修复定时器未清理导致的内存泄漏问题 添加 clearSearchTimerRef 和 focusTimerRef 来管理定时器 在组件清理和状态变化时清理所有定时器 * fix(useInPlaceEdit): 修复编辑模式下定时器未清理的问题 添加清理定时器的逻辑,避免组件卸载时内存泄漏 * refactor(useKnowledge): 使用ref管理定时器并统一检查知识库逻辑 将分散的setTimeout调用统一为checkAllBases方法 使用useRef管理定时器并在组件卸载时清理 * fix(useScrollPosition): 修复滚动位置恢复时的内存泄漏问题 添加清理函数以清除未完成的定时器,防止组件卸载时内存泄漏 * fix(WebSearchProviderSetting): 清理定时器防止内存泄漏 在组件卸载时清理检查API有效性的定时器,避免潜在的内存泄漏问题 * fix(selection-toolbar): 修复选中文本时定时器未清理的问题 * fix(translate): 修复复制文本时定时器未清理的问题 添加 copyTimerRef 来管理复制操作的定时器,并在组件卸载时清理定时器 * fix(WebSearchSettings): 使用useRef管理订阅验证定时器以避免内存泄漏 * fix(MCPSettings): 修复定时器未清理导致的内存泄漏问题 添加 useRef 来存储定时器引用,并在组件卸载时清理定时器 * refactor(ThinkingBlock): 使用 useTemporaryValue 替换手动 setTimeout 移除手动设置的 setTimeout 来重置 copied 状态,改用 useTemporaryValue hook 自动处理 * refactor(ChatNavigation): 使用 useRef 替代 useState 管理定时器 简化定时器管理逻辑,避免不必要的状态更新 * fix(AddAssistantPopup): 清理创建助手时的定时器以避免内存泄漏 添加useEffect清理定时器,防止组件卸载时内存泄漏 * feat(hooks): 添加useTimer钩子管理定时器 实现一个自定义hook来集中管理setTimeout和setInterval定时器 自动在组件卸载时清理所有定时器防止内存泄漏 * refactor(Inputbar): 使用 useTimer 替换 setTimeout 实现延迟更新 将 setTimeout 替换为 useTimer 的自定义 setTimeoutTimer 方法,提高代码可维护性并统一计时器管理 * refactor(WindowFooter): 使用 useTimer 替换 setTimeout 以管理定时器 * docs(useTimer): 更新定时器hook的注释格式和描述 * feat(hooks): 为useTimer添加返回清理函数的功能 允许从setTimeoutTimer和setIntervalTimer返回清理函数,便于手动清除定时器 * refactor(ImportAgentPopup): 使用useTimer替换setTimeout以管理定时器 * refactor: 使用useTimer替代setTimeout以优化定时器管理 * refactor(SearchResults): 使用useTimer替换setTimeout以管理定时器 * refactor(消息组件): 使用useTimer替换setTimeout以管理定时器 * refactor: 使用useTimer替换setTimeout以优化定时器管理 * refactor(AssistantsDrawer): 使用useTimer替换setTimeout以优化定时器管理 * refactor(Inputbar): 使用useTimer替换setTimeout以优化定时器管理 * refactor(MCPToolsButton): 使用useTimer优化定时器管理 * refactor(QuickPhrasesButton): 使用useTimer替换setTimeout以优化定时器管理 * refactor(ChatFlowHistory): 使用useTimer替换setTimeout以管理定时器 * refactor(Message): 使用useTimer替换setTimeout以管理定时器 * refactor(MessageAnchorLine): 使用useTimer替换setTimeout以优化定时器管理 * refactor(MessageGroup): 使用useTimer替换setTimeout以优化定时器管理 * refactor(MessageMenubar): 使用 useTemporaryValue 替换手动 setTimeout 逻辑 * refactor(Messages): 使用 useTimer 优化定时器管理 * refactor(MessageTools): 使用useTimer替换setTimeout以管理定时器 * fix(SelectionBox): 修复鼠标移动时未清除定时器导致的内存泄漏 在鼠标移动事件处理中增加定时器清理逻辑,避免组件卸载时未清除定时器导致的内存泄漏问题 * refactor(ErrorBlock): 使用自定义hook替换setTimeout 使用useTimer中的setTimeoutTimer替代原生setTimeout,便于统一管理定时器 * refactor(GeneralSettings): 使用useTimer替换setTimeout以实现更好的定时器管理 * refactor(ShortcutSettings): 使用useTimer替换setTimeout以优化定时器管理 * refactor(AssistantModelSettings): 使用useTimer替换setTimeout以优化定时器管理 * refactor(DataSettings): 使用useTimer替换setTimeout以增强定时器管理 统一使用useTimer hook管理所有定时器操作,提高代码可维护性 * refactor(NutstoreSettings): 使用useTimer优化setTimeout管理 替换直接使用setTimeout为useTimer hook的setTimeoutTimer方法,提升定时器管理的可维护性 * refactor(MCPSettings): 使用useTimer替换setTimeout以提升代码可维护性 * refactor(ProviderSetting): 使用useTimer优化setTimeout管理 * refactor(ProviderSettings): 使用 useTimer 替换 setTimeout 以优化定时器管理 * refactor(InputBar): 使用useTimer替换setTimeout以实现更好的定时器管理 * refactor(MessageEditor): 使用useTimer替换setTimeout以管理定时器 使用自定义hook useTimer来替代原生setTimeout,便于统一管理和清理定时器 * docs(useTimer): 添加 useTimer hook 的使用示例和详细说明 * refactor(MinappPopupContainer): 使用useTimer替换setTimeout实现 替换直接使用setTimeout为自定义hook useTimer,简化组件清理逻辑 * refactor(AddAssistantPopup): 使用useTimer替换手动定时器管理 用useTimer钩子替代手动管理定时器,简化代码并提高可维护性 * refactor(WebSearchSettings): 使用 useTimer 替换手动定时器管理 移除手动管理的定时器逻辑,改用 useTimer hook 统一处理 * refactor(WebSearchProviderSetting): 使用自定义hook替代原生定时器 用useTimer hook替换原有的setTimeout和clearTimeout逻辑,提高代码可维护性 * refactor(translate): 使用 useTemporaryValue 替换手动实现的复制状态定时器 * refactor(SelectionToolbar): 使用 useTimer 钩子替换 setTimeout 和 clearTimeout 重构 SelectionToolbar 组件,使用自定义的 useTimer 钩子来管理定时器,提升代码可维护性 清理隐藏时的定时器逻辑,避免内存泄漏 * fix(Translate): update settings into db (#9305) * fix(翻译): 修复设置没有储存到db的错误 * fix(translate): 修复自动检测方法设置更新失败的问题 添加错误处理逻辑,当更新自动检测方法设置失败时显示错误信息 * Fix AWS Bedrock models not receiving uploaded document content (#9337) * Initial plan * Add file content processing to AWS Bedrock client convertMessageToSdkParam method Co-authored-by: caozhiyuan <3415285+caozhiyuan@users.noreply.github.com> * Fix file content format to match other AI clients and update tests Co-authored-by: caozhiyuan <3415285+caozhiyuan@users.noreply.github.com> * Update src/renderer/src/aiCore/clients/aws/AwsBedrockAPIClient.ts --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: caozhiyuan <3415285+caozhiyuan@users.noreply.github.com> Co-authored-by: Phantom <59059173+EurFelux@users.noreply.github.com> * feat(migrate): initialize default assistant settings if not present (#9303) * feat(migrate): update migration logic for version 134; initialize default assistant settings if not present * Update src/renderer/src/store/migrate.ts Co-authored-by: Phantom <59059173+EurFelux@users.noreply.github.com> --------- Co-authored-by: Phantom <59059173+EurFelux@users.noreply.github.com> * feat: support language aliases for code editor (#9336) * feat(CodeEditor): support language aliases * fix: mermaid * refactor: lookup * chore: sort package.json * fix(SelectionHook): [macOS] add type safety to prevent crashes (#9354) chore: update selection-hook dependency to version 1.0.11 in package.json and yarn.lock * fix: sidebar code icon reset bug (#9307) (#9333) * fix: 修复侧边栏重置时 Code 图标消失的问题 (#9307) 问题原因: - types/index.ts 中的 SidebarIcon 类型定义缺少 'code_tools' - 存在重复的类型定义和常量定义导致不一致 修复内容: - 在 types/index.ts 的 SidebarIcon 类型中添加 'code_tools' - 删除 minapps.ts 中重复的 DEFAULT_SIDEBAR_ICONS 常量 - 统一从 @renderer/types 导入 SidebarIcon 类型 - 删除 settings.ts 中重复的 SidebarIcon 类型定义 这确保了在导航栏设置为左侧时,点击侧边栏设置的重置按钮后, Code 图标能够正确显示。 * refactor: 将侧边栏配置移至 config 目录 根据 code review 建议,将侧边栏相关配置从 store/settings.ts 移动到 config/sidebar.ts,使配置管理更加清晰。 改动内容: - 创建 config/sidebar.ts 存放侧边栏配置常量 - 更新相关文件的导入路径 - 在 settings.ts 中重新导出以保持向后兼容 - 添加 REQUIRED_SIDEBAR_ICONS 常量便于未来扩展 这个改动保持了最小化原则,不影响现有功能。 * refactor: improve locate highlight animation (#9345) * feat(utils): show weekday in date and datetime prompt variables (#9362) * feat(utils): 优化日期时间变量替换格式 为 {{date}} 和 {{datetime}} 变量替换添加更详细的格式选项,包括星期、年月日和时间信息 * test(prompt): 更新测试中日期时间的本地化格式 * refactor(CodeToolsPage): simplify CLI tool change handling and optimize provider filtering logic * fix(newMessage): reduce default display count from 20 to 10 * feat(AssistantService): introduce DEFAULT_ASSISTANT_SETTINGS for consistent assistant configuration and update migration logic for version 136 * chore: release v1.5.7-rc.2 * fix(Markdown/Link): set href to undefined when it's empty (#9343) fix(Markdown/Link): 处理空链接时设置href为undefined * fix(Inputbar): update file handling to use functional state update for setFiles * refactor(file): update isSupportedFile function to accept filePath instead of FileMetadata for improved clarity and consistency in file handling --------- Co-authored-by: icarus Co-authored-by: kangfenmao Co-authored-by: one Co-authored-by: Phantom <59059173+EurFelux@users.noreply.github.com> Co-authored-by: alickreborn0 Co-authored-by: yunze Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com> Co-authored-by: caozhiyuan <568022847@qq.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: caozhiyuan <3415285+caozhiyuan@users.noreply.github.com> Co-authored-by: SuYao Co-authored-by: Jason Young <44939412+farion1231@users.noreply.github.com> --- package.json | 1 + packages/shared/IpcChannel.ts | 1 + src/main/ipc.ts | 1 + src/main/services/FileStorage.ts | 32 ++++++++++++++++++- src/preload/index.ts | 3 +- src/renderer/src/i18n/locales/en-us.json | 1 + src/renderer/src/i18n/locales/ja-jp.json | 1 + src/renderer/src/i18n/locales/ru-ru.json | 1 + src/renderer/src/i18n/locales/zh-cn.json | 1 + src/renderer/src/i18n/locales/zh-tw.json | 1 + src/renderer/src/i18n/translate/el-gr.json | 1 + src/renderer/src/i18n/translate/es-es.json | 1 + src/renderer/src/i18n/translate/fr-fr.json | 1 + src/renderer/src/i18n/translate/pt-pt.json | 1 + .../pages/home/Inputbar/AttachmentButton.tsx | 30 ++++++++++++++--- .../src/pages/home/Inputbar/Inputbar.tsx | 24 ++++++-------- src/renderer/src/services/FileManager.ts | 4 ++- src/renderer/src/services/PasteService.ts | 5 +-- src/renderer/src/utils/file.ts | 28 ++++++++++++++++ yarn.lock | 15 +++++---- 20 files changed, 121 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 97adcc809c..b866ba7138 100644 --- a/package.json +++ b/package.json @@ -203,6 +203,7 @@ "husky": "^9.1.7", "i18next": "^23.11.5", "iconv-lite": "^0.6.3", + "isbinaryfile": "5.0.4", "jaison": "^2.0.2", "jest-styled-components": "^7.2.0", "linguist-languages": "^8.0.0", diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 20bc852e7e..56ebfb3d58 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -157,6 +157,7 @@ export enum IpcChannel { File_GetPdfInfo = 'file:getPdfInfo', Fs_Read = 'fs:read', File_OpenWithRelativePath = 'file:openWithRelativePath', + File_IsTextFile = 'file:isTextFile', // file service FileService_Upload = 'file-service:upload', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index f094e8d580..8689ab2c3b 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -444,6 +444,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile.bind(fileManager)) ipcMain.handle(IpcChannel.File_BinaryImage, fileManager.binaryImage.bind(fileManager)) ipcMain.handle(IpcChannel.File_OpenWithRelativePath, fileManager.openFileWithRelativePath.bind(fileManager)) + ipcMain.handle(IpcChannel.File_IsTextFile, fileManager.isTextFile.bind(fileManager)) // file service ipcMain.handle(IpcChannel.FileService_Upload, async (_, provider: Provider, file: FileMetadata) => { diff --git a/src/main/services/FileStorage.ts b/src/main/services/FileStorage.ts index f5df9ed3f7..39a16713d7 100644 --- a/src/main/services/FileStorage.ts +++ b/src/main/services/FileStorage.ts @@ -1,7 +1,8 @@ import { loggerService } from '@logger' import { getFilesDir, getFileType, getTempDir, readTextFileWithAutoEncoding } from '@main/utils/file' -import { documentExts, imageExts, MB } from '@shared/config/constant' +import { documentExts, imageExts, KB, MB } from '@shared/config/constant' import { FileMetadata } from '@types' +import chardet from 'chardet' import * as crypto from 'crypto' import { dialog, @@ -15,6 +16,7 @@ import { import * as fs from 'fs' import { writeFileSync } from 'fs' import { readFile } from 'fs/promises' +import { isBinaryFile } from 'isbinaryfile' import officeParser from 'officeparser' import * as path from 'path' import { PDFDocument } from 'pdf-lib' @@ -630,6 +632,34 @@ class FileStorage { public getFilePathById(file: FileMetadata): string { return path.join(this.storageDir, file.id + file.ext) } + + public isTextFile = async (_: Electron.IpcMainInvokeEvent, filePath: string): Promise => { + try { + const isBinary = await isBinaryFile(filePath) + if (isBinary) { + return false + } + + const length = 8 * KB + const fileHandle = await fs.promises.open(filePath, 'r') + const buffer = Buffer.alloc(length) + const { bytesRead } = await fileHandle.read(buffer, 0, length, 0) + await fileHandle.close() + + const sampleBuffer = buffer.subarray(0, bytesRead) + const matches = chardet.analyse(sampleBuffer) + + // 如果检测到的编码置信度较高,认为是文本文件 + if (matches.length > 0 && matches[0].confidence > 0.8) { + return true + } + + return false + } catch (error) { + logger.error('Failed to check if file is text:', error as Error) + return false + } + } } export const fileStorage = new FileStorage() diff --git a/src/preload/index.ts b/src/preload/index.ts index 1a630a19ee..1059826224 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -170,7 +170,8 @@ const api = { base64File: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64File, fileId), pdfInfo: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_GetPdfInfo, fileId), getPathForFile: (file: File) => webUtils.getPathForFile(file), - openFileWithRelativePath: (file: FileMetadata) => ipcRenderer.invoke(IpcChannel.File_OpenWithRelativePath, file) + openFileWithRelativePath: (file: FileMetadata) => ipcRenderer.invoke(IpcChannel.File_OpenWithRelativePath, file), + isTextFile: (filePath: string): Promise => ipcRenderer.invoke(IpcChannel.File_IsTextFile, filePath) }, fs: { read: (pathOrUrl: string, encoding?: BufferEncoding) => ipcRenderer.invoke(IpcChannel.Fs_Read, pathOrUrl, encoding) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index eef4f124aa..1841716293 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -322,6 +322,7 @@ "expand": "Expand", "file_error": "Error processing file", "file_not_supported": "Model does not support this file type", + "file_not_supported_count": "{{count}} files are not supported", "generate_image": "Generate image", "generate_image_not_supported": "The model does not support generating images.", "knowledge_base": "Knowledge Base", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 289914cca6..5842f6b24b 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -322,6 +322,7 @@ "expand": "展開", "file_error": "ファイル処理エラー", "file_not_supported": "モデルはこのファイルタイプをサポートしません", + "file_not_supported_count": "{{count}} 個のファイルはサポートされていません", "generate_image": "画像を生成する", "generate_image_not_supported": "モデルは画像の生成をサポートしていません。", "knowledge_base": "ナレッジベース", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index ca219486dd..d1a8fff8e5 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -322,6 +322,7 @@ "expand": "Развернуть", "file_error": "Ошибка обработки файла", "file_not_supported": "Модель не поддерживает этот тип файла", + "file_not_supported_count": "{{count}} файлов не поддерживаются", "generate_image": "Сгенерировать изображение", "generate_image_not_supported": "Модель не поддерживает генерацию изображений.", "knowledge_base": "База знаний", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index ff692f3bbd..812d182c94 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -322,6 +322,7 @@ "expand": "展开", "file_error": "文件处理出错", "file_not_supported": "模型不支持此文件类型", + "file_not_supported_count": "{{count}} 个文件不被支持", "generate_image": "生成图片", "generate_image_not_supported": "模型不支持生成图片", "knowledge_base": "知识库", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index aef5a324dd..5b0bc186ca 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -322,6 +322,7 @@ "expand": "展開", "file_error": "檔案處理錯誤", "file_not_supported": "模型不支援此檔案類型", + "file_not_supported_count": "{{count}} 個檔案不被支援", "generate_image": "生成圖片", "generate_image_not_supported": "模型不支援生成圖片", "knowledge_base": "知識庫", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 7e60d79b49..05c1efa2cf 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -322,6 +322,7 @@ "expand": "Επεκτάση", "file_error": "Σφάλμα κατά την επεξεργασία του αρχείου", "file_not_supported": "Το μοντέλο δεν υποστηρίζει αυτό το είδος αρχείων", + "file_not_supported_count": "{{count}} αρχεία δεν υποστηρίζονται", "generate_image": "Δημιουργία εικόνας", "generate_image_not_supported": "Το μοντέλο δεν υποστηρίζει τη δημιουργία εικόνων", "knowledge_base": "Βάση γνώσεων", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index ef4234f334..d63cda4c58 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -322,6 +322,7 @@ "expand": "Expandir", "file_error": "Error al procesar el archivo", "file_not_supported": "El modelo no admite este tipo de archivo", + "file_not_supported_count": "{{count}} archivos no soportados", "generate_image": "Generar imagen", "generate_image_not_supported": "El modelo no soporta la generación de imágenes", "knowledge_base": "Base de conocimientos", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 9e2f724425..a7575df024 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -322,6 +322,7 @@ "expand": "Développer", "file_error": "Erreur lors du traitement du fichier", "file_not_supported": "Le modèle ne prend pas en charge ce type de fichier", + "file_not_supported_count": "{{count}} fichiers non pris en charge", "generate_image": "Générer une image", "generate_image_not_supported": "Le modèle ne supporte pas la génération d'images", "knowledge_base": "Base de connaissances", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 9bed40be77..3234ad8215 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -322,6 +322,7 @@ "expand": "Expandir", "file_error": "Erro ao processar o arquivo", "file_not_supported": "O modelo não suporta este tipo de arquivo", + "file_not_supported_count": "{{count}} arquivos não suportados", "generate_image": "Gerar imagem", "generate_image_not_supported": "Modelo não suporta geração de imagem", "knowledge_base": "Base de conhecimento", diff --git a/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx b/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx index dc439e79c7..7e6d583fc3 100644 --- a/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx @@ -1,4 +1,5 @@ -import { FileType } from '@renderer/types' +import { FileMetadata, FileType } from '@renderer/types' +import { filterSupportedFiles } from '@renderer/utils/file' import { Tooltip } from 'antd' import { Paperclip } from 'lucide-react' import { FC, useCallback, useImperativeHandle } from 'react' @@ -30,20 +31,39 @@ const AttachmentButton: FC = ({ const { t } = useTranslation() const onSelectFile = useCallback(async () => { - const _files = await window.api.file.select({ + // when the number of extensions is greater than 20, use *.* to avoid selecting window lag + const useAllFiles = extensions.length > 20 + + const _files: FileMetadata[] = await window.api.file.select({ properties: ['openFile', 'multiSelections'], filters: [ { name: 'Files', - extensions: extensions.map((i) => i.replace('.', '')) + extensions: useAllFiles ? ['*'] : extensions.map((i) => i.replace('.', '')) } ] }) if (_files) { - setFiles([...files, ..._files]) + if (!useAllFiles) { + setFiles([...files, ..._files]) + return + } + const supportedFiles = await filterSupportedFiles(_files, extensions) + if (supportedFiles.length > 0) { + setFiles([...files, ...supportedFiles]) + } + + if (supportedFiles.length !== _files.length) { + window.message.info({ + key: 'file_not_supported', + content: t('chat.input.file_not_supported_count', { + count: _files.length - supportedFiles.length + }) + }) + } } - }, [extensions, files, setFiles]) + }, [extensions, files, setFiles, t]) const openQuickPanel = useCallback(() => { onSelectFile() diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 82a157adba..f36af1635e 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -37,7 +37,7 @@ import { setSearching } from '@renderer/store/runtime' import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk' import { Assistant, FileType, FileTypes, KnowledgeBase, KnowledgeItem, Model, Topic } from '@renderer/types' import type { MessageInputBaseParams } from '@renderer/types/newMessage' -import { classNames, delay, formatFileSize } from '@renderer/utils' +import { classNames, delay, filterSupportedFiles, formatFileSize } from '@renderer/utils' import { formatQuotedText } from '@renderer/utils/formats' import { getFilesFromDropEvent, @@ -585,26 +585,20 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = setText(text + data) - const files = await getFilesFromDropEvent(e).catch((err) => { + const droppedFiles = await getFilesFromDropEvent(e).catch((err) => { logger.error('handleDrop:', err) return null }) - if (files) { - let supportedFiles = 0 - - files.forEach((file) => { - if (supportedExts.includes(file.ext)) { - setFiles((prevFiles) => [...prevFiles, file]) - supportedFiles++ - } - }) - - // 如果有文件,但都不支持 - if (files.length > 0 && supportedFiles === 0) { + if (droppedFiles) { + const supportedFiles = await filterSupportedFiles(droppedFiles, supportedExts) + supportedFiles.length > 0 && setFiles((prevFiles) => [...prevFiles, ...supportedFiles]) + if (droppedFiles.length > 0 && supportedFiles.length !== droppedFiles.length) { window.message.info({ key: 'file_not_supported', - content: t('chat.input.file_not_supported') + content: t('chat.input.file_not_supported_count', { + count: droppedFiles.length - supportedFiles.length + }) }) } } diff --git a/src/renderer/src/services/FileManager.ts b/src/renderer/src/services/FileManager.ts index f6b6945df4..4b780cfa94 100644 --- a/src/renderer/src/services/FileManager.ts +++ b/src/renderer/src/services/FileManager.ts @@ -132,7 +132,9 @@ class FileManager { } static getSafePath(file: FileMetadata) { - return this.isDangerFile(file) ? getFileDirectory(this.getFilePath(file)) : this.getFilePath(file) + // use the path from the file metadata instead + // this function is used to get path for files which are not in the filestorage + return this.isDangerFile(file) ? getFileDirectory(file.path) : file.path } static getFileUrl(file: FileMetadata) { diff --git a/src/renderer/src/services/PasteService.ts b/src/renderer/src/services/PasteService.ts index 845ec536dd..277bc9ef66 100644 --- a/src/renderer/src/services/PasteService.ts +++ b/src/renderer/src/services/PasteService.ts @@ -1,6 +1,6 @@ import { loggerService } from '@logger' import { FileMetadata } from '@renderer/types' -import { getFileExtension } from '@renderer/utils' +import { getFileExtension, isSupportedFile } from '@renderer/utils' const logger = loggerService.withContext('PasteService') @@ -60,6 +60,7 @@ export const handlePaste = async ( // 2. 文件/图片粘贴(仅在无文本时处理) if (event.clipboardData?.files && event.clipboardData.files.length > 0) { event.preventDefault() + const extensionSet = new Set(supportExts) try { for (const file of event.clipboardData.files) { // 使用新的API获取文件路径 @@ -90,7 +91,7 @@ export const handlePaste = async ( } // 有路径的情况 - if (supportExts.includes(getFileExtension(filePath))) { + if (await isSupportedFile(filePath, extensionSet)) { const selectedFile = await window.api.file.get(filePath) if (selectedFile) { setFiles((prevFiles) => [...prevFiles, selectedFile]) diff --git a/src/renderer/src/utils/file.ts b/src/renderer/src/utils/file.ts index 0c0f5039a6..6218e3f5c1 100644 --- a/src/renderer/src/utils/file.ts +++ b/src/renderer/src/utils/file.ts @@ -1,3 +1,4 @@ +import { FileMetadata } from '@renderer/types' import { KB, MB } from '@shared/config/constant' /** @@ -55,3 +56,30 @@ export function removeSpecialCharactersForFileName(str: string): string { .replace(/[\r\n]+/g, ' ') .trim() } + +export async function isSupportedFile(filePath: string, supportExts: Set): Promise { + try { + if (supportExts.has(getFileExtension(filePath))) { + return true + } + + if (await window.api.file.isTextFile(filePath)) { + return true + } + + return false + } catch (error) { + return false + } +} + +export async function filterSupportedFiles(files: FileMetadata[], supportExts: string[]): Promise { + const extensionSet = new Set(supportExts) + const validationResults = await Promise.all( + files.map(async (file) => ({ + file, + isValid: await isSupportedFile(file.path, extensionSet) + })) + ) + return validationResults.filter((result) => result.isValid).map((result) => result.file) +} diff --git a/yarn.lock b/yarn.lock index c0950f0a4a..b9833e0cc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8510,6 +8510,7 @@ __metadata: husky: "npm:^9.1.7" i18next: "npm:^23.11.5" iconv-lite: "npm:^0.6.3" + isbinaryfile: "npm:5.0.4" jaison: "npm:^2.0.2" jest-styled-components: "npm:^7.2.0" jsdom: "npm:26.1.0" @@ -14389,6 +14390,13 @@ __metadata: languageName: node linkType: hard +"isbinaryfile@npm:5.0.4, isbinaryfile@npm:^5.0.0": + version: 5.0.4 + resolution: "isbinaryfile@npm:5.0.4" + checksum: 10c0/fea255bfae67ff4827e8dd2238d6700d4803d02b4d892b72eeac4541487284e901251a3427966af5018d4eb29fa155b036dcb75dd217634146a072991afbc2c2 + languageName: node + linkType: hard + "isbinaryfile@npm:^4.0.8": version: 4.0.10 resolution: "isbinaryfile@npm:4.0.10" @@ -14396,13 +14404,6 @@ __metadata: languageName: node linkType: hard -"isbinaryfile@npm:^5.0.0": - version: 5.0.4 - resolution: "isbinaryfile@npm:5.0.4" - checksum: 10c0/fea255bfae67ff4827e8dd2238d6700d4803d02b4d892b72eeac4541487284e901251a3427966af5018d4eb29fa155b036dcb75dd217634146a072991afbc2c2 - languageName: node - linkType: hard - "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0"