feat: support Relative Path Input for Backup Directory (#8471)

* chore(env): add .env.example file and update .gitignore

- Introduced a new .env.example file with NODE_OPTIONS configuration.
- Updated .gitignore to exclude .env.example from being ignored.
- Added instructions in dev.md for copying .env.example to .env.

* fix(MessageTools): improve error handling and logging in message preview rendering (#8453)

- Enhanced the rendering logic for message previews by adding a try-catch block to handle JSON parsing errors more gracefully.
- Updated the error handling to provide clearer error messages in the preview when exceptions occur.
- Added debug logging to track the rendering process of message content.

* refactor(Theme): update theme management to use setTheme function

- Replaced toggleTheme with setTheme for more explicit theme handling.
- Removed unused SunMoon icon from TabContainer and Sidebar components.
- Updated theme icon rendering logic to directly reflect the current theme state.
- Adjusted ThemeProvider to include setTheme in context for better theme management.

* refactor(ModelList): streamline button layout and improve accessibility

- Removed tooltip wrappers from manage and add model buttons for a cleaner UI.
- Introduced a new Flex container for primary and default buttons, enhancing layout consistency.
- Updated button rendering to improve accessibility and user experience.

* feat(ModelList): add bulk add/remove functionality for models with confirmation dialog

- Implemented onAddAll and onRemoveAll functions to handle bulk actions for models.
- Added confirmation dialog for adding all models to the list, enhancing user experience.
- Updated translations for confirmation messages in multiple languages.

* chore(languages): update languages with a script (#8445)

* chore(languages): update languages with a script

* refactor: update languages and merge it into constants

* refactor: add usf and ush

* refactor(ipc): enhance write permission check and add untildify utility

- Updated the hasWritePermission function to resolve paths using the new untildify utility, improving path handling.
- Modified IPC handler to await the permission check for better asynchronous handling.
- Introduced a new untildify function to convert paths starting with '~' to the user's home directory.

* fix(ModelEdit): enhance model type management and introduce new selection logic (#8420)

* fix(ModelEdit): enhance model type management and introduce new selection logic

- Added support for 'rerank' model type in the ModelEditContent component.
- Refactored type selection logic to utilize new utility functions for finding differences and unions in model types.
- Updated model type handling to include user selection status, improving user experience in type management.
- Adjusted migration logic to initialize newType for existing models, ensuring backward compatibility.
- Introduced isUserSelectedModelType utility to streamline model type checks across the application.

* refactor(isFunctionCallingModel): simplify model type check logic

- Replaced the inline check for 'function_calling' model type with a call to the new utility function isUserSelectedModelType, enhancing code clarity and maintainability.

* feat(collection): add utility functions for array operations

- Introduced `findIntersection`, `findDifference`, and `findUnion` functions to handle array operations with support for custom key selectors and comparison functions.
- Removed previous implementations from `index.ts` to streamline utility exports.
- Added comprehensive tests for new functions covering basic types and object types with various edge cases.

* refactor(collection): rename utility functions for clarity

- Renamed `findIntersection`, `findDifference`, and `findUnion` to `getIntersection`, `getDifference`, and `getUnion` respectively for improved clarity and consistency in naming.
- Updated corresponding tests to reflect the new function names, ensuring all tests pass with the updated utility functions.

* refactor(ModelEditContent): update model type management and improve selection logic

- Replaced utility function calls to `findDifference` and `findUnion` with `getDifference` and `getUnion` for consistency.
- Introduced temporary state management for model types to enhance user selection handling.
- Added a reset functionality for model type selections, improving user experience.
- Updated the rendering logic to conditionally disable certain model types based on user selections.

* fix(ModelEditContent): enhance model type selection logic with conditional disabling

- Introduced logic to conditionally disable 'rerank' and 'embedding' model types based on user selections.
- Updated the state management for model types to ensure correct user selection handling.
- Improved the confirmation modal to reflect the updated selection logic for better user experience.

* fix(ModelEditContent): refine model type selection and update confirmation logic

- Enhanced the logic for model type selection to ensure accurate user selections for 'rerank' and 'embedding'.
- Updated the confirmation modal to reflect changes in selection handling, improving user experience.
- Adjusted state management to correctly handle updates based on selected model types.

* fix(models): update model support logic to include 'qwen3-235b-a22b-instruct'

* refactor(models): rename 'newType' to 'capabilities' and update related logic in ModelEditContent and migration scripts

* feat(ipc): add App_ResolvePath channel and update path handling

- Introduced a new IPC channel `App_ResolvePath` to resolve file paths, enhancing path management.
- Updated the `hasWritePermission` function to log the original directory instead of the resolved one.
- Modified the `LocalBackupSettings` component to utilize the new `resolvePath` method for improved directory validation.

* add ut

* fix comments

* fix clear manually

* delete duplicate var

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>
Co-authored-by: SuYao <sy20010504@gmail.com>
Co-authored-by: one <wangan.cs@gmail.com>
This commit is contained in:
beyondkmp
2025-07-25 16:51:59 +08:00
committed by GitHub
parent 5918f800d7
commit 03b996d626
6 changed files with 89 additions and 14 deletions

View File

@@ -20,6 +20,7 @@ export enum IpcChannel {
App_HandleZoomFactor = 'app:handle-zoom-factor',
App_Select = 'app:select',
App_HasWritePermission = 'app:has-write-permission',
App_ResolvePath = 'app:resolve-path',
App_Copy = 'app:copy',
App_SetStopQuitApp = 'app:set-stop-quit-app',
App_SetAppDataPath = 'app:set-app-data-path',

View File

@@ -55,7 +55,7 @@ import { setOpenLinkExternal } from './services/WebviewService'
import { windowService } from './services/WindowService'
import { calculateDirectorySize, getResourcePath } from './utils'
import { decrypt, encrypt } from './utils/aes'
import { getCacheDir, getConfigDir, getFilesDir, hasWritePermission } from './utils/file'
import { getCacheDir, getConfigDir, getFilesDir, hasWritePermission, untildify } from './utils/file'
import { updateAppDataConfig } from './utils/init'
import { compress, decompress } from './utils/zip'
@@ -286,7 +286,12 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
})
ipcMain.handle(IpcChannel.App_HasWritePermission, async (_, filePath: string) => {
return hasWritePermission(filePath)
const hasPermission = await hasWritePermission(filePath)
return hasPermission
})
ipcMain.handle(IpcChannel.App_ResolvePath, async (_, filePath: string) => {
return path.resolve(untildify(filePath))
})
// Set app data path

View File

@@ -9,7 +9,7 @@ import { detectAll as detectEncodingAll } from 'jschardet'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { readTextFileWithAutoEncoding } from '../file'
import { getAllFiles, getAppConfigDir, getConfigDir, getFilesDir, getFileType, getTempDir } from '../file'
import { getAllFiles, getAppConfigDir, getConfigDir, getFilesDir, getFileType, getTempDir, untildify } from '../file'
// Mock dependencies
vi.mock('node:fs')
@@ -296,4 +296,51 @@ describe('file', () => {
expect(result).toBe(content)
})
})
describe('untildify', () => {
it('should replace ~ with home directory for paths starting with ~', () => {
const mockHome = '/mock/home'
expect(untildify('~')).toBe(mockHome)
expect(untildify('~/Documents')).toBe('/mock/home/Documents')
expect(untildify('~\\Documents')).toBe('/mock/home\\Documents')
expect(untildify('~/Documents/file.txt')).toBe('/mock/home/Documents/file.txt')
expect(untildify('~\\Documents\\file.txt')).toBe('/mock/home\\Documents\\file.txt')
})
it('should not replace ~ when not at the beginning', () => {
expect(untildify('folder/~/file')).toBe('folder/~/file')
expect(untildify('/home/user/~')).toBe('/home/user/~')
expect(untildify('Documents/~backup')).toBe('Documents/~backup')
})
it('should not replace ~ when not followed by path separator or end of string', () => {
expect(untildify('~abc')).toBe('~abc')
expect(untildify('~user')).toBe('~user')
expect(untildify('~file.txt')).toBe('~file.txt')
})
it('should handle paths that do not start with ~', () => {
expect(untildify('/absolute/path')).toBe('/absolute/path')
expect(untildify('./relative/path')).toBe('./relative/path')
expect(untildify('../parent/path')).toBe('../parent/path')
expect(untildify('relative/path')).toBe('relative/path')
expect(untildify('C:\\Windows\\System32')).toBe('C:\\Windows\\System32')
})
it('should handle edge cases', () => {
expect(untildify('')).toBe('')
expect(untildify(' ')).toBe(' ')
expect(untildify('~/')).toBe('/mock/home/')
expect(untildify('~\\')).toBe('/mock/home\\')
})
it('should handle special characters and unicode', () => {
expect(untildify('~/文档')).toBe('/mock/home/文档')
expect(untildify('~/папка')).toBe('/mock/home/папка')
expect(untildify('~/folder with spaces')).toBe('/mock/home/folder with spaces')
expect(untildify('~/folder-with-dashes')).toBe('/mock/home/folder-with-dashes')
expect(untildify('~/folder_with_underscores')).toBe('/mock/home/folder_with_underscores')
})
})
})

View File

@@ -28,9 +28,18 @@ function initFileTypeMap() {
// 初始化映射表
initFileTypeMap()
export function hasWritePermission(path: string) {
export function untildify(pathWithTilde: string) {
if (pathWithTilde.startsWith('~')) {
const homeDirectory = os.homedir()
return pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory)
}
return pathWithTilde
}
export async function hasWritePermission(dir: string) {
try {
fs.accessSync(path, fs.constants.W_OK)
logger.info(`Checking write permission for ${dir}`)
await fs.promises.access(dir, fs.constants.W_OK)
return true
} catch (error) {
return false

View File

@@ -59,6 +59,7 @@ const api = {
setAutoUpdate: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetAutoUpdate, isActive),
select: (options: Electron.OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.App_Select, options),
hasWritePermission: (path: string) => ipcRenderer.invoke(IpcChannel.App_HasWritePermission, path),
resolvePath: (path: string) => ipcRenderer.invoke(IpcChannel.App_ResolvePath, path),
setAppDataPath: (path: string) => ipcRenderer.invoke(IpcChannel.App_SetAppDataPath, path),
getDataPathFromArgs: () => ipcRenderer.invoke(IpcChannel.App_GetDataPathFromArgs),
copy: (oldPath: string, newPath: string, occupiedDirs: string[] = []) =>

View File

@@ -71,22 +71,24 @@ const LocalBackupSettings: React.FC = () => {
return false
}
const resolvedDir = await window.api.resolvePath(dir)
// check new local backup dir is not in app data path
// if is in app data path, show error
if (dir.startsWith(appInfo!.appDataPath)) {
if (resolvedDir.startsWith(appInfo!.appDataPath)) {
window.message.error(t('settings.data.local.directory.select_error_app_data_path'))
return false
}
// check new local backup dir is not in app install path
// if is in app install path, show error
if (dir.startsWith(appInfo!.installPath)) {
if (resolvedDir.startsWith(appInfo!.installPath)) {
window.message.error(t('settings.data.local.directory.select_error_in_app_install_path'))
return false
}
// check new app data path has write permission
const hasWritePermission = await window.api.hasWritePermission(dir)
const hasWritePermission = await window.api.hasWritePermission(resolvedDir)
if (!hasWritePermission) {
window.message.error(t('settings.data.local.directory.select_error_write_permission'))
return false
@@ -96,6 +98,15 @@ const LocalBackupSettings: React.FC = () => {
}
const handleLocalBackupDirChange = async (value: string) => {
if (value === localBackupDirSetting) {
return
}
if (value === '') {
handleClearDirectory()
return
}
if (await checkLocalBackupDirValid(value)) {
setLocalBackupDir(value)
dispatch(_setLocalBackupDir(value))
@@ -107,10 +118,10 @@ const LocalBackupSettings: React.FC = () => {
return
}
setLocalBackupDir('')
dispatch(_setLocalBackupDir(''))
dispatch(setLocalBackupAutoSync(false))
stopAutoSync('local')
if (localBackupDirSetting) {
setLocalBackupDir(localBackupDirSetting)
return
}
}
const onMaxBackupsChange = (value: number) => {
@@ -134,7 +145,7 @@ const LocalBackupSettings: React.FC = () => {
return
}
handleLocalBackupDirChange(newLocalBackupDir)
await handleLocalBackupDirChange(newLocalBackupDir)
} catch (error) {
logger.error('Failed to select directory:', error as Error)
}
@@ -191,7 +202,8 @@ const LocalBackupSettings: React.FC = () => {
<HStack gap="5px">
<Input
value={localBackupDir}
readOnly
onChange={(e) => setLocalBackupDir(e.target.value)}
onBlur={(e) => handleLocalBackupDirChange(e.target.value)}
placeholder={t('settings.data.local.directory.placeholder')}
style={{ minWidth: 200, maxWidth: 400, flex: 1 }}
/>