@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.
@@ -7,13 +7,20 @@ import { IpcHandler } from '@/utils/ipc/base';
7
7
 
8
8
  import SystemController from '../SystemCtr';
9
9
 
10
- const { ipcHandlers, ipcMainHandleMock, readdirSyncMock } = vi.hoisted(() => {
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 readdirSync = vi.fn();
16
- return { ipcHandlers: handlers, ipcMainHandleMock: handle, readdirSyncMock: readdirSync };
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: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
- }));
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
- const { systemPreferences } = await import('electron');
160
+ permissionsMock.getAuthStatus.mockReturnValue('authorized');
177
161
 
178
- await invokeIpc('system.requestAccessibilityAccess');
162
+ const result = await invokeIpc('system.requestAccessibilityAccess');
179
163
 
180
- expect(systemPreferences.isTrustedAccessibilityClient).toHaveBeenCalledWith(true);
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(systemPreferences.isTrustedAccessibilityClient).not.toHaveBeenCalled();
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
- const { systemPreferences } = await import('electron');
201
- vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('not-determined');
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(systemPreferences.getMediaAccessStatus).toHaveBeenCalledWith('microphone');
206
- expect(systemPreferences.askForMediaAccess).toHaveBeenCalledWith('microphone');
190
+ expect(permissionsMock.getAuthStatus).toHaveBeenCalledWith('microphone');
191
+ expect(permissionsMock.askForMicrophoneAccess).toHaveBeenCalled();
192
+ expect(result).toBe(true);
207
193
 
208
194
  // Reset
209
- vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('not-determined');
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, systemPreferences } = await import('electron');
214
- vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('granted');
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(systemPreferences.askForMediaAccess).not.toHaveBeenCalled();
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, systemPreferences } = await import('electron');
228
- vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('denied');
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(systemPreferences.askForMediaAccess).not.toHaveBeenCalled();
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
- vi.mocked(systemPreferences.getMediaAccessStatus).mockReturnValue('not-determined');
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, systemPreferences } = await import('electron');
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(systemPreferences.getMediaAccessStatus).not.toHaveBeenCalled();
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 use desktopCapturer and getDisplayMedia to trigger TCC and open System Settings on macOS', async () => {
260
- const { desktopCapturer, shell, systemPreferences } = await import('electron');
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(systemPreferences.getMediaAccessStatus).toHaveBeenCalledWith('screen');
265
- expect(desktopCapturer.getSources).toHaveBeenCalledWith({
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
- const { desktopCapturer, shell, systemPreferences } = await import('electron');
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(desktopCapturer.getSources).not.toHaveBeenCalled();
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(desktopCapturer.getSources).not.toHaveBeenCalled();
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 (can read protected directory)', async () => {
309
- readdirSyncMock.mockReturnValue(['file1', 'file2']);
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
- // On macOS 14 (Darwin 23), should check com.apple.stocks
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 (cannot read protected directory)', async () => {
321
- readdirSyncMock.mockImplementation(() => {
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
- readdirSyncMock.mockReturnValue(['file1', 'file2']);
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
- readdirSyncMock.mockImplementation(() => {
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
- readdirSyncMock.mockImplementation(() => {
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
+ }
@@ -30,6 +30,7 @@
30
30
  "src/main/**/*",
31
31
  "src/preload/**/*",
32
32
  "src/common/**/*",
33
- "electron-builder.js"
33
+ "electron-builder.js",
34
+ "native-deps.config.js"
34
35
  ]
35
36
  }
@@ -14,5 +14,6 @@ export default defineConfig({
14
14
  reportsDirectory: './coverage/app',
15
15
  },
16
16
  environment: 'node',
17
+ setupFiles: ['./src/main/__mocks__/setup.ts'],
17
18
  },
18
19
  });
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "features": [
5
+ "Update the cron patterns fields values."
6
+ ]
7
+ },
8
+ "date": "2026-01-10",
9
+ "version": "2.0.0-next.259"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "fixes": [
@@ -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.258",
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",