@lobehub/lobehub 2.0.0-next.258 → 2.0.0-next.259
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 +25 -0
- package/apps/desktop/{electron-builder.js → electron-builder.mjs} +24 -11
- package/apps/desktop/electron.vite.config.ts +10 -4
- package/apps/desktop/native-deps.config.mjs +102 -0
- package/apps/desktop/package.json +8 -7
- package/apps/desktop/src/main/__mocks__/node-mac-permissions.ts +21 -0
- package/apps/desktop/src/main/__mocks__/setup.ts +8 -0
- package/apps/desktop/src/main/controllers/SystemCtr.ts +20 -159
- package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +58 -90
- package/apps/desktop/src/main/utils/permissions.ts +307 -0
- package/apps/desktop/tsconfig.json +2 -1
- package/apps/desktop/vitest.config.mts +1 -0
- package/changelog/v1.json +9 -0
- package/locales/en-US/setting.json +1 -0
- package/locales/zh-CN/setting.json +1 -0
- package/package.json +1 -1
- package/packages/database/src/schemas/agentCronJob.ts +53 -19
- package/packages/file-loaders/package.json +1 -1
- package/scripts/electronWorkflow/buildNextApp.mts +39 -1
- package/scripts/electronWorkflow/modifiers/nextConfig.mts +26 -0
- package/src/app/[variants]/(main)/chat/cron/[cronId]/index.tsx +12 -8
- package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/CronJobForm.tsx +9 -7
- package/apps/desktop/src/main/utils/fullDiskAccess.ts +0 -121
|
@@ -7,13 +7,20 @@ import { IpcHandler } from '@/utils/ipc/base';
|
|
|
7
7
|
|
|
8
8
|
import SystemController from '../SystemCtr';
|
|
9
9
|
|
|
10
|
-
const { ipcHandlers, ipcMainHandleMock,
|
|
10
|
+
const { ipcHandlers, ipcMainHandleMock, permissionsMock } = 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
|
-
const
|
|
16
|
-
|
|
15
|
+
const permissions = {
|
|
16
|
+
askForAccessibilityAccess: vi.fn(() => undefined),
|
|
17
|
+
askForCameraAccess: vi.fn(() => Promise.resolve('authorized')),
|
|
18
|
+
askForFullDiskAccess: vi.fn(() => undefined),
|
|
19
|
+
askForMicrophoneAccess: vi.fn(() => Promise.resolve('authorized')),
|
|
20
|
+
askForScreenCaptureAccess: vi.fn(() => undefined),
|
|
21
|
+
getAuthStatus: vi.fn(() => 'authorized'),
|
|
22
|
+
};
|
|
23
|
+
return { ipcHandlers: handlers, ipcMainHandleMock: handle, permissionsMock: permissions };
|
|
17
24
|
});
|
|
18
25
|
|
|
19
26
|
const invokeIpc = async <T = any>(
|
|
@@ -80,31 +87,8 @@ vi.mock('electron-is', () => ({
|
|
|
80
87
|
macOS: vi.fn(() => true),
|
|
81
88
|
}));
|
|
82
89
|
|
|
83
|
-
// Mock node
|
|
84
|
-
vi.mock('node
|
|
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
|
-
}));
|
|
90
|
+
// Mock node-mac-permissions
|
|
91
|
+
vi.mock('node-mac-permissions', () => permissionsMock);
|
|
108
92
|
|
|
109
93
|
// Mock browserManager
|
|
110
94
|
const mockBrowserManager = {
|
|
@@ -173,22 +157,23 @@ describe('SystemController', () => {
|
|
|
173
157
|
|
|
174
158
|
describe('accessibility', () => {
|
|
175
159
|
it('should request accessibility access on macOS', async () => {
|
|
176
|
-
|
|
160
|
+
permissionsMock.getAuthStatus.mockReturnValue('authorized');
|
|
177
161
|
|
|
178
|
-
await invokeIpc('system.requestAccessibilityAccess');
|
|
162
|
+
const result = await invokeIpc('system.requestAccessibilityAccess');
|
|
179
163
|
|
|
180
|
-
expect(
|
|
164
|
+
expect(permissionsMock.askForAccessibilityAccess).toHaveBeenCalled();
|
|
165
|
+
expect(permissionsMock.getAuthStatus).toHaveBeenCalledWith('accessibility');
|
|
166
|
+
expect(result).toBe(true);
|
|
181
167
|
});
|
|
182
168
|
|
|
183
169
|
it('should return true on non-macOS when requesting accessibility access', async () => {
|
|
184
170
|
const { macOS } = await import('electron-is');
|
|
185
|
-
const { systemPreferences } = await import('electron');
|
|
186
171
|
vi.mocked(macOS).mockReturnValue(false);
|
|
187
172
|
|
|
188
173
|
const result = await invokeIpc('system.requestAccessibilityAccess');
|
|
189
174
|
|
|
190
175
|
expect(result).toBe(true);
|
|
191
|
-
expect(
|
|
176
|
+
expect(permissionsMock.askForAccessibilityAccess).not.toHaveBeenCalled();
|
|
192
177
|
|
|
193
178
|
// Reset
|
|
194
179
|
vi.mocked(macOS).mockReturnValue(true);
|
|
@@ -197,57 +182,55 @@ describe('SystemController', () => {
|
|
|
197
182
|
|
|
198
183
|
describe('microphone access', () => {
|
|
199
184
|
it('should ask for microphone access when status is not-determined', async () => {
|
|
200
|
-
|
|
201
|
-
|
|
185
|
+
permissionsMock.getAuthStatus.mockReturnValue('not determined');
|
|
186
|
+
permissionsMock.askForMicrophoneAccess.mockResolvedValue('authorized');
|
|
202
187
|
|
|
203
|
-
await invokeIpc('system.requestMicrophoneAccess');
|
|
188
|
+
const result = await invokeIpc('system.requestMicrophoneAccess');
|
|
204
189
|
|
|
205
|
-
expect(
|
|
206
|
-
expect(
|
|
190
|
+
expect(permissionsMock.getAuthStatus).toHaveBeenCalledWith('microphone');
|
|
191
|
+
expect(permissionsMock.askForMicrophoneAccess).toHaveBeenCalled();
|
|
192
|
+
expect(result).toBe(true);
|
|
207
193
|
|
|
208
194
|
// Reset
|
|
209
|
-
|
|
195
|
+
permissionsMock.getAuthStatus.mockReturnValue('authorized');
|
|
210
196
|
});
|
|
211
197
|
|
|
212
198
|
it('should return true immediately if microphone access is already granted', async () => {
|
|
213
|
-
const { shell
|
|
214
|
-
|
|
199
|
+
const { shell } = await import('electron');
|
|
200
|
+
permissionsMock.getAuthStatus.mockReturnValue('authorized');
|
|
215
201
|
|
|
216
202
|
const result = await invokeIpc('system.requestMicrophoneAccess');
|
|
217
203
|
|
|
218
204
|
expect(result).toBe(true);
|
|
219
|
-
expect(
|
|
205
|
+
expect(permissionsMock.askForMicrophoneAccess).not.toHaveBeenCalled();
|
|
220
206
|
expect(shell.openExternal).not.toHaveBeenCalled();
|
|
221
|
-
|
|
222
|
-
// Reset
|
|
223
|
-
vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('not-determined');
|
|
224
207
|
});
|
|
225
208
|
|
|
226
209
|
it('should open System Settings if microphone access is denied', async () => {
|
|
227
|
-
const { shell
|
|
228
|
-
|
|
210
|
+
const { shell } = await import('electron');
|
|
211
|
+
permissionsMock.getAuthStatus.mockReturnValue('denied');
|
|
229
212
|
|
|
230
213
|
const result = await invokeIpc('system.requestMicrophoneAccess');
|
|
231
214
|
|
|
232
215
|
expect(result).toBe(false);
|
|
233
|
-
expect(
|
|
216
|
+
expect(permissionsMock.askForMicrophoneAccess).not.toHaveBeenCalled();
|
|
234
217
|
expect(shell.openExternal).toHaveBeenCalledWith(
|
|
235
218
|
'x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone',
|
|
236
219
|
);
|
|
237
220
|
|
|
238
221
|
// Reset
|
|
239
|
-
|
|
222
|
+
permissionsMock.getAuthStatus.mockReturnValue('authorized');
|
|
240
223
|
});
|
|
241
224
|
|
|
242
225
|
it('should return true on non-macOS', async () => {
|
|
243
226
|
const { macOS } = await import('electron-is');
|
|
244
|
-
const { shell
|
|
227
|
+
const { shell } = await import('electron');
|
|
245
228
|
vi.mocked(macOS).mockReturnValue(false);
|
|
246
229
|
|
|
247
230
|
const result = await invokeIpc('system.requestMicrophoneAccess');
|
|
248
231
|
|
|
249
232
|
expect(result).toBe(true);
|
|
250
|
-
expect(
|
|
233
|
+
expect(permissionsMock.getAuthStatus).not.toHaveBeenCalled();
|
|
251
234
|
expect(shell.openExternal).not.toHaveBeenCalled();
|
|
252
235
|
|
|
253
236
|
// Reset
|
|
@@ -256,48 +239,33 @@ describe('SystemController', () => {
|
|
|
256
239
|
});
|
|
257
240
|
|
|
258
241
|
describe('screen recording', () => {
|
|
259
|
-
it('should
|
|
260
|
-
|
|
242
|
+
it('should request screen capture access on macOS', async () => {
|
|
243
|
+
permissionsMock.getAuthStatus.mockReturnValue('not determined');
|
|
261
244
|
|
|
262
245
|
const result = await invokeIpc('system.requestScreenAccess');
|
|
263
246
|
|
|
264
|
-
expect(
|
|
265
|
-
expect(
|
|
266
|
-
fetchWindowIcons: true,
|
|
267
|
-
thumbnailSize: { height: 144, width: 256 },
|
|
268
|
-
types: ['screen', 'window'],
|
|
269
|
-
});
|
|
270
|
-
expect(mockBrowserManager.getMainWindow).toHaveBeenCalled();
|
|
271
|
-
expect(shell.openExternal).toHaveBeenCalledWith(
|
|
272
|
-
'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture',
|
|
273
|
-
);
|
|
247
|
+
expect(permissionsMock.getAuthStatus).toHaveBeenCalledWith('screen');
|
|
248
|
+
expect(permissionsMock.askForScreenCaptureAccess).toHaveBeenCalled();
|
|
274
249
|
expect(typeof result).toBe('boolean');
|
|
275
250
|
});
|
|
276
251
|
|
|
277
252
|
it('should return true immediately if screen access is already granted', async () => {
|
|
278
|
-
|
|
279
|
-
vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('granted');
|
|
253
|
+
permissionsMock.getAuthStatus.mockReturnValue('authorized');
|
|
280
254
|
|
|
281
255
|
const result = await invokeIpc('system.requestScreenAccess');
|
|
282
256
|
|
|
283
257
|
expect(result).toBe(true);
|
|
284
|
-
expect(
|
|
285
|
-
expect(shell.openExternal).not.toHaveBeenCalled();
|
|
286
|
-
|
|
287
|
-
// Reset
|
|
288
|
-
vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('not-determined');
|
|
258
|
+
expect(permissionsMock.askForScreenCaptureAccess).not.toHaveBeenCalled();
|
|
289
259
|
});
|
|
290
260
|
|
|
291
261
|
it('should return true on non-macOS and not open settings', async () => {
|
|
292
262
|
const { macOS } = await import('electron-is');
|
|
293
|
-
const { desktopCapturer, shell } = await import('electron');
|
|
294
263
|
vi.mocked(macOS).mockReturnValue(false);
|
|
295
264
|
|
|
296
265
|
const result = await invokeIpc('system.requestScreenAccess');
|
|
297
266
|
|
|
298
267
|
expect(result).toBe(true);
|
|
299
|
-
expect(
|
|
300
|
-
expect(shell.openExternal).not.toHaveBeenCalled();
|
|
268
|
+
expect(permissionsMock.askForScreenCaptureAccess).not.toHaveBeenCalled();
|
|
301
269
|
|
|
302
270
|
// Reset
|
|
303
271
|
vi.mocked(macOS).mockReturnValue(true);
|
|
@@ -305,26 +273,24 @@ describe('SystemController', () => {
|
|
|
305
273
|
});
|
|
306
274
|
|
|
307
275
|
describe('full disk access', () => {
|
|
308
|
-
it('should return true when Full Disk Access is granted
|
|
309
|
-
|
|
276
|
+
it('should return true when Full Disk Access is granted', async () => {
|
|
277
|
+
permissionsMock.getAuthStatus.mockReturnValue('authorized');
|
|
310
278
|
|
|
311
279
|
const result = await invokeIpc('system.getFullDiskAccessStatus');
|
|
312
280
|
|
|
313
281
|
expect(result).toBe(true);
|
|
314
|
-
|
|
315
|
-
expect(readdirSyncMock).toHaveBeenCalledWith(
|
|
316
|
-
'/Users/testuser/Library/Containers/com.apple.stocks',
|
|
317
|
-
);
|
|
282
|
+
expect(permissionsMock.getAuthStatus).toHaveBeenCalledWith('full-disk-access');
|
|
318
283
|
});
|
|
319
284
|
|
|
320
|
-
it('should return false when Full Disk Access is not granted
|
|
321
|
-
|
|
322
|
-
throw new Error('EPERM: operation not permitted');
|
|
323
|
-
});
|
|
285
|
+
it('should return false when Full Disk Access is not granted', async () => {
|
|
286
|
+
permissionsMock.getAuthStatus.mockReturnValue('denied');
|
|
324
287
|
|
|
325
288
|
const result = await invokeIpc('system.getFullDiskAccessStatus');
|
|
326
289
|
|
|
327
290
|
expect(result).toBe(false);
|
|
291
|
+
|
|
292
|
+
// Reset
|
|
293
|
+
permissionsMock.getAuthStatus.mockReturnValue('authorized');
|
|
328
294
|
});
|
|
329
295
|
|
|
330
296
|
it('should return true on non-macOS', async () => {
|
|
@@ -370,7 +336,7 @@ describe('SystemController', () => {
|
|
|
370
336
|
});
|
|
371
337
|
|
|
372
338
|
it('should return granted if Full Disk Access is already granted', async () => {
|
|
373
|
-
|
|
339
|
+
permissionsMock.getAuthStatus.mockReturnValue('authorized');
|
|
374
340
|
|
|
375
341
|
const result = await invokeIpc('system.promptFullDiskAccessIfNotGranted');
|
|
376
342
|
|
|
@@ -379,9 +345,7 @@ describe('SystemController', () => {
|
|
|
379
345
|
|
|
380
346
|
it('should show dialog and open settings when user clicks Open Settings', async () => {
|
|
381
347
|
const { dialog, shell } = await import('electron');
|
|
382
|
-
|
|
383
|
-
throw new Error('EPERM: operation not permitted');
|
|
384
|
-
});
|
|
348
|
+
permissionsMock.getAuthStatus.mockReturnValue('denied');
|
|
385
349
|
vi.mocked(dialog.showMessageBox).mockResolvedValue({ response: 0 } as any);
|
|
386
350
|
|
|
387
351
|
const result = await invokeIpc('system.promptFullDiskAccessIfNotGranted');
|
|
@@ -389,13 +353,14 @@ describe('SystemController', () => {
|
|
|
389
353
|
expect(result).toBe('opened_settings');
|
|
390
354
|
expect(dialog.showMessageBox).toHaveBeenCalled();
|
|
391
355
|
expect(shell.openExternal).toHaveBeenCalled();
|
|
356
|
+
|
|
357
|
+
// Reset
|
|
358
|
+
permissionsMock.getAuthStatus.mockReturnValue('authorized');
|
|
392
359
|
});
|
|
393
360
|
|
|
394
361
|
it('should return skipped when user clicks Later', async () => {
|
|
395
362
|
const { dialog, shell } = await import('electron');
|
|
396
|
-
|
|
397
|
-
throw new Error('EPERM: operation not permitted');
|
|
398
|
-
});
|
|
363
|
+
permissionsMock.getAuthStatus.mockReturnValue('denied');
|
|
399
364
|
vi.mocked(dialog.showMessageBox).mockResolvedValue({ response: 1 } as any);
|
|
400
365
|
vi.mocked(shell.openExternal).mockClear();
|
|
401
366
|
|
|
@@ -405,6 +370,9 @@ describe('SystemController', () => {
|
|
|
405
370
|
expect(dialog.showMessageBox).toHaveBeenCalled();
|
|
406
371
|
// Should not open settings when user skips
|
|
407
372
|
expect(shell.openExternal).not.toHaveBeenCalled();
|
|
373
|
+
|
|
374
|
+
// Reset
|
|
375
|
+
permissionsMock.getAuthStatus.mockReturnValue('authorized');
|
|
408
376
|
});
|
|
409
377
|
});
|
|
410
378
|
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified macOS Permission Management using node-mac-permissions
|
|
3
|
+
* @see https://github.com/codebytere/node-mac-permissions
|
|
4
|
+
*/
|
|
5
|
+
import { shell } from 'electron';
|
|
6
|
+
import { macOS } from 'electron-is';
|
|
7
|
+
import {
|
|
8
|
+
askForAccessibilityAccess,
|
|
9
|
+
askForCameraAccess,
|
|
10
|
+
askForFullDiskAccess,
|
|
11
|
+
askForMicrophoneAccess,
|
|
12
|
+
askForScreenCaptureAccess,
|
|
13
|
+
getAuthStatus,
|
|
14
|
+
type AuthType,
|
|
15
|
+
type PermissionType,
|
|
16
|
+
} from 'node-mac-permissions';
|
|
17
|
+
|
|
18
|
+
import { createLogger } from './logger';
|
|
19
|
+
|
|
20
|
+
const logger = createLogger('utils:permissions');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Permission status mapping between node-mac-permissions and our internal representation
|
|
24
|
+
*/
|
|
25
|
+
export type PermissionStatus =
|
|
26
|
+
| 'authorized'
|
|
27
|
+
| 'denied'
|
|
28
|
+
| 'not-determined'
|
|
29
|
+
| 'restricted'
|
|
30
|
+
| 'granted'; // alias for authorized
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalize permission status to a consistent format
|
|
34
|
+
*/
|
|
35
|
+
function normalizeStatus(status: PermissionType | 'not determined'): PermissionStatus {
|
|
36
|
+
if (status === 'not determined') return 'not-determined';
|
|
37
|
+
if (status === 'authorized') return 'granted';
|
|
38
|
+
return status;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the authorization status for a specific permission type
|
|
43
|
+
*/
|
|
44
|
+
export function getPermissionStatus(type: AuthType): PermissionStatus {
|
|
45
|
+
if (!macOS()) {
|
|
46
|
+
logger.debug(`[Permission] Not macOS, returning granted for ${type}`);
|
|
47
|
+
return 'granted';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const status = getAuthStatus(type);
|
|
51
|
+
const normalized = normalizeStatus(status);
|
|
52
|
+
logger.info(`[Permission] ${type} status: ${normalized}`);
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if Accessibility permission is granted
|
|
58
|
+
*/
|
|
59
|
+
export function getAccessibilityStatus(): PermissionStatus {
|
|
60
|
+
return getPermissionStatus('accessibility');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Request Accessibility permission
|
|
65
|
+
* Opens System Preferences to the Accessibility pane
|
|
66
|
+
*/
|
|
67
|
+
export function requestAccessibilityAccess(): boolean {
|
|
68
|
+
if (!macOS()) {
|
|
69
|
+
logger.info('[Accessibility] Not macOS, returning true');
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
logger.info('[Accessibility] Requesting accessibility access...');
|
|
74
|
+
askForAccessibilityAccess();
|
|
75
|
+
|
|
76
|
+
// Check the status after requesting
|
|
77
|
+
const status = getPermissionStatus('accessibility');
|
|
78
|
+
return status === 'granted';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if Microphone permission is granted
|
|
83
|
+
*/
|
|
84
|
+
export function getMicrophoneStatus(): PermissionStatus {
|
|
85
|
+
return getPermissionStatus('microphone');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Request Microphone permission
|
|
90
|
+
* Shows the system permission dialog if not determined
|
|
91
|
+
*/
|
|
92
|
+
export async function requestMicrophoneAccess(): Promise<boolean> {
|
|
93
|
+
if (!macOS()) {
|
|
94
|
+
logger.info('[Microphone] Not macOS, returning true');
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const currentStatus = getPermissionStatus('microphone');
|
|
99
|
+
logger.info(`[Microphone] Current status: ${currentStatus}`);
|
|
100
|
+
|
|
101
|
+
if (currentStatus === 'granted') {
|
|
102
|
+
logger.info('[Microphone] Already granted');
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (currentStatus === 'not-determined') {
|
|
107
|
+
logger.info('[Microphone] Status is not-determined, requesting access...');
|
|
108
|
+
try {
|
|
109
|
+
const result = await askForMicrophoneAccess();
|
|
110
|
+
logger.info(`[Microphone] askForMicrophoneAccess result: ${result}`);
|
|
111
|
+
return result === 'authorized';
|
|
112
|
+
} catch (error) {
|
|
113
|
+
logger.error('[Microphone] askForMicrophoneAccess failed:', error);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// If denied or restricted, open System Settings for manual enable
|
|
119
|
+
logger.info(`[Microphone] Status is ${currentStatus}, opening System Settings...`);
|
|
120
|
+
await shell.openExternal(
|
|
121
|
+
'x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone',
|
|
122
|
+
);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if Camera permission is granted
|
|
128
|
+
*/
|
|
129
|
+
export function getCameraStatus(): PermissionStatus {
|
|
130
|
+
return getPermissionStatus('camera');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Request Camera permission
|
|
135
|
+
* Shows the system permission dialog if not determined
|
|
136
|
+
*/
|
|
137
|
+
export async function requestCameraAccess(): Promise<boolean> {
|
|
138
|
+
if (!macOS()) {
|
|
139
|
+
logger.info('[Camera] Not macOS, returning true');
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const currentStatus = getPermissionStatus('camera');
|
|
144
|
+
logger.info(`[Camera] Current status: ${currentStatus}`);
|
|
145
|
+
|
|
146
|
+
if (currentStatus === 'granted') {
|
|
147
|
+
logger.info('[Camera] Already granted');
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (currentStatus === 'not-determined') {
|
|
152
|
+
logger.info('[Camera] Status is not-determined, requesting access...');
|
|
153
|
+
try {
|
|
154
|
+
const result = await askForCameraAccess();
|
|
155
|
+
logger.info(`[Camera] askForCameraAccess result: ${result}`);
|
|
156
|
+
return result === 'authorized';
|
|
157
|
+
} catch (error) {
|
|
158
|
+
logger.error('[Camera] askForCameraAccess failed:', error);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// If denied or restricted, open System Settings for manual enable
|
|
164
|
+
logger.info(`[Camera] Status is ${currentStatus}, opening System Settings...`);
|
|
165
|
+
await shell.openExternal(
|
|
166
|
+
'x-apple.systempreferences:com.apple.preference.security?Privacy_Camera',
|
|
167
|
+
);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Check if Screen Recording permission is granted
|
|
173
|
+
*/
|
|
174
|
+
export function getScreenCaptureStatus(): PermissionStatus {
|
|
175
|
+
return getPermissionStatus('screen');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Request Screen Recording permission
|
|
180
|
+
* Opens System Preferences if access not granted
|
|
181
|
+
* @param openPreferences - Whether to open System Preferences (default: true)
|
|
182
|
+
*/
|
|
183
|
+
export async function requestScreenCaptureAccess(openPreferences = true): Promise<boolean> {
|
|
184
|
+
if (!macOS()) {
|
|
185
|
+
logger.info('[Screen] Not macOS, returning true');
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const currentStatus = getPermissionStatus('screen');
|
|
190
|
+
logger.info(`[Screen] Current status: ${currentStatus}`);
|
|
191
|
+
|
|
192
|
+
if (currentStatus === 'granted') {
|
|
193
|
+
logger.info('[Screen] Already granted');
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Request screen capture access - this will prompt the user or open settings
|
|
198
|
+
logger.info('[Screen] Requesting screen capture access...');
|
|
199
|
+
askForScreenCaptureAccess(openPreferences);
|
|
200
|
+
|
|
201
|
+
// Check the status after requesting
|
|
202
|
+
const newStatus = getPermissionStatus('screen');
|
|
203
|
+
logger.info(`[Screen] Status after request: ${newStatus}`);
|
|
204
|
+
return newStatus === 'granted';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Check if Full Disk Access permission is granted
|
|
209
|
+
*/
|
|
210
|
+
export function getFullDiskAccessStatus(): PermissionStatus {
|
|
211
|
+
return getPermissionStatus('full-disk-access');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Request Full Disk Access permission
|
|
216
|
+
* Opens System Preferences to the Full Disk Access pane
|
|
217
|
+
* Note: Full Disk Access cannot be granted programmatically,
|
|
218
|
+
* user must manually add the app in System Settings
|
|
219
|
+
*/
|
|
220
|
+
export function requestFullDiskAccess(): void {
|
|
221
|
+
if (!macOS()) {
|
|
222
|
+
logger.info('[FullDiskAccess] Not macOS, skipping');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
logger.info('[FullDiskAccess] Opening Full Disk Access settings...');
|
|
227
|
+
askForFullDiskAccess();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Open Full Disk Access settings page in System Settings
|
|
232
|
+
* Alternative method using shell.openExternal
|
|
233
|
+
*/
|
|
234
|
+
export async function openFullDiskAccessSettings(): Promise<void> {
|
|
235
|
+
if (!macOS()) {
|
|
236
|
+
logger.info('[FullDiskAccess] Not macOS, skipping');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
logger.info('[FullDiskAccess] Opening Full Disk Access settings via shell...');
|
|
241
|
+
|
|
242
|
+
// On macOS 13+ (Ventura), System Preferences is replaced by System Settings,
|
|
243
|
+
// and deep links may differ. We try multiple known schemes for compatibility.
|
|
244
|
+
const candidates = [
|
|
245
|
+
// macOS 13+ (Ventura and later) - System Settings
|
|
246
|
+
'x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles',
|
|
247
|
+
// macOS 13+ alternative format
|
|
248
|
+
'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles',
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
for (const url of candidates) {
|
|
252
|
+
try {
|
|
253
|
+
logger.info(`[FullDiskAccess] Trying URL: ${url}`);
|
|
254
|
+
await shell.openExternal(url);
|
|
255
|
+
logger.info(`[FullDiskAccess] Successfully opened via ${url}`);
|
|
256
|
+
return;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
logger.warn(`[FullDiskAccess] Failed with URL ${url}:`, error);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Fallback: open Privacy & Security pane
|
|
263
|
+
try {
|
|
264
|
+
const fallbackUrl = 'x-apple.systempreferences:com.apple.preference.security?Privacy';
|
|
265
|
+
logger.info(`[FullDiskAccess] Trying fallback URL: ${fallbackUrl}`);
|
|
266
|
+
await shell.openExternal(fallbackUrl);
|
|
267
|
+
logger.info('[FullDiskAccess] Opened Privacy & Security settings as fallback');
|
|
268
|
+
} catch (error) {
|
|
269
|
+
logger.error('[FullDiskAccess] Failed to open any Privacy settings:', error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Check if Input Monitoring permission is granted
|
|
275
|
+
*/
|
|
276
|
+
export function getInputMonitoringStatus(): PermissionStatus {
|
|
277
|
+
return getPermissionStatus('input-monitoring');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get media access status (compatibility wrapper for Electron API)
|
|
282
|
+
* Maps 'microphone' and 'screen' to corresponding permission checks
|
|
283
|
+
*/
|
|
284
|
+
export function getMediaAccessStatus(mediaType: 'microphone' | 'screen'): string {
|
|
285
|
+
if (!macOS()) return 'granted';
|
|
286
|
+
|
|
287
|
+
const status = getPermissionStatus(mediaType === 'microphone' ? 'microphone' : 'screen');
|
|
288
|
+
|
|
289
|
+
// Map our status back to Electron's expected format
|
|
290
|
+
switch (status) {
|
|
291
|
+
case 'granted': {
|
|
292
|
+
return 'granted';
|
|
293
|
+
}
|
|
294
|
+
case 'not-determined': {
|
|
295
|
+
return 'not-determined';
|
|
296
|
+
}
|
|
297
|
+
case 'denied': {
|
|
298
|
+
return 'denied';
|
|
299
|
+
}
|
|
300
|
+
case 'restricted': {
|
|
301
|
+
return 'restricted';
|
|
302
|
+
}
|
|
303
|
+
default: {
|
|
304
|
+
return 'unknown';
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
package/changelog/v1.json
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"agentCronJobs.form.validation.nameRequired": "Task name is required",
|
|
26
26
|
"agentCronJobs.interval.12hours": "Every 12 hours",
|
|
27
27
|
"agentCronJobs.interval.1hour": "Every hour",
|
|
28
|
+
"agentCronJobs.interval.2hours": "Every 2 hours",
|
|
28
29
|
"agentCronJobs.interval.30min": "Every 30 minutes",
|
|
29
30
|
"agentCronJobs.interval.6hours": "Every 6 hours",
|
|
30
31
|
"agentCronJobs.interval.daily": "Daily",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"agentCronJobs.form.validation.nameRequired": "任务名称不能为空",
|
|
26
26
|
"agentCronJobs.interval.12hours": "每12小时",
|
|
27
27
|
"agentCronJobs.interval.1hour": "每小时",
|
|
28
|
+
"agentCronJobs.interval.2hours": "每2小时",
|
|
28
29
|
"agentCronJobs.interval.30min": "每30分钟",
|
|
29
30
|
"agentCronJobs.interval.6hours": "每6小时",
|
|
30
31
|
"agentCronJobs.interval.daily": "每日",
|
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.259",
|
|
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",
|