@lobehub/lobehub 2.0.0-next.251 → 2.0.0-next.253
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/CHANGELOG.md +58 -0
- package/apps/desktop/build/entitlements.mac.plist +9 -0
- package/apps/desktop/resources/locales/zh-CN/dialog.json +5 -1
- package/apps/desktop/resources/locales/zh-CN/menu.json +7 -0
- package/apps/desktop/src/main/controllers/SystemCtr.ts +186 -94
- package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +200 -31
- package/apps/desktop/src/main/core/browser/Browser.ts +9 -0
- package/apps/desktop/src/main/locales/default/dialog.ts +7 -2
- package/apps/desktop/src/main/locales/default/menu.ts +7 -0
- package/apps/desktop/src/main/menus/impls/macOS.ts +44 -1
- package/apps/desktop/src/main/utils/fullDiskAccess.ts +121 -0
- package/changelog/v1.json +14 -0
- package/package.json +1 -1
- package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +0 -2
- package/packages/database/migrations/meta/_journal.json +1 -1
- package/packages/database/src/models/__tests__/topics/topic.create.test.ts +37 -8
- package/packages/database/src/models/topic.ts +71 -4
- package/packages/database/src/schemas/agentCronJob.ts +1 -2
- package/packages/electron-client-ipc/src/events/system.ts +1 -0
- package/packages/memory-user-memory/src/extractors/context.ts +1 -4
- package/packages/memory-user-memory/src/extractors/experience.ts +2 -8
- package/packages/memory-user-memory/src/extractors/preference.ts +2 -8
- package/packages/memory-user-memory/src/prompts/gatekeeper.ts +123 -123
- package/packages/memory-user-memory/src/prompts/layers/context.ts +152 -152
- package/packages/memory-user-memory/src/prompts/layers/experience.ts +159 -159
- package/packages/memory-user-memory/src/prompts/layers/identity.ts +213 -213
- package/packages/memory-user-memory/src/prompts/layers/preference.ts +160 -160
- package/packages/memory-user-memory/src/services/extractExecutor.ts +33 -30
- package/packages/memory-user-memory/src/types.ts +10 -8
- package/packages/types/src/topic/topic.ts +9 -0
- package/src/app/[variants]/(desktop)/desktop-onboarding/features/PermissionsStep.tsx +16 -30
- package/src/app/[variants]/(desktop)/desktop-onboarding/index.tsx +19 -9
- package/src/app/[variants]/(desktop)/desktop-onboarding/storage.ts +49 -0
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Body.tsx +4 -1
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/CronTopicList/CronTopicGroup.tsx +74 -0
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/CronTopicList/CronTopicItem.tsx +40 -0
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/CronTopicList/index.tsx +140 -0
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/index.tsx +1 -1
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/TopicListContent/index.tsx +1 -1
- package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/index.tsx +1 -1
- package/src/app/[variants]/(main)/chat/cron/[cronId]/index.tsx +664 -0
- package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/CronJobCards.tsx +160 -0
- package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/CronJobForm.tsx +202 -0
- package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/CronJobList.tsx +137 -0
- package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/hooks/useAgentCronJobs.ts +138 -0
- package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/index.tsx +130 -0
- package/src/app/[variants]/(main)/chat/profile/features/ProfileEditor/index.tsx +33 -3
- package/src/app/[variants]/router/desktopRouter.config.tsx +7 -0
- package/src/features/ChatInput/ActionBar/Params/Controls.tsx +7 -6
- package/src/hooks/useFetchCronTopics.ts +29 -0
- package/src/hooks/useFetchCronTopicsWithJobInfo.ts +56 -0
- package/src/hooks/useFetchTopics.ts +4 -1
- package/src/locales/default/setting.ts +44 -1
- package/src/server/routers/lambda/agentCronJob.ts +367 -0
- package/src/server/routers/lambda/image/index.test.ts +2 -2
- package/src/server/routers/lambda/index.ts +2 -0
- package/src/server/routers/lambda/topic.ts +15 -3
- package/src/server/services/aiAgent/index.ts +18 -1
- package/src/server/services/memory/userMemory/extract.ts +14 -6
- package/src/services/agentCronJob.ts +95 -0
- package/src/services/topic/index.ts +1 -0
- package/src/store/chat/slices/topic/action.ts +53 -2
- package/src/store/chat/slices/topic/initialState.ts +1 -0
- package/src/store/chat/slices/topic/selectors.ts +14 -6
- package/src/tools/placeholders.ts +1 -4
- package/apps/desktop/src/main/controllers/scripts/full-disk-access.applescript +0 -85
|
@@ -7,12 +7,13 @@ import { IpcHandler } from '@/utils/ipc/base';
|
|
|
7
7
|
|
|
8
8
|
import SystemController from '../SystemCtr';
|
|
9
9
|
|
|
10
|
-
const { ipcHandlers, ipcMainHandleMock } = vi.hoisted(() => {
|
|
10
|
+
const { ipcHandlers, ipcMainHandleMock, readdirSyncMock } = vi.hoisted(() => {
|
|
11
11
|
const handlers = new Map<string, (event: any, ...args: any[]) => any>();
|
|
12
12
|
const handle = vi.fn((channel: string, handler: any) => {
|
|
13
13
|
handlers.set(channel, handler);
|
|
14
14
|
});
|
|
15
|
-
|
|
15
|
+
const readdirSync = vi.fn();
|
|
16
|
+
return { ipcHandlers: handlers, ipcMainHandleMock: handle, readdirSyncMock: readdirSync };
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
const invokeIpc = async <T = any>(
|
|
@@ -44,28 +45,18 @@ vi.mock('@/utils/logger', () => ({
|
|
|
44
45
|
}),
|
|
45
46
|
}));
|
|
46
47
|
|
|
47
|
-
const { spawnMock } = vi.hoisted(() => ({
|
|
48
|
-
spawnMock: vi.fn(() => {
|
|
49
|
-
const handlers = new Map<string, (...args: any[]) => void>();
|
|
50
|
-
return {
|
|
51
|
-
on: vi.fn((event: string, cb: (...args: any[]) => void) => {
|
|
52
|
-
handlers.set(event, cb);
|
|
53
|
-
return undefined;
|
|
54
|
-
}),
|
|
55
|
-
} as any;
|
|
56
|
-
}),
|
|
57
|
-
}));
|
|
58
|
-
|
|
59
|
-
vi.mock('node:child_process', () => ({
|
|
60
|
-
spawn: (...args: any[]) => spawnMock.call(null, ...args),
|
|
61
|
-
}));
|
|
62
|
-
|
|
63
48
|
// Mock electron
|
|
64
49
|
vi.mock('electron', () => ({
|
|
65
50
|
app: {
|
|
66
51
|
getLocale: vi.fn(() => 'en-US'),
|
|
67
52
|
getPath: vi.fn((name: string) => `/mock/path/${name}`),
|
|
68
53
|
},
|
|
54
|
+
desktopCapturer: {
|
|
55
|
+
getSources: vi.fn(async () => []),
|
|
56
|
+
},
|
|
57
|
+
dialog: {
|
|
58
|
+
showMessageBox: vi.fn(async () => ({ response: 0 })),
|
|
59
|
+
},
|
|
69
60
|
ipcMain: {
|
|
70
61
|
handle: ipcMainHandleMock,
|
|
71
62
|
},
|
|
@@ -89,6 +80,32 @@ vi.mock('electron-is', () => ({
|
|
|
89
80
|
macOS: vi.fn(() => true),
|
|
90
81
|
}));
|
|
91
82
|
|
|
83
|
+
// Mock node:fs for Full Disk Access check
|
|
84
|
+
vi.mock('node:fs', () => ({
|
|
85
|
+
default: {
|
|
86
|
+
readdirSync: readdirSyncMock,
|
|
87
|
+
},
|
|
88
|
+
readdirSync: readdirSyncMock,
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
// Mock node:os for homedir and release
|
|
92
|
+
vi.mock('node:os', () => ({
|
|
93
|
+
default: {
|
|
94
|
+
homedir: vi.fn(() => '/Users/testuser'),
|
|
95
|
+
release: vi.fn(() => '23.0.0'), // Darwin 23 = macOS 14 (Sonoma)
|
|
96
|
+
},
|
|
97
|
+
homedir: vi.fn(() => '/Users/testuser'),
|
|
98
|
+
release: vi.fn(() => '23.0.0'),
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
// Mock node:path
|
|
102
|
+
vi.mock('node:path', () => ({
|
|
103
|
+
default: {
|
|
104
|
+
join: vi.fn((...args: string[]) => args.join('/')),
|
|
105
|
+
},
|
|
106
|
+
join: vi.fn((...args: string[]) => args.join('/')),
|
|
107
|
+
}));
|
|
108
|
+
|
|
92
109
|
// Mock browserManager
|
|
93
110
|
const mockBrowserManager = {
|
|
94
111
|
broadcastToAllWindows: vi.fn(),
|
|
@@ -112,6 +129,7 @@ const mockStoreManager = {
|
|
|
112
129
|
// Mock i18n
|
|
113
130
|
const mockI18n = {
|
|
114
131
|
changeLanguage: vi.fn().mockResolvedValue(undefined),
|
|
132
|
+
ns: vi.fn((namespace: string) => (key: string) => `${namespace}.${key}`),
|
|
115
133
|
};
|
|
116
134
|
|
|
117
135
|
const mockApp = {
|
|
@@ -177,14 +195,78 @@ describe('SystemController', () => {
|
|
|
177
195
|
});
|
|
178
196
|
});
|
|
179
197
|
|
|
180
|
-
describe('
|
|
181
|
-
it('should
|
|
198
|
+
describe('microphone access', () => {
|
|
199
|
+
it('should ask for microphone access when status is not-determined', async () => {
|
|
200
|
+
const { systemPreferences } = await import('electron');
|
|
201
|
+
vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('not-determined');
|
|
202
|
+
|
|
203
|
+
await invokeIpc('system.requestMicrophoneAccess');
|
|
204
|
+
|
|
205
|
+
expect(systemPreferences.getMediaAccessStatus).toHaveBeenCalledWith('microphone');
|
|
206
|
+
expect(systemPreferences.askForMediaAccess).toHaveBeenCalledWith('microphone');
|
|
207
|
+
|
|
208
|
+
// Reset
|
|
209
|
+
vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('not-determined');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should return true immediately if microphone access is already granted', async () => {
|
|
182
213
|
const { shell, systemPreferences } = await import('electron');
|
|
214
|
+
vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('granted');
|
|
215
|
+
|
|
216
|
+
const result = await invokeIpc('system.requestMicrophoneAccess');
|
|
217
|
+
|
|
218
|
+
expect(result).toBe(true);
|
|
219
|
+
expect(systemPreferences.askForMediaAccess).not.toHaveBeenCalled();
|
|
220
|
+
expect(shell.openExternal).not.toHaveBeenCalled();
|
|
221
|
+
|
|
222
|
+
// Reset
|
|
223
|
+
vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('not-determined');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should open System Settings if microphone access is denied', async () => {
|
|
227
|
+
const { shell, systemPreferences } = await import('electron');
|
|
228
|
+
vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('denied');
|
|
229
|
+
|
|
230
|
+
const result = await invokeIpc('system.requestMicrophoneAccess');
|
|
231
|
+
|
|
232
|
+
expect(result).toBe(false);
|
|
233
|
+
expect(systemPreferences.askForMediaAccess).not.toHaveBeenCalled();
|
|
234
|
+
expect(shell.openExternal).toHaveBeenCalledWith(
|
|
235
|
+
'x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone',
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// Reset
|
|
239
|
+
vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('not-determined');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should return true on non-macOS', async () => {
|
|
243
|
+
const { macOS } = await import('electron-is');
|
|
244
|
+
const { shell, systemPreferences } = await import('electron');
|
|
245
|
+
vi.mocked(macOS).mockReturnValue(false);
|
|
246
|
+
|
|
247
|
+
const result = await invokeIpc('system.requestMicrophoneAccess');
|
|
248
|
+
|
|
249
|
+
expect(result).toBe(true);
|
|
250
|
+
expect(systemPreferences.getMediaAccessStatus).not.toHaveBeenCalled();
|
|
251
|
+
expect(shell.openExternal).not.toHaveBeenCalled();
|
|
252
|
+
|
|
253
|
+
// Reset
|
|
254
|
+
vi.mocked(macOS).mockReturnValue(true);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('screen recording', () => {
|
|
259
|
+
it('should use desktopCapturer and getDisplayMedia to trigger TCC and open System Settings on macOS', async () => {
|
|
260
|
+
const { desktopCapturer, shell, systemPreferences } = await import('electron');
|
|
183
261
|
|
|
184
262
|
const result = await invokeIpc('system.requestScreenAccess');
|
|
185
263
|
|
|
186
264
|
expect(systemPreferences.getMediaAccessStatus).toHaveBeenCalledWith('screen');
|
|
187
|
-
expect(
|
|
265
|
+
expect(desktopCapturer.getSources).toHaveBeenCalledWith({
|
|
266
|
+
fetchWindowIcons: true,
|
|
267
|
+
thumbnailSize: { height: 144, width: 256 },
|
|
268
|
+
types: ['screen', 'window'],
|
|
269
|
+
});
|
|
188
270
|
expect(mockBrowserManager.getMainWindow).toHaveBeenCalled();
|
|
189
271
|
expect(shell.openExternal).toHaveBeenCalledWith(
|
|
190
272
|
'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture',
|
|
@@ -192,15 +274,29 @@ describe('SystemController', () => {
|
|
|
192
274
|
expect(typeof result).toBe('boolean');
|
|
193
275
|
});
|
|
194
276
|
|
|
277
|
+
it('should return true immediately if screen access is already granted', async () => {
|
|
278
|
+
const { desktopCapturer, shell, systemPreferences } = await import('electron');
|
|
279
|
+
vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('granted');
|
|
280
|
+
|
|
281
|
+
const result = await invokeIpc('system.requestScreenAccess');
|
|
282
|
+
|
|
283
|
+
expect(result).toBe(true);
|
|
284
|
+
expect(desktopCapturer.getSources).not.toHaveBeenCalled();
|
|
285
|
+
expect(shell.openExternal).not.toHaveBeenCalled();
|
|
286
|
+
|
|
287
|
+
// Reset
|
|
288
|
+
vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('not-determined');
|
|
289
|
+
});
|
|
290
|
+
|
|
195
291
|
it('should return true on non-macOS and not open settings', async () => {
|
|
196
292
|
const { macOS } = await import('electron-is');
|
|
197
|
-
const {
|
|
293
|
+
const { desktopCapturer, shell } = await import('electron');
|
|
198
294
|
vi.mocked(macOS).mockReturnValue(false);
|
|
199
295
|
|
|
200
296
|
const result = await invokeIpc('system.requestScreenAccess');
|
|
201
297
|
|
|
202
298
|
expect(result).toBe(true);
|
|
203
|
-
expect(
|
|
299
|
+
expect(desktopCapturer.getSources).not.toHaveBeenCalled();
|
|
204
300
|
expect(shell.openExternal).not.toHaveBeenCalled();
|
|
205
301
|
|
|
206
302
|
// Reset
|
|
@@ -209,6 +305,40 @@ describe('SystemController', () => {
|
|
|
209
305
|
});
|
|
210
306
|
|
|
211
307
|
describe('full disk access', () => {
|
|
308
|
+
it('should return true when Full Disk Access is granted (can read protected directory)', async () => {
|
|
309
|
+
readdirSyncMock.mockReturnValue(['file1', 'file2']);
|
|
310
|
+
|
|
311
|
+
const result = await invokeIpc('system.getFullDiskAccessStatus');
|
|
312
|
+
|
|
313
|
+
expect(result).toBe(true);
|
|
314
|
+
// On macOS 14 (Darwin 23), should check com.apple.stocks
|
|
315
|
+
expect(readdirSyncMock).toHaveBeenCalledWith(
|
|
316
|
+
'/Users/testuser/Library/Containers/com.apple.stocks',
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should return false when Full Disk Access is not granted (cannot read protected directory)', async () => {
|
|
321
|
+
readdirSyncMock.mockImplementation(() => {
|
|
322
|
+
throw new Error('EPERM: operation not permitted');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const result = await invokeIpc('system.getFullDiskAccessStatus');
|
|
326
|
+
|
|
327
|
+
expect(result).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should return true on non-macOS', async () => {
|
|
331
|
+
const { macOS } = await import('electron-is');
|
|
332
|
+
vi.mocked(macOS).mockReturnValue(false);
|
|
333
|
+
|
|
334
|
+
const result = await invokeIpc('system.getFullDiskAccessStatus');
|
|
335
|
+
|
|
336
|
+
expect(result).toBe(true);
|
|
337
|
+
|
|
338
|
+
// Reset
|
|
339
|
+
vi.mocked(macOS).mockReturnValue(true);
|
|
340
|
+
});
|
|
341
|
+
|
|
212
342
|
it('should try to open Full Disk Access settings with fallbacks', async () => {
|
|
213
343
|
const { shell } = await import('electron');
|
|
214
344
|
vi.mocked(shell.openExternal)
|
|
@@ -218,25 +348,64 @@ describe('SystemController', () => {
|
|
|
218
348
|
await invokeIpc('system.openFullDiskAccessSettings');
|
|
219
349
|
|
|
220
350
|
expect(shell.openExternal).toHaveBeenCalledWith(
|
|
221
|
-
'com.apple.settings
|
|
351
|
+
'x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles',
|
|
222
352
|
);
|
|
223
353
|
expect(shell.openExternal).toHaveBeenCalledWith(
|
|
224
354
|
'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles',
|
|
225
355
|
);
|
|
226
356
|
});
|
|
227
357
|
|
|
228
|
-
it('should
|
|
358
|
+
it('should open fallback Privacy settings if all candidates fail', async () => {
|
|
229
359
|
const { shell } = await import('electron');
|
|
230
|
-
vi.mocked(shell.openExternal)
|
|
360
|
+
vi.mocked(shell.openExternal)
|
|
361
|
+
.mockRejectedValueOnce(new Error('fail first'))
|
|
362
|
+
.mockRejectedValueOnce(new Error('fail second'))
|
|
363
|
+
.mockResolvedValueOnce(undefined);
|
|
231
364
|
|
|
232
|
-
await invokeIpc('system.openFullDiskAccessSettings'
|
|
365
|
+
await invokeIpc('system.openFullDiskAccessSettings');
|
|
233
366
|
|
|
234
|
-
expect(
|
|
235
|
-
'
|
|
236
|
-
expect.arrayContaining(['-e', expect.any(String), expect.any(String)]),
|
|
237
|
-
expect.objectContaining({ env: expect.any(Object) }),
|
|
367
|
+
expect(shell.openExternal).toHaveBeenCalledWith(
|
|
368
|
+
'x-apple.systempreferences:com.apple.preference.security?Privacy',
|
|
238
369
|
);
|
|
239
370
|
});
|
|
371
|
+
|
|
372
|
+
it('should return granted if Full Disk Access is already granted', async () => {
|
|
373
|
+
readdirSyncMock.mockReturnValue(['file1', 'file2']);
|
|
374
|
+
|
|
375
|
+
const result = await invokeIpc('system.promptFullDiskAccessIfNotGranted');
|
|
376
|
+
|
|
377
|
+
expect(result).toBe('granted');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should show dialog and open settings when user clicks Open Settings', async () => {
|
|
381
|
+
const { dialog, shell } = await import('electron');
|
|
382
|
+
readdirSyncMock.mockImplementation(() => {
|
|
383
|
+
throw new Error('EPERM: operation not permitted');
|
|
384
|
+
});
|
|
385
|
+
vi.mocked(dialog.showMessageBox).mockResolvedValue({ response: 0 } as any);
|
|
386
|
+
|
|
387
|
+
const result = await invokeIpc('system.promptFullDiskAccessIfNotGranted');
|
|
388
|
+
|
|
389
|
+
expect(result).toBe('opened_settings');
|
|
390
|
+
expect(dialog.showMessageBox).toHaveBeenCalled();
|
|
391
|
+
expect(shell.openExternal).toHaveBeenCalled();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should return skipped when user clicks Later', async () => {
|
|
395
|
+
const { dialog, shell } = await import('electron');
|
|
396
|
+
readdirSyncMock.mockImplementation(() => {
|
|
397
|
+
throw new Error('EPERM: operation not permitted');
|
|
398
|
+
});
|
|
399
|
+
vi.mocked(dialog.showMessageBox).mockResolvedValue({ response: 1 } as any);
|
|
400
|
+
vi.mocked(shell.openExternal).mockClear();
|
|
401
|
+
|
|
402
|
+
const result = await invokeIpc('system.promptFullDiskAccessIfNotGranted');
|
|
403
|
+
|
|
404
|
+
expect(result).toBe('skipped');
|
|
405
|
+
expect(dialog.showMessageBox).toHaveBeenCalled();
|
|
406
|
+
// Should not open settings when user skips
|
|
407
|
+
expect(shell.openExternal).not.toHaveBeenCalled();
|
|
408
|
+
});
|
|
240
409
|
});
|
|
241
410
|
|
|
242
411
|
describe('openExternalLink', () => {
|
|
@@ -183,6 +183,7 @@ export default class Browser {
|
|
|
183
183
|
private setupEventListeners(browserWindow: BrowserWindow): void {
|
|
184
184
|
this.setupReadyToShowListener(browserWindow);
|
|
185
185
|
this.setupCloseListener(browserWindow);
|
|
186
|
+
this.setupFocusListener(browserWindow);
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
private setupReadyToShowListener(browserWindow: BrowserWindow): void {
|
|
@@ -207,6 +208,14 @@ export default class Browser {
|
|
|
207
208
|
browserWindow.on('close', closeHandler);
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
private setupFocusListener(browserWindow: BrowserWindow): void {
|
|
212
|
+
logger.debug(`[${this.identifier}] Setting up 'focus' event listener.`);
|
|
213
|
+
browserWindow.on('focus', () => {
|
|
214
|
+
logger.debug(`[${this.identifier}] Window 'focus' event fired.`);
|
|
215
|
+
this.broadcast('windowFocused');
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
210
219
|
// ==================== Window Actions ====================
|
|
211
220
|
|
|
212
221
|
show(): void {
|
|
@@ -8,9 +8,14 @@ const dialog = {
|
|
|
8
8
|
'confirm.title': 'Please confirm',
|
|
9
9
|
'confirm.yes': 'Continue',
|
|
10
10
|
'error.button': 'OK',
|
|
11
|
-
'error.detail':
|
|
11
|
+
'error.detail': "Couldn't complete the action. Retry or try again later.",
|
|
12
12
|
'error.message': 'An error occurred',
|
|
13
13
|
'error.title': 'Error',
|
|
14
|
+
'fullDiskAccess.message':
|
|
15
|
+
'LobeHub needs Full Disk Access to read files and enable knowledge base features. Please grant access in System Settings.',
|
|
16
|
+
'fullDiskAccess.openSettings': 'Open Settings',
|
|
17
|
+
'fullDiskAccess.skip': 'Later',
|
|
18
|
+
'fullDiskAccess.title': 'Full Disk Access Required',
|
|
14
19
|
'update.downloadAndInstall': 'Download and Install',
|
|
15
20
|
'update.downloadComplete': 'Download Complete',
|
|
16
21
|
'update.downloadCompleteMessage': 'Update downloaded. Install now?',
|
|
@@ -22,4 +27,4 @@ const dialog = {
|
|
|
22
27
|
'update.skipThisVersion': 'Skip This Version',
|
|
23
28
|
};
|
|
24
29
|
|
|
25
|
-
export default dialog;
|
|
30
|
+
export default dialog;
|
|
@@ -7,6 +7,13 @@ const menu = {
|
|
|
7
7
|
'dev.openStore': 'Open Data Folder',
|
|
8
8
|
'dev.openUpdaterCacheDir': 'Open Updater Cache',
|
|
9
9
|
'dev.openUserDataDir': 'Open User Data',
|
|
10
|
+
'dev.permissions.accessibility.request': 'Request Accessibility Permission',
|
|
11
|
+
'dev.permissions.fullDisk.open': 'Open Full Disk Access Settings',
|
|
12
|
+
'dev.permissions.fullDisk.request': 'Request Full Disk Access Permission',
|
|
13
|
+
'dev.permissions.microphone.request': 'Request Microphone Permission',
|
|
14
|
+
'dev.permissions.notification.request': 'Request Notification Permission',
|
|
15
|
+
'dev.permissions.screen.request': 'Request Screen Recording Permission',
|
|
16
|
+
'dev.permissions.title': 'Permissions',
|
|
10
17
|
'dev.refreshMenu': 'Refresh Menu',
|
|
11
18
|
'dev.reload': 'Reload',
|
|
12
19
|
'dev.simulateAutoDownload': 'Simulate Auto Download (3s)',
|
|
@@ -2,6 +2,8 @@ import { Menu, MenuItemConstructorOptions, app, shell } from 'electron';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { isDev } from '@/const/env';
|
|
5
|
+
import NotificationCtr from '@/controllers/NotificationCtr';
|
|
6
|
+
import SystemController from '@/controllers/SystemCtr';
|
|
5
7
|
|
|
6
8
|
import type { IMenuPlatform, MenuOptions } from '../types';
|
|
7
9
|
import { BaseMenuPlatform } from './BaseMenuPlatform';
|
|
@@ -57,7 +59,6 @@ export class MacOSMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|
|
57
59
|
private getAppMenuTemplate(options?: MenuOptions): MenuItemConstructorOptions[] {
|
|
58
60
|
const appName = app.getName();
|
|
59
61
|
const showDev = isDev || options?.showDevItems;
|
|
60
|
-
|
|
61
62
|
// 创建命名空间翻译函数
|
|
62
63
|
const t = this.app.i18n.ns('menu');
|
|
63
64
|
|
|
@@ -269,6 +270,48 @@ export class MacOSMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|
|
269
270
|
label: t('dev.refreshMenu'),
|
|
270
271
|
},
|
|
271
272
|
{ type: 'separator' },
|
|
273
|
+
{
|
|
274
|
+
label: t('dev.permissions.title'),
|
|
275
|
+
submenu: [
|
|
276
|
+
{
|
|
277
|
+
click: () => {
|
|
278
|
+
const notificationCtr = this.app.getController(NotificationCtr);
|
|
279
|
+
void notificationCtr.requestNotificationPermission();
|
|
280
|
+
},
|
|
281
|
+
label: t('dev.permissions.notification.request'),
|
|
282
|
+
},
|
|
283
|
+
{ type: 'separator' },
|
|
284
|
+
{
|
|
285
|
+
click: () => {
|
|
286
|
+
const systemCtr = this.app.getController(SystemController);
|
|
287
|
+
void systemCtr.requestAccessibilityAccess();
|
|
288
|
+
},
|
|
289
|
+
label: t('dev.permissions.accessibility.request'),
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
click: () => {
|
|
293
|
+
const systemCtr = this.app.getController(SystemController);
|
|
294
|
+
void systemCtr.requestMicrophoneAccess();
|
|
295
|
+
},
|
|
296
|
+
label: t('dev.permissions.microphone.request'),
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
click: () => {
|
|
300
|
+
const systemCtr = this.app.getController(SystemController);
|
|
301
|
+
void systemCtr.requestScreenAccess();
|
|
302
|
+
},
|
|
303
|
+
label: t('dev.permissions.screen.request'),
|
|
304
|
+
},
|
|
305
|
+
{ type: 'separator' },
|
|
306
|
+
{
|
|
307
|
+
click: () => {
|
|
308
|
+
const systemCtr = this.app.getController(SystemController);
|
|
309
|
+
void systemCtr.promptFullDiskAccessIfNotGranted();
|
|
310
|
+
},
|
|
311
|
+
label: t('dev.permissions.fullDisk.request'),
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
},
|
|
272
315
|
{
|
|
273
316
|
click: () => {
|
|
274
317
|
const userDataPath = app.getPath('userData');
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full Disk Access utilities for macOS
|
|
3
|
+
* Based on https://github.com/inket/FullDiskAccess
|
|
4
|
+
*/
|
|
5
|
+
import { shell } from 'electron';
|
|
6
|
+
import { macOS } from 'electron-is';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { createLogger } from './logger';
|
|
12
|
+
|
|
13
|
+
const logger = createLogger('utils:fullDiskAccess');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the macOS major version number
|
|
17
|
+
* Returns 0 if not macOS or unable to determine
|
|
18
|
+
*
|
|
19
|
+
* Darwin version to macOS version mapping:
|
|
20
|
+
* - Darwin 23.x = macOS 14 (Sonoma)
|
|
21
|
+
* - Darwin 22.x = macOS 13 (Ventura)
|
|
22
|
+
* - Darwin 21.x = macOS 12 (Monterey)
|
|
23
|
+
* - Darwin 20.x = macOS 11 (Big Sur)
|
|
24
|
+
* - Darwin 19.x = macOS 10.15 (Catalina)
|
|
25
|
+
* - Darwin 18.x = macOS 10.14 (Mojave)
|
|
26
|
+
*/
|
|
27
|
+
export function getMacOSMajorVersion(): number {
|
|
28
|
+
if (!macOS()) return 0;
|
|
29
|
+
try {
|
|
30
|
+
const release = os.release(); // e.g., "23.0.0" for macOS 14 (Sonoma)
|
|
31
|
+
const darwinMajor = Number.parseInt(release.split('.')[0], 10);
|
|
32
|
+
if (darwinMajor >= 20) {
|
|
33
|
+
return darwinMajor - 9; // Darwin 20 = macOS 11, Darwin 21 = macOS 12, etc.
|
|
34
|
+
}
|
|
35
|
+
// For older versions, return 10 (covers Mojave and Catalina)
|
|
36
|
+
return 10;
|
|
37
|
+
} catch {
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if Full Disk Access is granted by attempting to read a protected directory.
|
|
44
|
+
*
|
|
45
|
+
* On macOS 12+ (Monterey, Ventura, Sonoma, Sequoia): checks ~/Library/Containers/com.apple.stocks
|
|
46
|
+
* On macOS 10.14-11 (Mojave, Catalina, Big Sur): checks ~/Library/Safari
|
|
47
|
+
*
|
|
48
|
+
* Reading these directories will also register the app in TCC database,
|
|
49
|
+
* making it appear in System Settings > Privacy & Security > Full Disk Access
|
|
50
|
+
*/
|
|
51
|
+
export function checkFullDiskAccess(): boolean {
|
|
52
|
+
if (!macOS()) return true;
|
|
53
|
+
|
|
54
|
+
const homeDir = os.homedir();
|
|
55
|
+
const macOSVersion = getMacOSMajorVersion();
|
|
56
|
+
|
|
57
|
+
// Determine which protected directory to check based on macOS version
|
|
58
|
+
let checkPath: string;
|
|
59
|
+
if (macOSVersion >= 12) {
|
|
60
|
+
// macOS 12+ (Monterey, Ventura, Sonoma, Sequoia)
|
|
61
|
+
checkPath = path.join(homeDir, 'Library', 'Containers', 'com.apple.stocks');
|
|
62
|
+
} else {
|
|
63
|
+
// macOS 10.14-11 (Mojave, Catalina, Big Sur)
|
|
64
|
+
checkPath = path.join(homeDir, 'Library', 'Safari');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
fs.readdirSync(checkPath);
|
|
69
|
+
logger.info(`[FullDiskAccess] Access granted (able to read ${checkPath})`);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
logger.info(`[FullDiskAccess] Access not granted (unable to read ${checkPath})`);
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Open Full Disk Access settings page in System Settings
|
|
79
|
+
*
|
|
80
|
+
* NOTE: Full Disk Access cannot be requested programmatically.
|
|
81
|
+
* User must manually add the app in System Settings.
|
|
82
|
+
* There is NO entitlement for Full Disk Access - it's purely TCC controlled.
|
|
83
|
+
*/
|
|
84
|
+
export async function openFullDiskAccessSettings(): Promise<void> {
|
|
85
|
+
if (!macOS()) {
|
|
86
|
+
logger.info('[FullDiskAccess] Not macOS, skipping');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
logger.info('[FullDiskAccess] Opening Full Disk Access settings...');
|
|
91
|
+
|
|
92
|
+
// On macOS 13+ (Ventura), System Preferences is replaced by System Settings,
|
|
93
|
+
// and deep links may differ. We try multiple known schemes for compatibility.
|
|
94
|
+
const candidates = [
|
|
95
|
+
// macOS 13+ (Ventura and later) - System Settings
|
|
96
|
+
'x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles',
|
|
97
|
+
// macOS 13+ alternative format
|
|
98
|
+
'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles',
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
for (const url of candidates) {
|
|
102
|
+
try {
|
|
103
|
+
logger.info(`[FullDiskAccess] Trying URL: ${url}`);
|
|
104
|
+
await shell.openExternal(url);
|
|
105
|
+
logger.info(`[FullDiskAccess] Successfully opened via ${url}`);
|
|
106
|
+
return;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
logger.warn(`[FullDiskAccess] Failed with URL ${url}:`, error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Fallback: open Privacy & Security pane
|
|
113
|
+
try {
|
|
114
|
+
const fallbackUrl = 'x-apple.systempreferences:com.apple.preference.security?Privacy';
|
|
115
|
+
logger.info(`[FullDiskAccess] Trying fallback URL: ${fallbackUrl}`);
|
|
116
|
+
await shell.openExternal(fallbackUrl);
|
|
117
|
+
logger.info('[FullDiskAccess] Opened Privacy & Security settings as fallback');
|
|
118
|
+
} catch (error) {
|
|
119
|
+
logger.error('[FullDiskAccess] Failed to open any Privacy settings:', error);
|
|
120
|
+
}
|
|
121
|
+
}
|
package/changelog/v1.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"children": {},
|
|
4
|
+
"date": "2026-01-09",
|
|
5
|
+
"version": "2.0.0-next.253"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"children": {
|
|
9
|
+
"features": [
|
|
10
|
+
"Add the agent cron job."
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"date": "2026-01-09",
|
|
14
|
+
"version": "2.0.0-next.252"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"children": {},
|
|
4
18
|
"date": "2026-01-09",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.253",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -11,7 +11,6 @@ import { useChatStore } from '@/store/chat';
|
|
|
11
11
|
|
|
12
12
|
import { NotebookDocument } from '../../../types';
|
|
13
13
|
|
|
14
|
-
|
|
15
14
|
const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
16
15
|
container: css`
|
|
17
16
|
position: relative;
|
|
@@ -26,7 +25,6 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
|
26
25
|
`,
|
|
27
26
|
content: css`
|
|
28
27
|
padding-inline: 16px;
|
|
29
|
-
|
|
30
28
|
font-size: 14px;
|
|
31
29
|
`,
|
|
32
30
|
expandButton: css`
|