@lobehub/chat 1.82.10 → 1.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/.env.desktop +1 -2
  2. package/.github/workflows/{release-desktop.yml → desktop-pr-build.yml} +59 -137
  3. package/.github/workflows/release-desktop-beta.yml +196 -0
  4. package/CHANGELOG.md +25 -0
  5. package/apps/desktop/.i18nrc.js +31 -0
  6. package/apps/desktop/Development.md +47 -0
  7. package/apps/desktop/README.md +6 -0
  8. package/apps/desktop/build/Icon-beta.icns +0 -0
  9. package/apps/desktop/build/Icon-nightly.icns +0 -0
  10. package/apps/desktop/build/Icon.icns +0 -0
  11. package/apps/desktop/build/entitlements.mac.plist +12 -0
  12. package/apps/desktop/build/favicon.ico +0 -0
  13. package/apps/desktop/build/icon-beta.png +0 -0
  14. package/apps/desktop/build/icon-dev.png +0 -0
  15. package/apps/desktop/build/icon-nightly.ico +0 -0
  16. package/apps/desktop/build/icon-nightly.png +0 -0
  17. package/apps/desktop/build/icon.ico +0 -0
  18. package/apps/desktop/build/icon.png +0 -0
  19. package/apps/desktop/dev-app-update.yml +6 -0
  20. package/apps/desktop/electron-builder.js +92 -0
  21. package/apps/desktop/electron.vite.config.ts +40 -0
  22. package/apps/desktop/package.json +72 -0
  23. package/apps/desktop/pnpm-workspace.yaml +5 -0
  24. package/apps/desktop/resources/error.html +136 -0
  25. package/apps/desktop/resources/locales/ar/common.json +32 -0
  26. package/apps/desktop/resources/locales/ar/dialog.json +31 -0
  27. package/apps/desktop/resources/locales/ar/menu.json +70 -0
  28. package/apps/desktop/resources/locales/bg-BG/common.json +32 -0
  29. package/apps/desktop/resources/locales/bg-BG/dialog.json +31 -0
  30. package/apps/desktop/resources/locales/bg-BG/menu.json +70 -0
  31. package/apps/desktop/resources/locales/de-DE/common.json +32 -0
  32. package/apps/desktop/resources/locales/de-DE/dialog.json +31 -0
  33. package/apps/desktop/resources/locales/de-DE/menu.json +70 -0
  34. package/apps/desktop/resources/locales/en-US/common.json +32 -0
  35. package/apps/desktop/resources/locales/en-US/dialog.json +31 -0
  36. package/apps/desktop/resources/locales/en-US/menu.json +70 -0
  37. package/apps/desktop/resources/locales/es-ES/common.json +32 -0
  38. package/apps/desktop/resources/locales/es-ES/dialog.json +31 -0
  39. package/apps/desktop/resources/locales/es-ES/menu.json +70 -0
  40. package/apps/desktop/resources/locales/fa-IR/common.json +32 -0
  41. package/apps/desktop/resources/locales/fa-IR/dialog.json +31 -0
  42. package/apps/desktop/resources/locales/fa-IR/menu.json +70 -0
  43. package/apps/desktop/resources/locales/fr-FR/common.json +32 -0
  44. package/apps/desktop/resources/locales/fr-FR/dialog.json +31 -0
  45. package/apps/desktop/resources/locales/fr-FR/menu.json +70 -0
  46. package/apps/desktop/resources/locales/it-IT/common.json +32 -0
  47. package/apps/desktop/resources/locales/it-IT/dialog.json +31 -0
  48. package/apps/desktop/resources/locales/it-IT/menu.json +70 -0
  49. package/apps/desktop/resources/locales/ja-JP/common.json +32 -0
  50. package/apps/desktop/resources/locales/ja-JP/dialog.json +31 -0
  51. package/apps/desktop/resources/locales/ja-JP/menu.json +70 -0
  52. package/apps/desktop/resources/locales/ko-KR/common.json +32 -0
  53. package/apps/desktop/resources/locales/ko-KR/dialog.json +31 -0
  54. package/apps/desktop/resources/locales/ko-KR/menu.json +70 -0
  55. package/apps/desktop/resources/locales/nl-NL/common.json +32 -0
  56. package/apps/desktop/resources/locales/nl-NL/dialog.json +31 -0
  57. package/apps/desktop/resources/locales/nl-NL/menu.json +70 -0
  58. package/apps/desktop/resources/locales/pl-PL/common.json +32 -0
  59. package/apps/desktop/resources/locales/pl-PL/dialog.json +31 -0
  60. package/apps/desktop/resources/locales/pl-PL/menu.json +70 -0
  61. package/apps/desktop/resources/locales/pt-BR/common.json +32 -0
  62. package/apps/desktop/resources/locales/pt-BR/dialog.json +31 -0
  63. package/apps/desktop/resources/locales/pt-BR/menu.json +70 -0
  64. package/apps/desktop/resources/locales/ru-RU/common.json +32 -0
  65. package/apps/desktop/resources/locales/ru-RU/dialog.json +31 -0
  66. package/apps/desktop/resources/locales/ru-RU/menu.json +70 -0
  67. package/apps/desktop/resources/locales/tr-TR/common.json +32 -0
  68. package/apps/desktop/resources/locales/tr-TR/dialog.json +31 -0
  69. package/apps/desktop/resources/locales/tr-TR/menu.json +70 -0
  70. package/apps/desktop/resources/locales/vi-VN/common.json +32 -0
  71. package/apps/desktop/resources/locales/vi-VN/dialog.json +31 -0
  72. package/apps/desktop/resources/locales/vi-VN/menu.json +70 -0
  73. package/apps/desktop/resources/locales/zh-CN/common.json +32 -0
  74. package/apps/desktop/resources/locales/zh-CN/dialog.json +31 -0
  75. package/apps/desktop/resources/locales/zh-CN/menu.json +70 -0
  76. package/apps/desktop/resources/locales/zh-TW/common.json +32 -0
  77. package/apps/desktop/resources/locales/zh-TW/dialog.json +31 -0
  78. package/apps/desktop/resources/locales/zh-TW/menu.json +70 -0
  79. package/apps/desktop/resources/splash.html +88 -0
  80. package/apps/desktop/scripts/i18nWorkflow/const.ts +18 -0
  81. package/apps/desktop/scripts/i18nWorkflow/genDefaultLocale.ts +35 -0
  82. package/apps/desktop/scripts/i18nWorkflow/genDiff.ts +57 -0
  83. package/apps/desktop/scripts/i18nWorkflow/index.ts +35 -0
  84. package/apps/desktop/scripts/i18nWorkflow/utils.ts +54 -0
  85. package/apps/desktop/scripts/pglite-server.ts +14 -0
  86. package/apps/desktop/src/common/routes.ts +78 -0
  87. package/apps/desktop/src/main/appBrowsers.ts +47 -0
  88. package/apps/desktop/src/main/const/dir.ts +29 -0
  89. package/apps/desktop/src/main/const/env.ts +3 -0
  90. package/apps/desktop/src/main/const/store.ts +22 -0
  91. package/apps/desktop/src/main/controllers/AuthCtr.ts +390 -0
  92. package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +95 -0
  93. package/apps/desktop/src/main/controllers/DevtoolsCtr.ts +9 -0
  94. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +380 -0
  95. package/apps/desktop/src/main/controllers/MenuCtr.ts +29 -0
  96. package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +335 -0
  97. package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +321 -0
  98. package/apps/desktop/src/main/controllers/ShortcutCtr.ts +19 -0
  99. package/apps/desktop/src/main/controllers/SystemCtr.ts +93 -0
  100. package/apps/desktop/src/main/controllers/UpdaterCtr.ts +43 -0
  101. package/apps/desktop/src/main/controllers/UploadFileCtr.ts +34 -0
  102. package/apps/desktop/src/main/controllers/_template.ts +9 -0
  103. package/apps/desktop/src/main/controllers/index.ts +58 -0
  104. package/apps/desktop/src/main/core/App.ts +370 -0
  105. package/apps/desktop/src/main/core/Browser.ts +345 -0
  106. package/apps/desktop/src/main/core/BrowserManager.ts +154 -0
  107. package/apps/desktop/src/main/core/I18nManager.ts +185 -0
  108. package/apps/desktop/src/main/core/IoCContainer.ts +12 -0
  109. package/apps/desktop/src/main/core/MenuManager.ts +64 -0
  110. package/apps/desktop/src/main/core/ShortcutManager.ts +173 -0
  111. package/apps/desktop/src/main/core/StoreManager.ts +89 -0
  112. package/apps/desktop/src/main/core/UpdaterManager.ts +321 -0
  113. package/apps/desktop/src/main/index.ts +5 -0
  114. package/apps/desktop/src/main/locales/default/common.ts +34 -0
  115. package/apps/desktop/src/main/locales/default/dialog.ts +33 -0
  116. package/apps/desktop/src/main/locales/default/index.ts +11 -0
  117. package/apps/desktop/src/main/locales/default/menu.ts +72 -0
  118. package/apps/desktop/src/main/locales/resources.ts +35 -0
  119. package/apps/desktop/src/main/menus/impls/BaseMenuPlatform.ts +10 -0
  120. package/apps/desktop/src/main/menus/impls/linux.ts +243 -0
  121. package/apps/desktop/src/main/menus/impls/macOS.ts +360 -0
  122. package/apps/desktop/src/main/menus/impls/windows.ts +226 -0
  123. package/apps/desktop/src/main/menus/index.ts +34 -0
  124. package/apps/desktop/src/main/menus/types.ts +28 -0
  125. package/apps/desktop/src/main/modules/fileSearch/impl/macOS.ts +577 -0
  126. package/apps/desktop/src/main/modules/fileSearch/index.ts +23 -0
  127. package/apps/desktop/src/main/modules/fileSearch/type.ts +27 -0
  128. package/apps/desktop/src/main/modules/updater/configs.ts +22 -0
  129. package/apps/desktop/src/main/modules/updater/utils.ts +33 -0
  130. package/apps/desktop/src/main/services/fileSearchSrv.ts +35 -0
  131. package/apps/desktop/src/main/services/fileSrv.ts +255 -0
  132. package/apps/desktop/src/main/services/index.ts +9 -0
  133. package/apps/desktop/src/main/shortcuts/config.ts +18 -0
  134. package/apps/desktop/src/main/shortcuts/index.ts +1 -0
  135. package/apps/desktop/src/main/types/fileSearch.ts +51 -0
  136. package/apps/desktop/src/main/types/store.ts +14 -0
  137. package/apps/desktop/src/main/utils/file-system.ts +15 -0
  138. package/apps/desktop/src/main/utils/logger.ts +44 -0
  139. package/apps/desktop/src/main/utils/next-electron-rsc.ts +383 -0
  140. package/apps/desktop/src/preload/electronApi.ts +18 -0
  141. package/apps/desktop/src/preload/index.ts +14 -0
  142. package/apps/desktop/src/preload/invoke.ts +10 -0
  143. package/apps/desktop/src/preload/routeInterceptor.ts +162 -0
  144. package/apps/desktop/tsconfig.json +21 -0
  145. package/changelog/v1.json +9 -0
  146. package/package.json +1 -1
  147. package/packages/electron-client-ipc/src/events/remoteServer.ts +11 -4
  148. package/packages/electron-client-ipc/src/types/dataSync.ts +15 -0
  149. package/packages/electron-client-ipc/src/types/index.ts +2 -1
  150. package/packages/electron-client-ipc/src/types/proxyTRPCRequest.ts +21 -0
  151. package/packages/electron-server-ipc/src/const.ts +3 -3
  152. package/packages/electron-server-ipc/src/ipcClient.test.ts +7 -6
  153. package/packages/electron-server-ipc/src/ipcClient.ts +17 -8
  154. package/packages/electron-server-ipc/src/ipcServer.ts +7 -3
  155. package/scripts/electronWorkflow/setDesktopVersion.ts +60 -43
  156. package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +1 -1
  157. package/src/components/Analytics/Desktop.tsx +19 -0
  158. package/src/components/Analytics/index.tsx +3 -0
  159. package/src/database/core/db-adaptor.ts +4 -1
  160. package/src/database/core/electron.ts +317 -0
  161. package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx → features/ElectronTitlebar/Connection/ConnectionMode.tsx} +24 -21
  162. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Option.tsx +3 -5
  163. package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx → features/ElectronTitlebar/Connection/RemoteStatus.tsx} +10 -7
  164. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/index.tsx +4 -4
  165. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateModal.tsx +2 -1
  166. package/src/libs/trpc/client/async.ts +6 -0
  167. package/src/libs/trpc/client/edge.ts +6 -0
  168. package/src/libs/trpc/client/helpers/desktopRemoteRPCFetch.ts +72 -0
  169. package/src/libs/trpc/client/index.ts +1 -0
  170. package/src/libs/trpc/client/lambda.ts +10 -1
  171. package/src/libs/trpc/client/tools.ts +6 -0
  172. package/src/server/globalConfig/index.ts +0 -3
  173. package/src/server/modules/ElectronIPCClient/index.ts +3 -1
  174. package/src/server/routers/desktop/index.ts +2 -0
  175. package/src/server/routers/desktop/mcp.ts +47 -0
  176. package/src/server/routers/lambda/user.ts +38 -23
  177. package/src/server/routers/tools/mcp.ts +0 -6
  178. package/src/services/electron/remoteServer.ts +4 -4
  179. package/src/services/mcp.ts +17 -7
  180. package/src/services/upload.ts +9 -0
  181. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +11 -2
  182. package/src/store/chat/slices/builtinTool/actions/localFile.ts +110 -53
  183. package/src/store/electron/actions/sync.ts +20 -19
  184. package/src/store/electron/initialState.ts +3 -3
  185. package/src/store/electron/selectors/sync.ts +6 -3
  186. package/src/store/electron/store.ts +2 -0
  187. package/src/store/file/slices/upload/action.ts +11 -3
  188. package/src/store/tool/selectors/tool.ts +10 -1
  189. package/src/utils/fetch/headers.ts +27 -0
  190. package/src/utils/fetch/index.ts +2 -0
  191. package/src/utils/fetch/request.ts +28 -0
  192. package/packages/electron-client-ipc/src/types/remoteServer.ts +0 -8
  193. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Waiting.tsx +0 -0
  194. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateNotification.tsx +0 -0
  195. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/index.tsx +0 -0
@@ -0,0 +1,380 @@
1
+ import {
2
+ ListLocalFileParams,
3
+ LocalMoveFilesResultItem,
4
+ LocalReadFileParams,
5
+ LocalReadFileResult,
6
+ LocalReadFilesParams,
7
+ LocalSearchFilesParams,
8
+ MoveLocalFilesParams,
9
+ OpenLocalFileParams,
10
+ OpenLocalFolderParams,
11
+ RenameLocalFileResult,
12
+ } from '@lobechat/electron-client-ipc';
13
+ import { SYSTEM_FILES_TO_IGNORE, loadFile } from '@lobechat/file-loaders';
14
+ import { shell } from 'electron';
15
+ import * as fs from 'node:fs';
16
+ import { rename as renamePromise } from 'node:fs/promises';
17
+ import * as path from 'node:path';
18
+ import { promisify } from 'node:util';
19
+
20
+ import FileSearchService from '@/services/fileSearchSrv';
21
+ import { FileResult, SearchOptions } from '@/types/fileSearch';
22
+ import { makeSureDirExist } from '@/utils/file-system';
23
+
24
+ import { ControllerModule, ipcClientEvent } from './index';
25
+
26
+ const statPromise = promisify(fs.stat);
27
+ const readdirPromise = promisify(fs.readdir);
28
+ const renamePromiseFs = promisify(fs.rename);
29
+ const accessPromise = promisify(fs.access);
30
+
31
+ export default class LocalFileCtr extends ControllerModule {
32
+ private get searchService() {
33
+ return this.app.getService(FileSearchService);
34
+ }
35
+
36
+ /**
37
+ * Handle IPC event for local file search
38
+ */
39
+ @ipcClientEvent('searchLocalFiles')
40
+ async handleLocalFilesSearch(params: LocalSearchFilesParams): Promise<FileResult[]> {
41
+ const options: Omit<SearchOptions, 'keywords'> = {
42
+ limit: 30,
43
+ };
44
+
45
+ return this.searchService.search(params.keywords, options);
46
+ }
47
+
48
+ @ipcClientEvent('openLocalFile')
49
+ async handleOpenLocalFile({ path: filePath }: OpenLocalFileParams): Promise<{
50
+ error?: string;
51
+ success: boolean;
52
+ }> {
53
+ try {
54
+ await shell.openPath(filePath);
55
+ return { success: true };
56
+ } catch (error) {
57
+ console.error(`Failed to open file ${filePath}:`, error);
58
+ return { error: (error as Error).message, success: false };
59
+ }
60
+ }
61
+
62
+ @ipcClientEvent('openLocalFolder')
63
+ async handleOpenLocalFolder({ path: targetPath, isDirectory }: OpenLocalFolderParams): Promise<{
64
+ error?: string;
65
+ success: boolean;
66
+ }> {
67
+ try {
68
+ const folderPath = isDirectory ? targetPath : path.dirname(targetPath);
69
+ await shell.openPath(folderPath);
70
+ return { success: true };
71
+ } catch (error) {
72
+ console.error(`Failed to open folder for path ${targetPath}:`, error);
73
+ return { error: (error as Error).message, success: false };
74
+ }
75
+ }
76
+
77
+ @ipcClientEvent('readLocalFiles')
78
+ async readFiles({ paths }: LocalReadFilesParams): Promise<LocalReadFileResult[]> {
79
+ const results: LocalReadFileResult[] = [];
80
+
81
+ for (const filePath of paths) {
82
+ // 初始化结果对象
83
+ const result = await this.readFile({ path: filePath });
84
+
85
+ results.push(result);
86
+ }
87
+
88
+ return results;
89
+ }
90
+
91
+ @ipcClientEvent('readLocalFile')
92
+ async readFile({ path: filePath, loc }: LocalReadFileParams): Promise<LocalReadFileResult> {
93
+ try {
94
+ const effectiveLoc = loc ?? [0, 200];
95
+
96
+ const fileDocument = await loadFile(filePath);
97
+
98
+ const [startLine, endLine] = effectiveLoc;
99
+ const lines = fileDocument.content.split('\n');
100
+ const totalLineCount = lines.length;
101
+ const totalCharCount = fileDocument.content.length;
102
+
103
+ // Adjust slice indices to be 0-based and inclusive/exclusive
104
+ const selectedLines = lines.slice(startLine, endLine);
105
+ const content = selectedLines.join('\n');
106
+ const charCount = content.length;
107
+ const lineCount = selectedLines.length;
108
+
109
+ const result: LocalReadFileResult = {
110
+ // Char count for the selected range
111
+ charCount,
112
+ // Content for the selected range
113
+ content,
114
+ createdTime: fileDocument.createdTime,
115
+ fileType: fileDocument.fileType,
116
+ filename: fileDocument.filename,
117
+ lineCount,
118
+ loc: effectiveLoc,
119
+ // Line count for the selected range
120
+ modifiedTime: fileDocument.modifiedTime,
121
+
122
+ // Total char count of the file
123
+ totalCharCount,
124
+ // Total line count of the file
125
+ totalLineCount,
126
+ };
127
+
128
+ try {
129
+ const stats = await statPromise(filePath);
130
+ if (stats.isDirectory()) {
131
+ result.content = 'This is a directory and cannot be read as plain text.';
132
+ result.charCount = 0;
133
+ result.lineCount = 0;
134
+ // Keep total counts for directory as 0 as well, or decide if they should reflect metadata size
135
+ result.totalCharCount = 0;
136
+ result.totalLineCount = 0;
137
+ }
138
+ } catch (statError) {
139
+ console.error(`Stat failed for ${filePath} after loadFile:`, statError);
140
+ }
141
+
142
+ return result;
143
+ } catch (error) {
144
+ console.error(`Error processing file ${filePath}:`, error);
145
+ const errorMessage = (error as Error).message;
146
+ return {
147
+ charCount: 0,
148
+ content: `Error accessing or processing file: ${errorMessage}`,
149
+ createdTime: new Date(),
150
+ fileType: path.extname(filePath).toLowerCase().replace('.', '') || 'unknown',
151
+ filename: path.basename(filePath),
152
+ lineCount: 0,
153
+ loc: [0, 0],
154
+ modifiedTime: new Date(),
155
+ totalCharCount: 0, // Add total counts to error result
156
+ totalLineCount: 0,
157
+ };
158
+ }
159
+ }
160
+
161
+ @ipcClientEvent('listLocalFiles')
162
+ async listLocalFiles({ path: dirPath }: ListLocalFileParams): Promise<FileResult[]> {
163
+ const results: FileResult[] = [];
164
+ try {
165
+ const entries = await readdirPromise(dirPath);
166
+
167
+ for (const entry of entries) {
168
+ // Skip specific system files based on the ignore list
169
+ if (SYSTEM_FILES_TO_IGNORE.includes(entry)) {
170
+ continue;
171
+ }
172
+
173
+ const fullPath = path.join(dirPath, entry);
174
+ try {
175
+ const stats = await statPromise(fullPath);
176
+ const isDirectory = stats.isDirectory();
177
+ results.push({
178
+ createdTime: stats.birthtime,
179
+ isDirectory,
180
+ lastAccessTime: stats.atime,
181
+ modifiedTime: stats.mtime,
182
+ name: entry,
183
+ path: fullPath,
184
+ size: stats.size,
185
+ type: isDirectory ? 'directory' : path.extname(entry).toLowerCase().replace('.', ''),
186
+ });
187
+ } catch (statError) {
188
+ // Silently ignore files we can't stat (e.g. permissions)
189
+ console.error(`Failed to stat ${fullPath}:`, statError);
190
+ }
191
+ }
192
+
193
+ // Sort entries: folders first, then by name
194
+ results.sort((a, b) => {
195
+ if (a.isDirectory !== b.isDirectory) {
196
+ return a.isDirectory ? -1 : 1; // Directories first
197
+ }
198
+ // Add null/undefined checks for robustness if needed, though names should exist
199
+ return (a.name || '').localeCompare(b.name || ''); // Then sort by name
200
+ });
201
+
202
+ return results;
203
+ } catch (error) {
204
+ console.error(`Failed to list directory ${dirPath}:`, error);
205
+ // Rethrow or return an empty array/error object depending on desired behavior
206
+ // For now, returning empty array on error listing directory itself
207
+ return [];
208
+ }
209
+ }
210
+
211
+ @ipcClientEvent('moveLocalFiles')
212
+ async handleMoveFiles({ items }: MoveLocalFilesParams): Promise<LocalMoveFilesResultItem[]> {
213
+ const results: LocalMoveFilesResultItem[] = [];
214
+
215
+ if (!items || items.length === 0) {
216
+ console.warn('moveLocalFiles called with empty items array.');
217
+ return [];
218
+ }
219
+
220
+ // 逐个处理移动请求
221
+ for (const item of items) {
222
+ const { oldPath: sourcePath, newPath } = item;
223
+ const resultItem: LocalMoveFilesResultItem = {
224
+ newPath: undefined,
225
+ sourcePath,
226
+ success: false,
227
+ };
228
+
229
+ // 基本验证
230
+ if (!sourcePath || !newPath) {
231
+ resultItem.error = 'Both oldPath and newPath are required for each item.';
232
+ results.push(resultItem);
233
+ continue;
234
+ }
235
+
236
+ try {
237
+ // 检查源是否存在
238
+ try {
239
+ await accessPromise(sourcePath, fs.constants.F_OK);
240
+ } catch (accessError: any) {
241
+ if (accessError.code === 'ENOENT') {
242
+ throw new Error(`Source path not found: ${sourcePath}`);
243
+ } else {
244
+ throw new Error(
245
+ `Permission denied accessing source path: ${sourcePath}. ${accessError.message}`,
246
+ );
247
+ }
248
+ }
249
+
250
+ // 检查目标路径是否与源路径相同
251
+ if (path.normalize(sourcePath) === path.normalize(newPath)) {
252
+ console.log(`Skipping move: source and target path are identical: ${sourcePath}`);
253
+ resultItem.success = true;
254
+ resultItem.newPath = newPath; // 即使未移动,也报告目标路径
255
+ results.push(resultItem);
256
+ continue;
257
+ }
258
+
259
+ // LBYL: 确保目标目录存在
260
+ const targetDir = path.dirname(newPath);
261
+ makeSureDirExist(targetDir);
262
+
263
+ // 执行移动 (rename)
264
+ await renamePromiseFs(sourcePath, newPath);
265
+ resultItem.success = true;
266
+ resultItem.newPath = newPath;
267
+ console.log(`Successfully moved ${sourcePath} to ${newPath}`);
268
+ } catch (error) {
269
+ console.error(`Error moving ${sourcePath} to ${newPath}:`, error);
270
+ // 使用与 handleMoveFile 类似的错误处理逻辑
271
+ let errorMessage = (error as Error).message;
272
+ if ((error as any).code === 'ENOENT')
273
+ errorMessage = `Source path not found: ${sourcePath}.`;
274
+ else if ((error as any).code === 'EPERM' || (error as any).code === 'EACCES')
275
+ errorMessage = `Permission denied to move the item at ${sourcePath}. Check file/folder permissions.`;
276
+ else if ((error as any).code === 'EBUSY')
277
+ errorMessage = `The file or directory at ${sourcePath} or ${newPath} is busy or locked by another process.`;
278
+ else if ((error as any).code === 'EXDEV')
279
+ errorMessage = `Cannot move across different file systems or drives. Source: ${sourcePath}, Target: ${newPath}.`;
280
+ else if ((error as any).code === 'EISDIR')
281
+ errorMessage = `Cannot overwrite a directory with a file, or vice versa. Source: ${sourcePath}, Target: ${newPath}.`;
282
+ else if ((error as any).code === 'ENOTEMPTY')
283
+ errorMessage = `The target directory ${newPath} is not empty (relevant on some systems if target exists and is a directory).`;
284
+ else if ((error as any).code === 'EEXIST')
285
+ errorMessage = `An item already exists at the target path: ${newPath}.`;
286
+ // 保留来自访问检查或目录检查的更具体错误
287
+ else if (
288
+ !errorMessage.startsWith('Source path not found') &&
289
+ !errorMessage.startsWith('Permission denied accessing source path') &&
290
+ !errorMessage.includes('Target directory')
291
+ ) {
292
+ // Keep the original error message if none of the specific codes match
293
+ }
294
+ resultItem.error = errorMessage;
295
+ }
296
+ results.push(resultItem);
297
+ }
298
+
299
+ return results;
300
+ }
301
+
302
+ @ipcClientEvent('renameLocalFile')
303
+ async handleRenameFile({
304
+ path: currentPath,
305
+ newName,
306
+ }: {
307
+ newName: string;
308
+ path: string;
309
+ }): Promise<RenameLocalFileResult> {
310
+ // Basic validation (can also be done in frontend action)
311
+ if (!currentPath || !newName) {
312
+ return { error: 'Both path and newName are required.', newPath: '', success: false };
313
+ }
314
+ // Prevent path traversal or using invalid characters/names
315
+ if (
316
+ newName.includes('/') ||
317
+ newName.includes('\\') ||
318
+ newName === '.' ||
319
+ newName === '..' ||
320
+ /["*/:<>?\\|]/.test(newName) // Check for typical invalid filename characters
321
+ ) {
322
+ return {
323
+ error:
324
+ 'Invalid new name. It cannot contain path separators (/, \\), be "." or "..", or include characters like < > : " / \\ | ? *.',
325
+ newPath: '',
326
+ success: false,
327
+ };
328
+ }
329
+
330
+ let newPath: string;
331
+ try {
332
+ const dir = path.dirname(currentPath);
333
+ newPath = path.join(dir, newName);
334
+
335
+ // Check if paths are identical after calculation
336
+ if (path.normalize(currentPath) === path.normalize(newPath)) {
337
+ console.log(
338
+ `Skipping rename: oldPath and calculated newPath are identical: ${currentPath}`,
339
+ );
340
+ // Consider success as no change is needed, but maybe inform the user?
341
+ // Return success for now.
342
+ return { newPath, success: true };
343
+ }
344
+ } catch (error) {
345
+ console.error(`Error calculating new path for rename ${currentPath} to ${newName}:`, error);
346
+ return {
347
+ error: `Internal error calculating the new path: ${(error as Error).message}`,
348
+ newPath: '',
349
+ success: false,
350
+ };
351
+ }
352
+
353
+ // Perform the rename operation using fs.promises.rename directly
354
+ try {
355
+ await renamePromise(currentPath, newPath);
356
+ console.log(`Successfully renamed ${currentPath} to ${newPath}`);
357
+ // Optionally return the newPath if frontend needs it
358
+ // return { success: true, newPath: newPath };
359
+ return { newPath, success: true };
360
+ } catch (error) {
361
+ console.error(`Error renaming ${currentPath} to ${newPath}:`, error);
362
+ let errorMessage = (error as Error).message;
363
+ // Provide more specific error messages based on common codes
364
+ if ((error as any).code === 'ENOENT') {
365
+ errorMessage = `File or directory not found at the original path: ${currentPath}.`;
366
+ } else if ((error as any).code === 'EPERM' || (error as any).code === 'EACCES') {
367
+ errorMessage = `Permission denied to rename the item at ${currentPath}. Check file/folder permissions.`;
368
+ } else if ((error as any).code === 'EBUSY') {
369
+ errorMessage = `The file or directory at ${currentPath} or ${newPath} is busy or locked by another process.`;
370
+ } else if ((error as any).code === 'EISDIR' || (error as any).code === 'ENOTDIR') {
371
+ errorMessage = `Cannot rename - conflict between file and directory. Source: ${currentPath}, Target: ${newPath}.`;
372
+ } else if ((error as any).code === 'EEXIST') {
373
+ // Target already exists
374
+ errorMessage = `Cannot rename: an item with the name '${newName}' already exists at this location.`;
375
+ }
376
+ // Add more specific checks as needed
377
+ return { error: errorMessage, newPath: '', success: false };
378
+ }
379
+ }
380
+ }
@@ -0,0 +1,29 @@
1
+ import { ControllerModule, ipcClientEvent } from './index';
2
+
3
+ export default class MenuController extends ControllerModule {
4
+ /**
5
+ * 刷新菜单
6
+ */
7
+ @ipcClientEvent('refreshAppMenu')
8
+ refreshAppMenu() {
9
+ // 注意:可能需要根据具体情况决定是否允许渲染进程刷新所有菜单
10
+ return this.app.menuManager.refreshMenus();
11
+ }
12
+
13
+ /**
14
+ * 显示上下文菜单
15
+ */
16
+ @ipcClientEvent('showContextMenu')
17
+ showContextMenu(type: string, data?: any) {
18
+ return this.app.menuManager.showContextMenu(type, data);
19
+ }
20
+
21
+ /**
22
+ * 设置开发菜单可见性
23
+ */
24
+ @ipcClientEvent('setDevMenuVisibility')
25
+ setDevMenuVisibility(visible: boolean) {
26
+ // 调用 MenuManager 的方法来重建应用菜单
27
+ return this.app.menuManager.rebuildAppMenu({ showDevItems: visible });
28
+ }
29
+ }