@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.
- package/.env.desktop +1 -2
- package/.github/workflows/{release-desktop.yml → desktop-pr-build.yml} +59 -137
- package/.github/workflows/release-desktop-beta.yml +196 -0
- package/CHANGELOG.md +50 -0
- package/apps/desktop/.i18nrc.js +31 -0
- package/apps/desktop/Development.md +47 -0
- package/apps/desktop/README.md +6 -0
- package/apps/desktop/build/Icon-beta.icns +0 -0
- package/apps/desktop/build/Icon-nightly.icns +0 -0
- package/apps/desktop/build/Icon.icns +0 -0
- package/apps/desktop/build/entitlements.mac.plist +12 -0
- package/apps/desktop/build/favicon.ico +0 -0
- package/apps/desktop/build/icon-beta.png +0 -0
- package/apps/desktop/build/icon-dev.png +0 -0
- package/apps/desktop/build/icon-nightly.ico +0 -0
- package/apps/desktop/build/icon-nightly.png +0 -0
- package/apps/desktop/build/icon.ico +0 -0
- package/apps/desktop/build/icon.png +0 -0
- package/apps/desktop/dev-app-update.yml +6 -0
- package/apps/desktop/electron-builder.js +92 -0
- package/apps/desktop/electron.vite.config.ts +40 -0
- package/apps/desktop/package.json +72 -0
- package/apps/desktop/pnpm-workspace.yaml +5 -0
- package/apps/desktop/resources/error.html +136 -0
- package/apps/desktop/resources/locales/ar/common.json +32 -0
- package/apps/desktop/resources/locales/ar/dialog.json +31 -0
- package/apps/desktop/resources/locales/ar/menu.json +70 -0
- package/apps/desktop/resources/locales/bg-BG/common.json +32 -0
- package/apps/desktop/resources/locales/bg-BG/dialog.json +31 -0
- package/apps/desktop/resources/locales/bg-BG/menu.json +70 -0
- package/apps/desktop/resources/locales/de-DE/common.json +32 -0
- package/apps/desktop/resources/locales/de-DE/dialog.json +31 -0
- package/apps/desktop/resources/locales/de-DE/menu.json +70 -0
- package/apps/desktop/resources/locales/en-US/common.json +32 -0
- package/apps/desktop/resources/locales/en-US/dialog.json +31 -0
- package/apps/desktop/resources/locales/en-US/menu.json +70 -0
- package/apps/desktop/resources/locales/es-ES/common.json +32 -0
- package/apps/desktop/resources/locales/es-ES/dialog.json +31 -0
- package/apps/desktop/resources/locales/es-ES/menu.json +70 -0
- package/apps/desktop/resources/locales/fa-IR/common.json +32 -0
- package/apps/desktop/resources/locales/fa-IR/dialog.json +31 -0
- package/apps/desktop/resources/locales/fa-IR/menu.json +70 -0
- package/apps/desktop/resources/locales/fr-FR/common.json +32 -0
- package/apps/desktop/resources/locales/fr-FR/dialog.json +31 -0
- package/apps/desktop/resources/locales/fr-FR/menu.json +70 -0
- package/apps/desktop/resources/locales/it-IT/common.json +32 -0
- package/apps/desktop/resources/locales/it-IT/dialog.json +31 -0
- package/apps/desktop/resources/locales/it-IT/menu.json +70 -0
- package/apps/desktop/resources/locales/ja-JP/common.json +32 -0
- package/apps/desktop/resources/locales/ja-JP/dialog.json +31 -0
- package/apps/desktop/resources/locales/ja-JP/menu.json +70 -0
- package/apps/desktop/resources/locales/ko-KR/common.json +32 -0
- package/apps/desktop/resources/locales/ko-KR/dialog.json +31 -0
- package/apps/desktop/resources/locales/ko-KR/menu.json +70 -0
- package/apps/desktop/resources/locales/nl-NL/common.json +32 -0
- package/apps/desktop/resources/locales/nl-NL/dialog.json +31 -0
- package/apps/desktop/resources/locales/nl-NL/menu.json +70 -0
- package/apps/desktop/resources/locales/pl-PL/common.json +32 -0
- package/apps/desktop/resources/locales/pl-PL/dialog.json +31 -0
- package/apps/desktop/resources/locales/pl-PL/menu.json +70 -0
- package/apps/desktop/resources/locales/pt-BR/common.json +32 -0
- package/apps/desktop/resources/locales/pt-BR/dialog.json +31 -0
- package/apps/desktop/resources/locales/pt-BR/menu.json +70 -0
- package/apps/desktop/resources/locales/ru-RU/common.json +32 -0
- package/apps/desktop/resources/locales/ru-RU/dialog.json +31 -0
- package/apps/desktop/resources/locales/ru-RU/menu.json +70 -0
- package/apps/desktop/resources/locales/tr-TR/common.json +32 -0
- package/apps/desktop/resources/locales/tr-TR/dialog.json +31 -0
- package/apps/desktop/resources/locales/tr-TR/menu.json +70 -0
- package/apps/desktop/resources/locales/vi-VN/common.json +32 -0
- package/apps/desktop/resources/locales/vi-VN/dialog.json +31 -0
- package/apps/desktop/resources/locales/vi-VN/menu.json +70 -0
- package/apps/desktop/resources/locales/zh-CN/common.json +32 -0
- package/apps/desktop/resources/locales/zh-CN/dialog.json +31 -0
- package/apps/desktop/resources/locales/zh-CN/menu.json +70 -0
- package/apps/desktop/resources/locales/zh-TW/common.json +32 -0
- package/apps/desktop/resources/locales/zh-TW/dialog.json +31 -0
- package/apps/desktop/resources/locales/zh-TW/menu.json +70 -0
- package/apps/desktop/resources/splash.html +88 -0
- package/apps/desktop/scripts/i18nWorkflow/const.ts +18 -0
- package/apps/desktop/scripts/i18nWorkflow/genDefaultLocale.ts +35 -0
- package/apps/desktop/scripts/i18nWorkflow/genDiff.ts +57 -0
- package/apps/desktop/scripts/i18nWorkflow/index.ts +35 -0
- package/apps/desktop/scripts/i18nWorkflow/utils.ts +54 -0
- package/apps/desktop/scripts/pglite-server.ts +14 -0
- package/apps/desktop/src/common/routes.ts +78 -0
- package/apps/desktop/src/main/appBrowsers.ts +47 -0
- package/apps/desktop/src/main/const/dir.ts +29 -0
- package/apps/desktop/src/main/const/env.ts +3 -0
- package/apps/desktop/src/main/const/store.ts +22 -0
- package/apps/desktop/src/main/controllers/AuthCtr.ts +390 -0
- package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +95 -0
- package/apps/desktop/src/main/controllers/DevtoolsCtr.ts +9 -0
- package/apps/desktop/src/main/controllers/LocalFileCtr.ts +380 -0
- package/apps/desktop/src/main/controllers/MenuCtr.ts +29 -0
- package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +335 -0
- package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +321 -0
- package/apps/desktop/src/main/controllers/ShortcutCtr.ts +19 -0
- package/apps/desktop/src/main/controllers/SystemCtr.ts +93 -0
- package/apps/desktop/src/main/controllers/UpdaterCtr.ts +43 -0
- package/apps/desktop/src/main/controllers/UploadFileCtr.ts +34 -0
- package/apps/desktop/src/main/controllers/_template.ts +9 -0
- package/apps/desktop/src/main/controllers/index.ts +58 -0
- package/apps/desktop/src/main/core/App.ts +370 -0
- package/apps/desktop/src/main/core/Browser.ts +345 -0
- package/apps/desktop/src/main/core/BrowserManager.ts +154 -0
- package/apps/desktop/src/main/core/I18nManager.ts +185 -0
- package/apps/desktop/src/main/core/IoCContainer.ts +12 -0
- package/apps/desktop/src/main/core/MenuManager.ts +64 -0
- package/apps/desktop/src/main/core/ShortcutManager.ts +173 -0
- package/apps/desktop/src/main/core/StoreManager.ts +89 -0
- package/apps/desktop/src/main/core/UpdaterManager.ts +321 -0
- package/apps/desktop/src/main/index.ts +5 -0
- package/apps/desktop/src/main/locales/default/common.ts +34 -0
- package/apps/desktop/src/main/locales/default/dialog.ts +33 -0
- package/apps/desktop/src/main/locales/default/index.ts +11 -0
- package/apps/desktop/src/main/locales/default/menu.ts +72 -0
- package/apps/desktop/src/main/locales/resources.ts +35 -0
- package/apps/desktop/src/main/menus/impls/BaseMenuPlatform.ts +10 -0
- package/apps/desktop/src/main/menus/impls/linux.ts +243 -0
- package/apps/desktop/src/main/menus/impls/macOS.ts +360 -0
- package/apps/desktop/src/main/menus/impls/windows.ts +226 -0
- package/apps/desktop/src/main/menus/index.ts +34 -0
- package/apps/desktop/src/main/menus/types.ts +28 -0
- package/apps/desktop/src/main/modules/fileSearch/impl/macOS.ts +577 -0
- package/apps/desktop/src/main/modules/fileSearch/index.ts +23 -0
- package/apps/desktop/src/main/modules/fileSearch/type.ts +27 -0
- package/apps/desktop/src/main/modules/updater/configs.ts +22 -0
- package/apps/desktop/src/main/modules/updater/utils.ts +33 -0
- package/apps/desktop/src/main/services/fileSearchSrv.ts +35 -0
- package/apps/desktop/src/main/services/fileSrv.ts +255 -0
- package/apps/desktop/src/main/services/index.ts +9 -0
- package/apps/desktop/src/main/shortcuts/config.ts +18 -0
- package/apps/desktop/src/main/shortcuts/index.ts +1 -0
- package/apps/desktop/src/main/types/fileSearch.ts +51 -0
- package/apps/desktop/src/main/types/store.ts +14 -0
- package/apps/desktop/src/main/utils/file-system.ts +15 -0
- package/apps/desktop/src/main/utils/logger.ts +44 -0
- package/apps/desktop/src/main/utils/next-electron-rsc.ts +383 -0
- package/apps/desktop/src/preload/electronApi.ts +18 -0
- package/apps/desktop/src/preload/index.ts +14 -0
- package/apps/desktop/src/preload/invoke.ts +10 -0
- package/apps/desktop/src/preload/routeInterceptor.ts +162 -0
- package/apps/desktop/tsconfig.json +21 -0
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/packages/electron-client-ipc/src/events/remoteServer.ts +11 -4
- package/packages/electron-client-ipc/src/types/dataSync.ts +15 -0
- package/packages/electron-client-ipc/src/types/index.ts +2 -1
- package/packages/electron-client-ipc/src/types/proxyTRPCRequest.ts +21 -0
- package/packages/electron-server-ipc/src/const.ts +3 -3
- package/packages/electron-server-ipc/src/ipcClient.test.ts +7 -6
- package/packages/electron-server-ipc/src/ipcClient.ts +17 -8
- package/packages/electron-server-ipc/src/ipcServer.ts +7 -3
- package/scripts/electronWorkflow/setDesktopVersion.ts +60 -43
- package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +1 -1
- package/src/components/Analytics/Desktop.tsx +19 -0
- package/src/components/Analytics/index.tsx +3 -0
- package/src/config/aiModels/wenxin.ts +95 -8
- package/src/database/core/db-adaptor.ts +4 -1
- package/src/database/core/electron.ts +317 -0
- package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx → features/ElectronTitlebar/Connection/ConnectionMode.tsx} +24 -21
- package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Option.tsx +3 -5
- package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx → features/ElectronTitlebar/Connection/RemoteStatus.tsx} +10 -7
- package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/index.tsx +4 -4
- package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateModal.tsx +2 -1
- package/src/libs/trpc/client/async.ts +6 -0
- package/src/libs/trpc/client/edge.ts +6 -0
- package/src/libs/trpc/client/helpers/desktopRemoteRPCFetch.ts +72 -0
- package/src/libs/trpc/client/index.ts +1 -0
- package/src/libs/trpc/client/lambda.ts +10 -1
- package/src/libs/trpc/client/tools.ts +6 -0
- package/src/server/globalConfig/index.ts +0 -3
- package/src/server/modules/ElectronIPCClient/index.ts +3 -1
- package/src/server/routers/desktop/index.ts +2 -0
- package/src/server/routers/desktop/mcp.ts +47 -0
- package/src/server/routers/lambda/user.ts +38 -23
- package/src/server/routers/tools/mcp.ts +0 -6
- package/src/services/electron/remoteServer.ts +4 -4
- package/src/services/mcp.ts +17 -7
- package/src/services/upload.ts +9 -0
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +11 -2
- package/src/store/chat/slices/builtinTool/actions/localFile.ts +110 -53
- package/src/store/electron/actions/sync.ts +20 -19
- package/src/store/electron/initialState.ts +3 -3
- package/src/store/electron/selectors/sync.ts +6 -3
- package/src/store/electron/store.ts +2 -0
- package/src/store/file/slices/upload/action.ts +11 -3
- package/src/store/tool/selectors/tool.ts +10 -1
- package/src/utils/fetch/headers.ts +27 -0
- package/src/utils/fetch/index.ts +2 -0
- package/src/utils/fetch/request.ts +28 -0
- package/packages/electron-client-ipc/src/types/remoteServer.ts +0 -8
- /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Waiting.tsx +0 -0
- /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateNotification.tsx +0 -0
- /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
|
+
};
|