@lobehub/chat 1.82.9 → 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 (196) 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 +50 -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 +18 -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/config/aiModels/wenxin.ts +95 -8
  160. package/src/database/core/db-adaptor.ts +4 -1
  161. package/src/database/core/electron.ts +317 -0
  162. package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx → features/ElectronTitlebar/Connection/ConnectionMode.tsx} +24 -21
  163. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Option.tsx +3 -5
  164. package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx → features/ElectronTitlebar/Connection/RemoteStatus.tsx} +10 -7
  165. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/index.tsx +4 -4
  166. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateModal.tsx +2 -1
  167. package/src/libs/trpc/client/async.ts +6 -0
  168. package/src/libs/trpc/client/edge.ts +6 -0
  169. package/src/libs/trpc/client/helpers/desktopRemoteRPCFetch.ts +72 -0
  170. package/src/libs/trpc/client/index.ts +1 -0
  171. package/src/libs/trpc/client/lambda.ts +10 -1
  172. package/src/libs/trpc/client/tools.ts +6 -0
  173. package/src/server/globalConfig/index.ts +0 -3
  174. package/src/server/modules/ElectronIPCClient/index.ts +3 -1
  175. package/src/server/routers/desktop/index.ts +2 -0
  176. package/src/server/routers/desktop/mcp.ts +47 -0
  177. package/src/server/routers/lambda/user.ts +38 -23
  178. package/src/server/routers/tools/mcp.ts +0 -6
  179. package/src/services/electron/remoteServer.ts +4 -4
  180. package/src/services/mcp.ts +17 -7
  181. package/src/services/upload.ts +9 -0
  182. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +11 -2
  183. package/src/store/chat/slices/builtinTool/actions/localFile.ts +110 -53
  184. package/src/store/electron/actions/sync.ts +20 -19
  185. package/src/store/electron/initialState.ts +3 -3
  186. package/src/store/electron/selectors/sync.ts +6 -3
  187. package/src/store/electron/store.ts +2 -0
  188. package/src/store/file/slices/upload/action.ts +11 -3
  189. package/src/store/tool/selectors/tool.ts +10 -1
  190. package/src/utils/fetch/headers.ts +27 -0
  191. package/src/utils/fetch/index.ts +2 -0
  192. package/src/utils/fetch/request.ts +28 -0
  193. package/packages/electron-client-ipc/src/types/remoteServer.ts +0 -8
  194. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Waiting.tsx +0 -0
  195. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateNotification.tsx +0 -0
  196. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/index.tsx +0 -0
@@ -0,0 +1,577 @@
1
+ import { exec, spawn } from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import readline from 'node:readline';
5
+ import { promisify } from 'node:util';
6
+
7
+ import { FileResult, SearchOptions } from '@/types/fileSearch';
8
+ import { createLogger } from '@/utils/logger';
9
+
10
+ import { FileSearchImpl } from '../type';
11
+
12
+ const execPromise = promisify(exec);
13
+ const statPromise = promisify(fs.stat);
14
+
15
+ // Create logger
16
+ const logger = createLogger('module:FileSearch:macOS');
17
+
18
+ export class MacOSSearchServiceImpl extends FileSearchImpl {
19
+ /**
20
+ * Perform file search
21
+ * @param options Search options
22
+ * @returns Promise of search result list
23
+ */
24
+ async search(options: SearchOptions): Promise<FileResult[]> {
25
+ // Build the command first, regardless of execution method
26
+ const command = this.buildSearchCommand(options);
27
+ logger.debug(`Executing command: ${command}`);
28
+
29
+ // Use spawn for both live and non-live updates to handle large outputs
30
+ return new Promise((resolve, reject) => {
31
+ const [cmd, ...args] = command.split(' ');
32
+ const childProcess = spawn(cmd, args);
33
+
34
+ let results: string[] = []; // Store raw file paths
35
+ let stderrData = '';
36
+
37
+ // Create a readline interface to process stdout line by line
38
+ const rl = readline.createInterface({
39
+ crlfDelay: Infinity,
40
+ input: childProcess.stdout, // Handle different line endings
41
+ });
42
+
43
+ rl.on('line', (line) => {
44
+ const trimmedLine = line.trim();
45
+ if (trimmedLine) {
46
+ results.push(trimmedLine);
47
+
48
+ // If we have a limit and we've reached it (in non-live mode), stop processing
49
+ if (!options.liveUpdate && options.limit && results.length >= options.limit) {
50
+ logger.debug(`Reached limit (${options.limit}), closing readline and killing process.`);
51
+ rl.close(); // Stop reading lines
52
+ childProcess.kill(); // Terminate the mdfind process
53
+ }
54
+ }
55
+ });
56
+
57
+ childProcess.stderr.on('data', (data) => {
58
+ const errorMsg = data.toString();
59
+ stderrData += errorMsg;
60
+ logger.warn(`Search stderr: ${errorMsg}`);
61
+ });
62
+
63
+ childProcess.on('error', (error) => {
64
+ logger.error(`Search process error: ${error.message}`, error);
65
+ reject(new Error(`Search process failed to start: ${error.message}`));
66
+ });
67
+
68
+ childProcess.on('close', async (code) => {
69
+ logger.debug(`Search process exited with code ${code}`);
70
+
71
+ // Even if the process was killed due to limit, code might be null or non-zero.
72
+ // Process the results collected so far.
73
+ if (code !== 0 && stderrData && results.length === 0) {
74
+ // If exited with error code and we have stderr message and no results, reject.
75
+ // Filter specific ignorable errors if necessary
76
+ if (!stderrData.includes('Index is unavailable') && !stderrData.includes('kMD')) {
77
+ // Avoid rejecting for common Spotlight query syntax errors or index issues if some results might still be valid
78
+ reject(new Error(`Search process exited with code ${code}: ${stderrData}`));
79
+ return;
80
+ } else {
81
+ logger.warn(
82
+ `Search process exited with code ${code} but contained potentially ignorable errors: ${stderrData}`,
83
+ );
84
+ }
85
+ }
86
+
87
+ try {
88
+ // Process the collected file paths
89
+ // Ensure limit is applied again here in case killing the process didn't stop exactly at the limit
90
+ const limitedResults =
91
+ options.limit && results.length > options.limit
92
+ ? results.slice(0, options.limit)
93
+ : results;
94
+
95
+ const processedResults = await this.processSearchResultsFromPaths(
96
+ limitedResults,
97
+ options,
98
+ );
99
+ resolve(processedResults);
100
+ } catch (processingError) {
101
+ logger.error('Error processing search results:', processingError);
102
+ reject(new Error(`Failed to process search results: ${processingError.message}`));
103
+ }
104
+ });
105
+
106
+ // Handle live update specific logic (if needed in the future, e.g., sending initial batch)
107
+ if (options.liveUpdate) {
108
+ // For live update, we might want to resolve an initial batch
109
+ // or rely purely on events sent elsewhere.
110
+ // Current implementation resolves when the stream closes.
111
+ // We could add a timeout to resolve with initial results if needed.
112
+ logger.debug('Live update enabled, results will be processed on close.');
113
+ // Note: The previous `executeLiveSearch` logic is now integrated here.
114
+ // If specific live update event emission is needed, it would be added here,
115
+ // potentially calling a callback provided in options.
116
+ }
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Check search service status
122
+ * @returns Promise indicating if Spotlight service is available
123
+ */
124
+ async checkSearchServiceStatus(): Promise<boolean> {
125
+ return this.checkSpotlightStatus();
126
+ }
127
+
128
+ /**
129
+ * Update search index
130
+ * @param path Optional specified path
131
+ * @returns Promise indicating operation success
132
+ */
133
+ async updateSearchIndex(path?: string): Promise<boolean> {
134
+ return this.updateSpotlightIndex(path);
135
+ }
136
+
137
+ /**
138
+ * Build mdfind command string
139
+ * @param options Search options
140
+ * @returns Complete command string
141
+ */
142
+ private buildSearchCommand(options: SearchOptions): string {
143
+ // Basic command
144
+ let command = 'mdfind';
145
+
146
+ // Add options
147
+ const mdFindOptions: string[] = [];
148
+
149
+ // macOS mdfind doesn't support -limit parameter, we'll limit results in post-processing
150
+
151
+ // Search in specific directory
152
+ if (options.onlyIn) {
153
+ mdFindOptions.push(`-onlyin "${options.onlyIn}"`);
154
+ }
155
+
156
+ // Live update
157
+ if (options.liveUpdate) {
158
+ mdFindOptions.push('-live');
159
+ }
160
+
161
+ // Detailed metadata
162
+ if (options.detailed) {
163
+ mdFindOptions.push(
164
+ '-attr kMDItemDisplayName kMDItemContentType kMDItemKind kMDItemFSSize kMDItemFSCreationDate kMDItemFSContentChangeDate',
165
+ );
166
+ }
167
+
168
+ // Build query expression
169
+ let queryExpression = '';
170
+
171
+ // Basic query
172
+ if (options.keywords) {
173
+ // If the query string doesn't use Spotlight query syntax (doesn't contain kMDItem properties),
174
+ // treat it as plain text search
175
+ if (!options.keywords.includes('kMDItem')) {
176
+ queryExpression = `"${options.keywords.replaceAll('"', '\\"')}"`;
177
+ } else {
178
+ queryExpression = options.keywords;
179
+ }
180
+ }
181
+
182
+ // File content search
183
+ if (options.contentContains) {
184
+ if (queryExpression) {
185
+ queryExpression = `${queryExpression} && kMDItemTextContent == "*${options.contentContains}*"cd`;
186
+ } else {
187
+ queryExpression = `kMDItemTextContent == "*${options.contentContains}*"cd`;
188
+ }
189
+ }
190
+
191
+ // File type filtering
192
+ if (options.fileTypes && options.fileTypes.length > 0) {
193
+ const typeConditions = options.fileTypes
194
+ .map((type) => `kMDItemContentType == "${type}"`)
195
+ .join(' || ');
196
+ if (queryExpression) {
197
+ queryExpression = `${queryExpression} && (${typeConditions})`;
198
+ } else {
199
+ queryExpression = `(${typeConditions})`;
200
+ }
201
+ }
202
+
203
+ // Date filtering - Modified date
204
+ if (options.modifiedAfter || options.modifiedBefore) {
205
+ let dateCondition = '';
206
+
207
+ if (options.modifiedAfter) {
208
+ const dateString = options.modifiedAfter.toISOString().split('T')[0];
209
+ dateCondition += `kMDItemFSContentChangeDate >= $time.iso(${dateString})`;
210
+ }
211
+
212
+ if (options.modifiedBefore) {
213
+ if (dateCondition) dateCondition += ' && ';
214
+ const dateString = options.modifiedBefore.toISOString().split('T')[0];
215
+ dateCondition += `kMDItemFSContentChangeDate <= $time.iso(${dateString})`;
216
+ }
217
+
218
+ if (queryExpression) {
219
+ queryExpression = `${queryExpression} && (${dateCondition})`;
220
+ } else {
221
+ queryExpression = dateCondition;
222
+ }
223
+ }
224
+
225
+ // Date filtering - Creation date
226
+ if (options.createdAfter || options.createdBefore) {
227
+ let dateCondition = '';
228
+
229
+ if (options.createdAfter) {
230
+ const dateString = options.createdAfter.toISOString().split('T')[0];
231
+ dateCondition += `kMDItemFSCreationDate >= $time.iso(${dateString})`;
232
+ }
233
+
234
+ if (options.createdBefore) {
235
+ if (dateCondition) dateCondition += ' && ';
236
+ const dateString = options.createdBefore.toISOString().split('T')[0];
237
+ dateCondition += `kMDItemFSCreationDate <= $time.iso(${dateString})`;
238
+ }
239
+
240
+ if (queryExpression) {
241
+ queryExpression = `${queryExpression} && (${dateCondition})`;
242
+ } else {
243
+ queryExpression = dateCondition;
244
+ }
245
+ }
246
+
247
+ // Combine complete command
248
+ if (mdFindOptions.length > 0) {
249
+ command += ' ' + mdFindOptions.join(' ');
250
+ }
251
+
252
+ // Finally add query expression
253
+ command += ` ${queryExpression}`;
254
+
255
+ return command;
256
+ }
257
+
258
+ /**
259
+ * Execute live search, returns initial results and sets callback
260
+ * @param command mdfind command
261
+ * @param options Search options
262
+ * @returns Promise of initial search results
263
+ * @deprecated This logic is now integrated into the main search method using spawn.
264
+ */
265
+ // private executeLiveSearch(command: string, options: SearchOptions): Promise<FileResult[]> { ... }
266
+ // Remove or comment out the old executeLiveSearch method
267
+
268
+ /**
269
+ * Process search results from a list of file paths
270
+ * @param filePaths Array of file path strings
271
+ * @param options Search options
272
+ * @returns Formatted file result list
273
+ */
274
+ private async processSearchResultsFromPaths(
275
+ filePaths: string[],
276
+ options: SearchOptions,
277
+ ): Promise<FileResult[]> {
278
+ // Create a result object for each file path
279
+ const resultPromises = filePaths.map(async (filePath) => {
280
+ try {
281
+ // Get file information
282
+ const stats = await statPromise(filePath);
283
+
284
+ // Create basic result object
285
+ const result: FileResult = {
286
+ createdTime: stats.birthtime,
287
+ isDirectory: stats.isDirectory(),
288
+ lastAccessTime: stats.atime,
289
+ metadata: {},
290
+ modifiedTime: stats.mtime,
291
+ name: path.basename(filePath),
292
+ path: filePath,
293
+ size: stats.size,
294
+ type: path.extname(filePath).toLowerCase().replace('.', ''),
295
+ };
296
+
297
+ // If detailed information is needed, get additional metadata
298
+ if (options.detailed) {
299
+ result.metadata = await this.getDetailedMetadata(filePath);
300
+ }
301
+
302
+ // Determine content type
303
+ result.contentType = this.determineContentType(result.name, result.type);
304
+
305
+ return result;
306
+ } catch (error) {
307
+ logger.warn(`Error processing file stats for ${filePath}: ${error.message}`, error);
308
+ // Return partial information, even if unable to get complete file stats
309
+ return {
310
+ contentType: 'unknown',
311
+ createdTime: new Date(),
312
+ isDirectory: false,
313
+ lastAccessTime: new Date(),
314
+ modifiedTime: new Date(),
315
+ name: path.basename(filePath),
316
+ path: filePath,
317
+ size: 0,
318
+ type: path.extname(filePath).toLowerCase().replace('.', ''),
319
+ };
320
+ }
321
+ });
322
+
323
+ // Wait for all file information processing to complete
324
+ let results = await Promise.all(resultPromises);
325
+
326
+ // Sort results
327
+ if (options.sortBy) {
328
+ results = this.sortResults(results, options.sortBy, options.sortDirection);
329
+ }
330
+
331
+ // Apply limit here as mdfind doesn't support -limit parameter
332
+ if (options.limit && options.limit > 0 && results.length > options.limit) {
333
+ results = results.slice(0, options.limit);
334
+ }
335
+
336
+ return results;
337
+ }
338
+
339
+ /**
340
+ * Process search results
341
+ * @param stdout Command output (now unused directly, processing happens line by line)
342
+ * @param options Search options
343
+ * @returns Formatted file result list
344
+ * @deprecated Use processSearchResultsFromPaths instead.
345
+ */
346
+ // private async processSearchResults(stdout: string, options: SearchOptions): Promise<FileResult[]> { ... }
347
+ // Remove or comment out the old processSearchResults method
348
+
349
+ /**
350
+ * Get detailed metadata for a file
351
+ * @param filePath File path
352
+ * @returns Metadata object
353
+ */
354
+ private async getDetailedMetadata(filePath: string): Promise<Record<string, any>> {
355
+ try {
356
+ // Use mdls command to get all metadata
357
+ const { stdout } = await execPromise(`mdls "${filePath}"`);
358
+
359
+ // Parse mdls output
360
+ const metadata: Record<string, any> = {};
361
+ const lines = stdout.split('\n');
362
+
363
+ let currentKey = '';
364
+ let isMultilineValue = false;
365
+ let multilineValue: string[] = [];
366
+
367
+ for (const line of lines) {
368
+ if (isMultilineValue) {
369
+ if (line.includes(')')) {
370
+ // Multiline value ends
371
+ multilineValue.push(line.trim());
372
+ metadata[currentKey] = multilineValue.join(' ');
373
+ isMultilineValue = false;
374
+ multilineValue = [];
375
+ } else {
376
+ // Continue collecting multiline value
377
+ multilineValue.push(line.trim());
378
+ }
379
+ continue;
380
+ }
381
+
382
+ const match = line.match(/^(\w+)\s+=\s+(.*)$/);
383
+ if (match) {
384
+ currentKey = match[1];
385
+ const value = match[2].trim();
386
+
387
+ // Check for multiline value start
388
+ if (value.includes('(') && !value.includes(')')) {
389
+ isMultilineValue = true;
390
+ multilineValue = [value];
391
+ } else {
392
+ // Process single line value
393
+ metadata[currentKey] = this.parseMetadataValue(value);
394
+ }
395
+ }
396
+ }
397
+
398
+ return metadata;
399
+ } catch (error) {
400
+ logger.warn(`Error getting metadata for ${filePath}: ${error.message}`, error);
401
+ return {};
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Parse metadata value
407
+ * @param value Metadata raw value string
408
+ * @returns Parsed value
409
+ */
410
+ private parseMetadataValue(input: string): any {
411
+ let value = input;
412
+ // Remove quotes from mdls output
413
+ if (value.startsWith('"') && value.endsWith('"')) {
414
+ // eslint-disable-next-line unicorn/prefer-string-slice
415
+ value = value.substring(1, value.length - 1);
416
+ }
417
+
418
+ // Handle special values
419
+ if (value === '(null)') return null;
420
+ if (value === 'Yes' || value === 'true') return true;
421
+ if (value === 'No' || value === 'false') return false;
422
+
423
+ // Try to parse date (format like "2023-05-16 14:30:45 +0000")
424
+ const dateMatch = value.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [+-]\d{4})$/);
425
+ if (dateMatch) {
426
+ try {
427
+ return new Date(value);
428
+ } catch {
429
+ // If date parsing fails, return original string
430
+ return value;
431
+ }
432
+ }
433
+
434
+ // Try to parse number
435
+ if (/^-?\d+(\.\d+)?$/.test(value)) {
436
+ return Number(value);
437
+ }
438
+
439
+ // Default return string
440
+ return value;
441
+ }
442
+
443
+ /**
444
+ * Determine file content type
445
+ * @param fileName File name
446
+ * @param extension File extension
447
+ * @returns Content type description
448
+ */
449
+ private determineContentType(fileName: string, extension: string): string {
450
+ // Map common file extensions to content types
451
+ const typeMap: Record<string, string> = {
452
+ '7z': 'archive',
453
+ 'aac': 'audio',
454
+ // Others
455
+ 'app': 'application',
456
+ 'avi': 'video',
457
+ 'c': 'code',
458
+ 'cpp': 'code',
459
+ 'css': 'code',
460
+ 'dmg': 'disk-image',
461
+ 'doc': 'document',
462
+ 'docx': 'document',
463
+ 'gif': 'image',
464
+ 'gz': 'archive',
465
+ 'heic': 'image',
466
+ 'html': 'code',
467
+ 'iso': 'disk-image',
468
+ 'java': 'code',
469
+ 'jpeg': 'image',
470
+ // Images
471
+ 'jpg': 'image',
472
+ // Code
473
+ 'js': 'code',
474
+ 'json': 'code',
475
+ 'mkv': 'video',
476
+ 'mov': 'video',
477
+ // Audio
478
+ 'mp3': 'audio',
479
+ // Video
480
+ 'mp4': 'video',
481
+ 'ogg': 'audio',
482
+ // Documents
483
+ 'pdf': 'document',
484
+ 'png': 'image',
485
+ 'ppt': 'presentation',
486
+ 'pptx': 'presentation',
487
+ 'py': 'code',
488
+ 'rar': 'archive',
489
+ 'rtf': 'text',
490
+ 'svg': 'image',
491
+ 'swift': 'code',
492
+ 'tar': 'archive',
493
+ 'ts': 'code',
494
+ 'txt': 'text',
495
+ 'wav': 'audio',
496
+ 'webp': 'image',
497
+ 'xls': 'spreadsheet',
498
+ 'xlsx': 'spreadsheet',
499
+ // Archive files
500
+ 'zip': 'archive',
501
+ };
502
+
503
+ // Find matching content type
504
+ return typeMap[extension.toLowerCase()] || 'unknown';
505
+ }
506
+
507
+ /**
508
+ * Sort results
509
+ * @param results Result list
510
+ * @param sortBy Sort field
511
+ * @param direction Sort direction
512
+ * @returns Sorted result list
513
+ */
514
+ private sortResults(
515
+ results: FileResult[],
516
+ sortBy: 'name' | 'date' | 'size',
517
+ direction: 'asc' | 'desc' = 'asc',
518
+ ): FileResult[] {
519
+ const sortedResults = [...results];
520
+
521
+ sortedResults.sort((a, b) => {
522
+ let comparison = 0;
523
+
524
+ switch (sortBy) {
525
+ case 'name': {
526
+ comparison = a.name.localeCompare(b.name);
527
+ break;
528
+ }
529
+ case 'date': {
530
+ comparison = a.modifiedTime.getTime() - b.modifiedTime.getTime();
531
+ break;
532
+ }
533
+ case 'size': {
534
+ comparison = a.size - b.size;
535
+ break;
536
+ }
537
+ }
538
+
539
+ return direction === 'asc' ? comparison : -comparison;
540
+ });
541
+
542
+ return sortedResults;
543
+ }
544
+
545
+ /**
546
+ * Check Spotlight service status
547
+ * @returns Promise indicating if Spotlight is available
548
+ */
549
+ private async checkSpotlightStatus(): Promise<boolean> {
550
+ try {
551
+ // Try to run a simple mdfind command - macOS doesn't support -limit parameter
552
+ await execPromise('mdfind -name test -onlyin ~ -count');
553
+ return true;
554
+ } catch (error) {
555
+ logger.error(`Spotlight is not available: ${error.message}`, error);
556
+ return false;
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Update Spotlight index
562
+ * @param path Optional specified path
563
+ * @returns Promise indicating operation success
564
+ */
565
+ private async updateSpotlightIndex(path?: string): Promise<boolean> {
566
+ try {
567
+ // mdutil command is used to manage Spotlight index
568
+ const command = path ? `mdutil -E "${path}"` : 'mdutil -E /';
569
+
570
+ await execPromise(command);
571
+ return true;
572
+ } catch (error) {
573
+ logger.error(`Failed to update Spotlight index: ${error.message}`, error);
574
+ return false;
575
+ }
576
+ }
577
+ }
@@ -0,0 +1,23 @@
1
+ import { platform } from 'node:os';
2
+
3
+ import { MacOSSearchServiceImpl } from './impl/macOS';
4
+
5
+ export const createFileSearchModule = () => {
6
+ const currentPlatform = platform();
7
+
8
+ switch (currentPlatform) {
9
+ case 'darwin': {
10
+ return new MacOSSearchServiceImpl();
11
+ }
12
+ // case 'win32':
13
+ // return new WindowsSearchServiceImpl();
14
+ // case 'linux':
15
+ // return new LinuxSearchServiceImpl();
16
+ default: {
17
+ return new MacOSSearchServiceImpl();
18
+ // throw new Error(`Unsupported platform: ${currentPlatform}`);
19
+ }
20
+ }
21
+ };
22
+
23
+ export { FileSearchImpl } from './type';
@@ -0,0 +1,27 @@
1
+ import { FileResult, SearchOptions } from '@/types/fileSearch';
2
+
3
+ /**
4
+ * File Search Service Implementation Abstract Class
5
+ * Defines the interface that different platform file search implementations need to implement
6
+ */
7
+ export abstract class FileSearchImpl {
8
+ /**
9
+ * Perform file search
10
+ * @param options Search options
11
+ * @returns Promise of search result list
12
+ */
13
+ abstract search(options: SearchOptions): Promise<FileResult[]>;
14
+
15
+ /**
16
+ * Check search service status
17
+ * @returns Promise indicating if service is available
18
+ */
19
+ abstract checkSearchServiceStatus(): Promise<boolean>;
20
+
21
+ /**
22
+ * Update search index
23
+ * @param path Optional specified path
24
+ * @returns Promise indicating operation success
25
+ */
26
+ abstract updateSearchIndex(path?: string): Promise<boolean>;
27
+ }
@@ -0,0 +1,22 @@
1
+ import { isDev } from '@/const/env';
2
+
3
+ // 更新频道(stable, beta, alpha 等)
4
+ export const UPDATE_CHANNEL = process.env.UPDATE_CHANNEL;
5
+
6
+ export const updaterConfig = {
7
+ // 应用更新配置
8
+ app: {
9
+ // 是否自动检查更新
10
+ autoCheckUpdate: true,
11
+ // 是否自动下载更新
12
+ autoDownloadUpdate: true,
13
+ // 检查更新的时间间隔(毫秒)
14
+ checkUpdateInterval: 60 * 60 * 1000, // 1小时
15
+ },
16
+
17
+ // 是否启用应用更新
18
+ enableAppUpdate: !isDev,
19
+
20
+ // 是否启用渲染层热更新
21
+ enableRenderHotUpdate: !isDev,
22
+ };
@@ -0,0 +1,33 @@
1
+ import semver from 'semver';
2
+
3
+ /**
4
+ * 判断是否需要应用更新而非仅渲染层更新
5
+ * @param currentVersion 当前版本
6
+ * @param nextVersion 新版本
7
+ * @returns 是否需要应用更新
8
+ */
9
+ export const shouldUpdateApp = (currentVersion: string, nextVersion: string): boolean => {
10
+ // 如果版本号包含 .app 后缀,强制进行应用更新
11
+ if (nextVersion.includes('.app')) {
12
+ return true;
13
+ }
14
+
15
+ try {
16
+ // 解析版本号
17
+ const current = semver.parse(currentVersion);
18
+ const next = semver.parse(nextVersion);
19
+
20
+ if (!current || !next) return true;
21
+
22
+ // 主版本号或次版本号变更时,需要进行应用更新
23
+ if (current.major !== next.major || current.minor !== next.minor) {
24
+ return true;
25
+ }
26
+
27
+ // 仅修订版本号变更,优先进行渲染层热更新
28
+ return false;
29
+ } catch {
30
+ // 解析失败时,默认进行应用更新
31
+ return true;
32
+ }
33
+ };