@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/apps/desktop/build/entitlements.mac.plist +9 -0
  3. package/apps/desktop/resources/locales/zh-CN/dialog.json +5 -1
  4. package/apps/desktop/resources/locales/zh-CN/menu.json +7 -0
  5. package/apps/desktop/src/main/controllers/SystemCtr.ts +186 -94
  6. package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +200 -31
  7. package/apps/desktop/src/main/core/browser/Browser.ts +9 -0
  8. package/apps/desktop/src/main/locales/default/dialog.ts +7 -2
  9. package/apps/desktop/src/main/locales/default/menu.ts +7 -0
  10. package/apps/desktop/src/main/menus/impls/macOS.ts +44 -1
  11. package/apps/desktop/src/main/utils/fullDiskAccess.ts +121 -0
  12. package/changelog/v1.json +14 -0
  13. package/package.json +1 -1
  14. package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +0 -2
  15. package/packages/database/migrations/meta/_journal.json +1 -1
  16. package/packages/database/src/models/__tests__/topics/topic.create.test.ts +37 -8
  17. package/packages/database/src/models/topic.ts +71 -4
  18. package/packages/database/src/schemas/agentCronJob.ts +1 -2
  19. package/packages/electron-client-ipc/src/events/system.ts +1 -0
  20. package/packages/memory-user-memory/src/extractors/context.ts +1 -4
  21. package/packages/memory-user-memory/src/extractors/experience.ts +2 -8
  22. package/packages/memory-user-memory/src/extractors/preference.ts +2 -8
  23. package/packages/memory-user-memory/src/prompts/gatekeeper.ts +123 -123
  24. package/packages/memory-user-memory/src/prompts/layers/context.ts +152 -152
  25. package/packages/memory-user-memory/src/prompts/layers/experience.ts +159 -159
  26. package/packages/memory-user-memory/src/prompts/layers/identity.ts +213 -213
  27. package/packages/memory-user-memory/src/prompts/layers/preference.ts +160 -160
  28. package/packages/memory-user-memory/src/services/extractExecutor.ts +33 -30
  29. package/packages/memory-user-memory/src/types.ts +10 -8
  30. package/packages/types/src/topic/topic.ts +9 -0
  31. package/src/app/[variants]/(desktop)/desktop-onboarding/features/PermissionsStep.tsx +16 -30
  32. package/src/app/[variants]/(desktop)/desktop-onboarding/index.tsx +19 -9
  33. package/src/app/[variants]/(desktop)/desktop-onboarding/storage.ts +49 -0
  34. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Body.tsx +4 -1
  35. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/CronTopicList/CronTopicGroup.tsx +74 -0
  36. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/CronTopicList/CronTopicItem.tsx +40 -0
  37. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/CronTopicList/index.tsx +140 -0
  38. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/index.tsx +1 -1
  39. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/TopicListContent/index.tsx +1 -1
  40. package/src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/index.tsx +1 -1
  41. package/src/app/[variants]/(main)/chat/cron/[cronId]/index.tsx +664 -0
  42. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/CronJobCards.tsx +160 -0
  43. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/CronJobForm.tsx +202 -0
  44. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/CronJobList.tsx +137 -0
  45. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/hooks/useAgentCronJobs.ts +138 -0
  46. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/index.tsx +130 -0
  47. package/src/app/[variants]/(main)/chat/profile/features/ProfileEditor/index.tsx +33 -3
  48. package/src/app/[variants]/router/desktopRouter.config.tsx +7 -0
  49. package/src/features/ChatInput/ActionBar/Params/Controls.tsx +7 -6
  50. package/src/hooks/useFetchCronTopics.ts +29 -0
  51. package/src/hooks/useFetchCronTopicsWithJobInfo.ts +56 -0
  52. package/src/hooks/useFetchTopics.ts +4 -1
  53. package/src/locales/default/setting.ts +44 -1
  54. package/src/server/routers/lambda/agentCronJob.ts +367 -0
  55. package/src/server/routers/lambda/image/index.test.ts +2 -2
  56. package/src/server/routers/lambda/index.ts +2 -0
  57. package/src/server/routers/lambda/topic.ts +15 -3
  58. package/src/server/services/aiAgent/index.ts +18 -1
  59. package/src/server/services/memory/userMemory/extract.ts +14 -6
  60. package/src/services/agentCronJob.ts +95 -0
  61. package/src/services/topic/index.ts +1 -0
  62. package/src/store/chat/slices/topic/action.ts +53 -2
  63. package/src/store/chat/slices/topic/initialState.ts +1 -0
  64. package/src/store/chat/slices/topic/selectors.ts +14 -6
  65. package/src/tools/placeholders.ts +1 -4
  66. package/apps/desktop/src/main/controllers/scripts/full-disk-access.applescript +0 -85
package/CHANGELOG.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.253](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.252...v2.0.0-next.253)
6
+
7
+ <sup>Released on **2026-01-09**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **desktop**: Improve macOS permission requests and Full Disk Access detection.
12
+
13
+ #### 🐛 Bug Fixes
14
+
15
+ - **controls**: Update checkbox toggle behavior and pass value to ParamControlWrapper.
16
+
17
+ <br/>
18
+
19
+ <details>
20
+ <summary><kbd>Improvements and Fixes</kbd></summary>
21
+
22
+ #### What's improved
23
+
24
+ - **desktop**: Improve macOS permission requests and Full Disk Access detection, closes [#11380](https://github.com/lobehub/lobe-chat/issues/11380) ([2d5868f](https://github.com/lobehub/lobe-chat/commit/2d5868f))
25
+
26
+ #### What's fixed
27
+
28
+ - **controls**: Update checkbox toggle behavior and pass value to ParamControlWrapper, closes [#11363](https://github.com/lobehub/lobe-chat/issues/11363) ([1f1ef94](https://github.com/lobehub/lobe-chat/commit/1f1ef94))
29
+
30
+ </details>
31
+
32
+ <div align="right">
33
+
34
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
35
+
36
+ </div>
37
+
38
+ ## [Version 2.0.0-next.252](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.251...v2.0.0-next.252)
39
+
40
+ <sup>Released on **2026-01-09**</sup>
41
+
42
+ #### ✨ Features
43
+
44
+ - **misc**: Add the agent cron job.
45
+
46
+ <br/>
47
+
48
+ <details>
49
+ <summary><kbd>Improvements and Fixes</kbd></summary>
50
+
51
+ #### What's improved
52
+
53
+ - **misc**: Add the agent cron job, closes [#11370](https://github.com/lobehub/lobe-chat/issues/11370) ([10e47d9](https://github.com/lobehub/lobe-chat/commit/10e47d9))
54
+
55
+ </details>
56
+
57
+ <div align="right">
58
+
59
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
60
+
61
+ </div>
62
+
5
63
  ## [Version 2.0.0-next.251](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.250...v2.0.0-next.251)
6
64
 
7
65
  <sup>Released on **2026-01-09**</sup>
@@ -2,11 +2,20 @@
2
2
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
3
  <plist version="1.0">
4
4
  <dict>
5
+ <!-- Hardened Runtime exceptions for Electron -->
5
6
  <key>com.apple.security.cs.allow-jit</key>
6
7
  <true/>
7
8
  <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8
9
  <true/>
9
10
  <key>com.apple.security.cs.allow-dyld-environment-variables</key>
10
11
  <true/>
12
+
13
+ <!-- Microphone access for voice interactions -->
14
+ <key>com.apple.security.device.audio-input</key>
15
+ <true/>
16
+
17
+ <!-- Camera access (for future video features) -->
18
+ <key>com.apple.security.device.camera</key>
19
+ <true/>
11
20
  </dict>
12
21
  </plist>
@@ -11,6 +11,10 @@
11
11
  "error.detail": "操作未完成。你可以重试,或稍后再试。",
12
12
  "error.message": "发生错误",
13
13
  "error.title": "错误",
14
+ "fullDiskAccess.message": "LobeHub 需要完全磁盘访问权限来读取文件并启用知识库功能。请在系统设置中授予权限。",
15
+ "fullDiskAccess.openSettings": "打开设置",
16
+ "fullDiskAccess.skip": "稍后",
17
+ "fullDiskAccess.title": "需要完全磁盘访问权限",
14
18
  "update.checkingUpdate": "检查新版本",
15
19
  "update.checkingUpdateDesc": "正在获取版本信息…",
16
20
  "update.downloadAndInstall": "下载并安装",
@@ -41,4 +45,4 @@
41
45
  "waitingOAuth.helpText": "如果浏览器没有自动打开,请点击取消后重新尝试",
42
46
  "waitingOAuth.retry": "重试",
43
47
  "waitingOAuth.title": "等待授权连接"
44
- }
48
+ }
@@ -7,6 +7,13 @@
7
7
  "dev.openStore": "打开本地数据目录",
8
8
  "dev.openUpdaterCacheDir": "更新缓存目录",
9
9
  "dev.openUserDataDir": "用户配置目录",
10
+ "dev.permissions.accessibility.request": "请求辅助功能权限",
11
+ "dev.permissions.fullDisk.open": "打开「完全磁盘访问」设置",
12
+ "dev.permissions.fullDisk.request": "请求完全磁盘访问权限",
13
+ "dev.permissions.microphone.request": "请求麦克风权限",
14
+ "dev.permissions.notification.request": "请求通知权限",
15
+ "dev.permissions.screen.request": "请求屏幕录制权限",
16
+ "dev.permissions.title": "权限",
10
17
  "dev.refreshMenu": "刷新菜单",
11
18
  "dev.reload": "重新加载",
12
19
  "dev.simulateAutoDownload": "模拟启动后台自动下载更新(3s 下完)",
@@ -1,14 +1,12 @@
1
1
  import { ElectronAppState, ThemeMode } from '@lobechat/electron-client-ipc';
2
- import { app, dialog, nativeTheme, shell, systemPreferences } from 'electron';
2
+ import { app, desktopCapturer, dialog, nativeTheme, shell, systemPreferences } from 'electron';
3
3
  import { macOS } from 'electron-is';
4
- import { spawn } from 'node:child_process';
5
- import path from 'node:path';
6
4
  import process from 'node:process';
7
5
 
6
+ import { checkFullDiskAccess, openFullDiskAccessSettings } from '@/utils/fullDiskAccess';
8
7
  import { createLogger } from '@/utils/logger';
9
8
 
10
9
  import { ControllerModule, IpcMethod } from './index';
11
- import fullDiskAccessAutoAddScript from './scripts/full-disk-access.applescript?raw';
12
10
 
13
11
  const logger = createLogger('controllers:SystemCtr');
14
12
 
@@ -57,14 +55,98 @@ export default class SystemController extends ControllerModule {
57
55
 
58
56
  @IpcMethod()
59
57
  requestAccessibilityAccess() {
60
- if (!macOS()) return true;
61
- return systemPreferences.isTrustedAccessibilityClient(true);
58
+ if (!macOS()) {
59
+ logger.info('[Accessibility] Not macOS, returning true');
60
+ return true;
61
+ }
62
+ logger.info('[Accessibility] Requesting accessibility access (will prompt if not granted)...');
63
+ // Pass true to prompt user if not already trusted
64
+ const result = systemPreferences.isTrustedAccessibilityClient(true);
65
+ logger.info(`[Accessibility] isTrustedAccessibilityClient(true) returned: ${result}`);
66
+ return result;
62
67
  }
63
68
 
64
69
  @IpcMethod()
65
70
  getAccessibilityStatus() {
66
- if (!macOS()) return true;
67
- return systemPreferences.isTrustedAccessibilityClient(false);
71
+ if (!macOS()) {
72
+ logger.info('[Accessibility] Not macOS, returning true');
73
+ return true;
74
+ }
75
+ // Pass false to just check without prompting
76
+ const status = systemPreferences.isTrustedAccessibilityClient(false);
77
+ logger.info(`[Accessibility] Current status: ${status}`);
78
+ return status;
79
+ }
80
+
81
+ /**
82
+ * Check if Full Disk Access is granted.
83
+ * This works by attempting to read a protected system directory.
84
+ * Calling this also registers the app in the TCC database, making it appear
85
+ * in System Settings > Privacy & Security > Full Disk Access.
86
+ */
87
+ @IpcMethod()
88
+ getFullDiskAccessStatus(): boolean {
89
+ return checkFullDiskAccess();
90
+ }
91
+
92
+ /**
93
+ * Prompt the user with a native dialog if Full Disk Access is not granted.
94
+ * Based on https://github.com/inket/FullDiskAccess
95
+ *
96
+ * @param options - Dialog options
97
+ * @returns 'granted' if already granted, 'opened_settings' if user chose to open settings,
98
+ * 'skipped' if user chose to skip, 'cancelled' if dialog was cancelled
99
+ */
100
+ @IpcMethod()
101
+ async promptFullDiskAccessIfNotGranted(options?: {
102
+ message?: string;
103
+ openSettingsButtonText?: string;
104
+ skipButtonText?: string;
105
+ title?: string;
106
+ }): Promise<'cancelled' | 'granted' | 'opened_settings' | 'skipped'> {
107
+ // Check if already granted
108
+ if (checkFullDiskAccess()) {
109
+ logger.info('[FullDiskAccess] Already granted, skipping prompt');
110
+
111
+ return 'granted';
112
+ }
113
+
114
+ if (!macOS()) {
115
+ logger.info('[FullDiskAccess] Not macOS, returning granted');
116
+ return 'granted';
117
+ }
118
+
119
+ const mainWindow = this.app.browserManager.getMainWindow()?.browserWindow;
120
+
121
+ // Get localized strings
122
+ const t = this.app.i18n.ns('dialog');
123
+ const title = options?.title || t('fullDiskAccess.title');
124
+ const message = options?.message || t('fullDiskAccess.message');
125
+ const openSettingsButtonText =
126
+ options?.openSettingsButtonText || t('fullDiskAccess.openSettings');
127
+ const skipButtonText = options?.skipButtonText || t('fullDiskAccess.skip');
128
+
129
+ logger.info('[FullDiskAccess] Showing native prompt dialog');
130
+
131
+ const result = await dialog.showMessageBox(mainWindow!, {
132
+ buttons: [openSettingsButtonText, skipButtonText],
133
+ cancelId: 1,
134
+ defaultId: 0,
135
+ message: message,
136
+ title: title,
137
+ type: 'info',
138
+ });
139
+
140
+ if (result.response === 0) {
141
+ // User chose to open settings
142
+ logger.info('[FullDiskAccess] User chose to open settings');
143
+ await this.openFullDiskAccessSettings();
144
+ return 'opened_settings';
145
+ } else {
146
+ // User chose to skip or cancelled
147
+ logger.info('[FullDiskAccess] User chose to skip');
148
+ return 'skipped';
149
+ }
68
150
  }
69
151
 
70
152
  @IpcMethod()
@@ -75,119 +157,129 @@ export default class SystemController extends ControllerModule {
75
157
 
76
158
  @IpcMethod()
77
159
  async requestMicrophoneAccess(): Promise<boolean> {
78
- if (!macOS()) return true;
79
- return systemPreferences.askForMediaAccess('microphone');
160
+ if (!macOS()) {
161
+ logger.info('[Microphone] Not macOS, returning true');
162
+ return true;
163
+ }
164
+
165
+ const status = systemPreferences.getMediaAccessStatus('microphone');
166
+ logger.info(`[Microphone] Current status: ${status}`);
167
+
168
+ // Only ask for access if status is 'not-determined'
169
+ // If already denied/restricted, the system won't show a prompt
170
+ if (status === 'not-determined') {
171
+ logger.info('[Microphone] Status is not-determined, calling askForMediaAccess...');
172
+ try {
173
+ const result = await systemPreferences.askForMediaAccess('microphone');
174
+ logger.info(`[Microphone] askForMediaAccess result: ${result}`);
175
+ return result;
176
+ } catch (error) {
177
+ logger.error('[Microphone] askForMediaAccess failed:', error);
178
+ return false;
179
+ }
180
+ }
181
+
182
+ if (status === 'granted') {
183
+ logger.info('[Microphone] Already granted');
184
+ return true;
185
+ }
186
+
187
+ // If denied or restricted, open System Settings for manual enable
188
+ logger.info(`[Microphone] Status is ${status}, opening System Settings...`);
189
+ await shell.openExternal(
190
+ 'x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone',
191
+ );
192
+
193
+ return false;
80
194
  }
81
195
 
82
196
  @IpcMethod()
83
197
  async requestScreenAccess(): Promise<boolean> {
84
- if (!macOS()) return true;
198
+ if (!macOS()) {
199
+ logger.info('[Screen] Not macOS, returning true');
200
+ return true;
201
+ }
202
+
203
+ const status = systemPreferences.getMediaAccessStatus('screen');
204
+ logger.info(`[Screen] Current status: ${status}`);
205
+
206
+ // If already granted, no need to do anything
207
+ if (status === 'granted') {
208
+ logger.info('[Screen] Already granted');
209
+ return true;
210
+ }
85
211
 
86
212
  // IMPORTANT:
87
213
  // On macOS, the app may NOT appear in "Screen Recording" list until it actually
88
214
  // requests the permission once (TCC needs to register this app).
89
- // So we try to proactively request it first, then open System Settings for manual toggle.
90
- // 1) Best-effort: try Electron runtime API if available (not typed in Electron 38).
215
+ // We use multiple approaches to ensure TCC registration:
216
+ // 1. desktopCapturer.getSources() in main process
217
+ // 2. getDisplayMedia() in renderer as fallback
218
+
219
+ // Approach 1: Use desktopCapturer in main process
220
+ logger.info('[Screen] Attempting TCC registration via desktopCapturer.getSources...');
91
221
  try {
92
- const status = systemPreferences.getMediaAccessStatus('screen');
93
- if (status !== 'granted') {
94
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
95
- await (systemPreferences as any).askForMediaAccess?.('screen');
96
- }
222
+ // Using a reasonable thumbnail size and both types to ensure TCC registration
223
+ const sources = await desktopCapturer.getSources({
224
+ fetchWindowIcons: true,
225
+ thumbnailSize: { height: 144, width: 256 },
226
+ types: ['screen', 'window'],
227
+ });
228
+ // Access the sources to ensure the capture actually happens
229
+ logger.info(`[Screen] desktopCapturer.getSources returned ${sources.length} sources`);
97
230
  } catch (error) {
98
- logger.warn('Failed to request screen recording access via systemPreferences', error);
231
+ logger.warn('[Screen] desktopCapturer.getSources failed:', error);
99
232
  }
100
233
 
101
- // 2) Reliable trigger: run a one-shot getDisplayMedia in renderer to register TCC entry.
102
- // This will show the OS capture picker; once the user selects/cancels, we stop tracks immediately.
234
+ // Approach 2: Trigger getDisplayMedia in renderer as additional attempt
235
+ // This shows the OS capture picker which definitely registers with TCC
236
+ logger.info('[Screen] Attempting TCC registration via getDisplayMedia in renderer...');
103
237
  try {
104
- const status = systemPreferences.getMediaAccessStatus('screen');
105
- if (status !== 'granted') {
106
- const mainWindow = this.app.browserManager.getMainWindow()?.browserWindow;
107
- if (mainWindow && !mainWindow.isDestroyed()) {
108
- const script = `
109
- (() => {
110
- const stop = (stream) => {
111
- try { stream.getTracks().forEach((t) => t.stop()); } catch {}
112
- };
113
- return navigator.mediaDevices.getDisplayMedia({ video: true, audio: false })
114
- .then((stream) => { stop(stream); return true; })
115
- .catch(() => false);
238
+ const mainWindow = this.app.browserManager.getMainWindow()?.browserWindow;
239
+ if (mainWindow && !mainWindow.isDestroyed()) {
240
+ const script = `
241
+ (async () => {
242
+ try {
243
+ const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });
244
+ stream.getTracks().forEach(t => t.stop());
245
+ return true;
246
+ } catch (e) {
247
+ console.error('[Screen] getDisplayMedia error:', e);
248
+ return false;
249
+ }
116
250
  })()
117
- `.trim();
251
+ `.trim();
118
252
 
119
- await mainWindow.webContents.executeJavaScript(script, true);
120
- }
253
+ const result = await mainWindow.webContents.executeJavaScript(script, true);
254
+ logger.info(`[Screen] getDisplayMedia result: ${result}`);
255
+ } else {
256
+ logger.warn('[Screen] Main window not available for getDisplayMedia');
121
257
  }
122
258
  } catch (error) {
123
- logger.warn('Failed to request screen recording access via getDisplayMedia', error);
259
+ logger.warn('[Screen] getDisplayMedia failed:', error);
124
260
  }
125
261
 
262
+ // Check status after attempts
263
+ const newStatus = systemPreferences.getMediaAccessStatus('screen');
264
+ logger.info(`[Screen] Status after TCC attempts: ${newStatus}`);
265
+
266
+ // Open System Settings for user to manually enable screen recording
267
+ logger.info('[Screen] Opening System Settings for Screen Recording...');
126
268
  await shell.openExternal(
127
269
  'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture',
128
270
  );
129
271
 
130
- return systemPreferences.getMediaAccessStatus('screen') === 'granted';
131
- }
132
-
133
- @IpcMethod()
134
- openFullDiskAccessSettings(payload?: { autoAdd?: boolean }) {
135
- if (!macOS()) return;
136
- const { autoAdd = false } = payload || {};
137
-
138
- // NOTE:
139
- // - Full Disk Access cannot be requested programmatically like microphone/screen.
140
- // - On macOS 13+ (Ventura), System Preferences is replaced by System Settings,
141
- // and deep links may differ. We try multiple known schemes for compatibility.
142
- const candidates = [
143
- // macOS 13+ (System Settings)
144
- 'com.apple.settings:Privacy&path=FullDiskAccess',
145
- // Older macOS (System Preferences)
146
- 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles',
147
- ];
148
- if (autoAdd) this.tryAutoAddFullDiskAccess();
149
-
150
- (async () => {
151
- for (const url of candidates) {
152
- try {
153
- await shell.openExternal(url);
154
- return;
155
- } catch (error) {
156
- logger.warn(`Failed to open Full Disk Access settings via ${url}`, error);
157
- }
158
- }
159
- })();
272
+ const finalStatus = systemPreferences.getMediaAccessStatus('screen');
273
+ logger.info(`[Screen] Final status: ${finalStatus}`);
274
+ return finalStatus === 'granted';
160
275
  }
161
276
 
162
277
  /**
163
- * Best-effort UI automation to add this app into Full Disk Access list.
164
- *
165
- * Limitations:
166
- * - This uses AppleScript UI scripting (System Events) and may require the user to grant
167
- * additional "Automation" permission (to control System Settings).
168
- * - UI structure differs across macOS versions/languages; we fall back silently.
278
+ * Open Full Disk Access settings page
169
279
  */
170
- private tryAutoAddFullDiskAccess() {
171
- if (!macOS()) return;
172
-
173
- const exePath = app.getPath('exe');
174
- // /Applications/App.app/Contents/MacOS/App -> /Applications/App.app
175
- const appBundlePath = path.resolve(path.dirname(exePath), '..', '..');
176
-
177
- // Keep the script minimal and resilient; failure should not break onboarding flow.
178
- const script = fullDiskAccessAutoAddScript.trim();
179
-
180
- try {
181
- const child = spawn('osascript', ['-e', script, appBundlePath], { env: process.env });
182
- child.on('error', (error) => {
183
- logger.warn('Full Disk Access auto-add (osascript) failed to start', error);
184
- });
185
- child.on('exit', (code) => {
186
- logger.debug('Full Disk Access auto-add (osascript) exited', { code });
187
- });
188
- } catch (error) {
189
- logger.warn('Full Disk Access auto-add failed', error);
190
- }
280
+ @IpcMethod()
281
+ async openFullDiskAccessSettings() {
282
+ return openFullDiskAccessSettings();
191
283
  }
192
284
 
193
285
  @IpcMethod()