@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
|
@@ -43,7 +43,7 @@ export const agentCronJobs = pgTable(
|
|
|
43
43
|
|
|
44
44
|
// Core configuration
|
|
45
45
|
enabled: boolean('enabled').default(true),
|
|
46
|
-
cronPattern: text('cron_pattern').notNull(), // e.g., "
|
|
46
|
+
cronPattern: text('cron_pattern').notNull(), // e.g., "*/30 * * * *" (every 30 minutes)
|
|
47
47
|
timezone: text('timezone').default('UTC'),
|
|
48
48
|
|
|
49
49
|
// Content fields
|
|
@@ -82,27 +82,61 @@ export const cronPatternSchema = z
|
|
|
82
82
|
'Invalid cron pattern',
|
|
83
83
|
);
|
|
84
84
|
|
|
85
|
-
// Minimum 30 minutes validation
|
|
85
|
+
// Minimum 30 minutes validation (using standard cron format)
|
|
86
86
|
export const minimumIntervalSchema = z.string().refine((pattern) => {
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
'0
|
|
91
|
-
'0
|
|
92
|
-
'0
|
|
93
|
-
'0
|
|
94
|
-
'0
|
|
95
|
-
'0
|
|
96
|
-
'0
|
|
87
|
+
// Standard cron format: minute hour day month weekday
|
|
88
|
+
const allowedPatterns = [
|
|
89
|
+
'*/30 * * * *', // Every 30 minutes
|
|
90
|
+
'0 * * * *', // Every hour
|
|
91
|
+
'0 */2 * * *', // Every 2 hours
|
|
92
|
+
'0 */3 * * *', // Every 3 hours
|
|
93
|
+
'0 */4 * * *', // Every 4 hours
|
|
94
|
+
'0 */6 * * *', // Every 6 hours
|
|
95
|
+
'0 */8 * * *', // Every 8 hours
|
|
96
|
+
'0 */12 * * *', // Every 12 hours
|
|
97
|
+
'0 0 * * *', // Daily at midnight
|
|
98
|
+
'0 0 * * 0', // Weekly on Sunday
|
|
99
|
+
'0 0 1 * *', // Monthly on 1st
|
|
97
100
|
];
|
|
98
101
|
|
|
99
|
-
// Check if it matches allowed patterns
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
// Check if it matches allowed patterns
|
|
103
|
+
if (allowedPatterns.includes(pattern)) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Parse pattern to validate minimum 30-minute interval
|
|
108
|
+
const parts = pattern.split(' ');
|
|
109
|
+
if (parts.length !== 5) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const [minute, hour] = parts;
|
|
114
|
+
|
|
115
|
+
// Allow minute intervals >= 30 (e.g., */30, */45, */60)
|
|
116
|
+
if (minute.startsWith('*/')) {
|
|
117
|
+
const interval = parseInt(minute.slice(2));
|
|
118
|
+
if (!isNaN(interval) && interval >= 30) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Allow hourly patterns: 0 */N * * * where N >= 1
|
|
124
|
+
if (minute === '0' && hour.startsWith('*/')) {
|
|
125
|
+
const interval = parseInt(hour.slice(2));
|
|
126
|
+
if (!isNaN(interval) && interval >= 1) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Allow specific hour patterns: 0 N * * * (runs once per day)
|
|
132
|
+
if (minute === '0' && /^\d+$/.test(hour)) {
|
|
133
|
+
const h = parseInt(hour);
|
|
134
|
+
if (!isNaN(h) && h >= 0 && h <= 23) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return false;
|
|
106
140
|
}, 'Minimum execution interval is 30 minutes');
|
|
107
141
|
|
|
108
142
|
export const executionConditionsSchema = z
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
"test:coverage": "vitest --coverage --silent='passed-only'"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@napi-rs/canvas": "^0.1.70",
|
|
29
28
|
"@xmldom/xmldom": "^0.9.8",
|
|
30
29
|
"concat-stream": "^2.0.0",
|
|
31
30
|
"debug": "^4.4.3",
|
|
@@ -37,6 +36,7 @@
|
|
|
37
36
|
"yauzl": "^3.2.0"
|
|
38
37
|
},
|
|
39
38
|
"devDependencies": {
|
|
39
|
+
"@napi-rs/canvas": "^0.1.70",
|
|
40
40
|
"@types/concat-stream": "^2.0.3",
|
|
41
41
|
"@types/yauzl": "^2.10.3",
|
|
42
42
|
"typescript": "^5.9.3"
|
|
@@ -20,6 +20,31 @@ const foldersToSymlink = [
|
|
|
20
20
|
|
|
21
21
|
const foldersToCopy = ['src', 'scripts'];
|
|
22
22
|
|
|
23
|
+
// Assets to remove from desktop build output (not needed for Electron app)
|
|
24
|
+
const assetsToRemove = [
|
|
25
|
+
// Icons & favicons
|
|
26
|
+
'apple-touch-icon.png',
|
|
27
|
+
'favicon.ico',
|
|
28
|
+
'favicon-32x32.ico',
|
|
29
|
+
'favicon-16x16.png',
|
|
30
|
+
'favicon-32x32.png',
|
|
31
|
+
|
|
32
|
+
// SEO & sitemap
|
|
33
|
+
'sitemap.xml',
|
|
34
|
+
'sitemap-index.xml',
|
|
35
|
+
'sitemap',
|
|
36
|
+
'robots.txt',
|
|
37
|
+
|
|
38
|
+
// Incompatible pages
|
|
39
|
+
'not-compatible.html',
|
|
40
|
+
'not-compatible',
|
|
41
|
+
|
|
42
|
+
// Large media assets
|
|
43
|
+
'videos',
|
|
44
|
+
'screenshots',
|
|
45
|
+
'og',
|
|
46
|
+
];
|
|
47
|
+
|
|
23
48
|
const filesToCopy = [
|
|
24
49
|
'package.json',
|
|
25
50
|
'tsconfig.json',
|
|
@@ -85,10 +110,13 @@ const build = async () => {
|
|
|
85
110
|
|
|
86
111
|
console.log('🏗 Running next build in shadow workspace...');
|
|
87
112
|
try {
|
|
88
|
-
execSync('next build
|
|
113
|
+
execSync('next build', {
|
|
89
114
|
cwd: TEMP_DIR,
|
|
90
115
|
env: {
|
|
91
116
|
...process.env,
|
|
117
|
+
// Pass PROJECT_ROOT to next.config.ts for outputFileTracingRoot
|
|
118
|
+
// This fixes Turbopack symlink resolution when building in shadow workspace
|
|
119
|
+
ELECTRON_BUILD_PROJECT_ROOT: PROJECT_ROOT,
|
|
92
120
|
NODE_OPTIONS: process.env.NODE_OPTIONS || '--max-old-space-size=8192',
|
|
93
121
|
},
|
|
94
122
|
stdio: 'inherit',
|
|
@@ -106,6 +134,16 @@ const build = async () => {
|
|
|
106
134
|
if (fs.existsSync(sourceOutDir)) {
|
|
107
135
|
console.log('📦 Moving "out" directory...');
|
|
108
136
|
await fs.move(sourceOutDir, targetOutDir);
|
|
137
|
+
|
|
138
|
+
// Remove unnecessary assets from desktop build
|
|
139
|
+
console.log('🗑️ Removing unnecessary assets...');
|
|
140
|
+
for (const asset of assetsToRemove) {
|
|
141
|
+
const assetPath = path.join(targetOutDir, asset);
|
|
142
|
+
if (fs.existsSync(assetPath)) {
|
|
143
|
+
await fs.remove(assetPath);
|
|
144
|
+
console.log(` Removed: ${asset}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
109
147
|
} else {
|
|
110
148
|
console.warn("⚠️ 'out' directory not found. Using '.next' instead (fallback)?");
|
|
111
149
|
const sourceNextDir = path.join(TEMP_DIR, '.next');
|
|
@@ -144,6 +144,32 @@ export const modifyNextConfig = async (TEMP_DIR: string) => {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
// 6. Inject outputFileTracingRoot to fix symlink resolution for Turbopack
|
|
148
|
+
// When building in shadow workspace (TEMP_DIR), symlinks (e.g., node_modules) point to PROJECT_ROOT
|
|
149
|
+
// Turbopack's root defaults to TEMP_DIR, causing strip_prefix to fail for paths outside TEMP_DIR
|
|
150
|
+
// Setting outputFileTracingRoot to PROJECT_ROOT allows Turbopack to correctly resolve these symlinks
|
|
151
|
+
// We use ELECTRON_BUILD_PROJECT_ROOT env var which is set by buildNextApp.mts
|
|
152
|
+
const outputFileTracingRootPair = nextConfigDecl.find({
|
|
153
|
+
rule: {
|
|
154
|
+
pattern: 'outputFileTracingRoot: $A',
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
if (!outputFileTracingRootPair) {
|
|
158
|
+
const objectNode = nextConfigDecl.find({
|
|
159
|
+
rule: { kind: 'object' },
|
|
160
|
+
});
|
|
161
|
+
if (objectNode) {
|
|
162
|
+
const range = objectNode.range();
|
|
163
|
+
// Insert outputFileTracingRoot that reads from env var at build time
|
|
164
|
+
// Falls back to current directory if not in electron build context
|
|
165
|
+
edits.push({
|
|
166
|
+
end: range.start.index + 1,
|
|
167
|
+
start: range.start.index + 1,
|
|
168
|
+
text: "\n outputFileTracingRoot: process.env.ELECTRON_BUILD_PROJECT_ROOT || process.cwd(),",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
147
173
|
// Remove withPWA wrapper
|
|
148
174
|
const withPWA = root.find({
|
|
149
175
|
rule: {
|
|
@@ -85,13 +85,15 @@ const AutoSaveHintSlot = memo(() => {
|
|
|
85
85
|
return <AutoSaveHint lastUpdatedTime={lastUpdatedTime} saveStatus={status} />;
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
// Standard cron format: minute hour day month weekday
|
|
88
89
|
const CRON_PATTERNS = [
|
|
89
|
-
{ label: 'agentCronJobs.interval.30min', value: '
|
|
90
|
-
{ label: 'agentCronJobs.interval.1hour', value: '0
|
|
90
|
+
{ label: 'agentCronJobs.interval.30min', value: '*/30 * * * *' },
|
|
91
|
+
{ label: 'agentCronJobs.interval.1hour', value: '0 * * * *' },
|
|
92
|
+
{ label: 'agentCronJobs.interval.2hours', value: '0 */2 * * *' },
|
|
91
93
|
{ label: 'agentCronJobs.interval.6hours', value: '0 */6 * * *' },
|
|
92
94
|
{ label: 'agentCronJobs.interval.12hours', value: '0 */12 * * *' },
|
|
93
|
-
{ label: 'agentCronJobs.interval.daily', value: '0 0
|
|
94
|
-
{ label: 'agentCronJobs.interval.weekly', value: '0 0
|
|
95
|
+
{ label: 'agentCronJobs.interval.daily', value: '0 0 * * *' },
|
|
96
|
+
{ label: 'agentCronJobs.interval.weekly', value: '0 0 * * 0' },
|
|
95
97
|
];
|
|
96
98
|
|
|
97
99
|
const WEEKDAY_OPTIONS = [
|
|
@@ -115,13 +117,15 @@ const WEEKDAY_LABELS: Record<number, string> = {
|
|
|
115
117
|
};
|
|
116
118
|
|
|
117
119
|
const getIntervalText = (cronPattern: string) => {
|
|
120
|
+
// Standard cron format mapping
|
|
118
121
|
const intervalMap: Record<string, string> = {
|
|
122
|
+
'*/30 * * * *': 'agentCronJobs.interval.30min',
|
|
123
|
+
'0 * * * *': 'agentCronJobs.interval.1hour',
|
|
119
124
|
'0 */12 * * *': 'agentCronJobs.interval.12hours',
|
|
120
|
-
'0 */
|
|
125
|
+
'0 */2 * * *': 'agentCronJobs.interval.2hours',
|
|
121
126
|
'0 */6 * * *': 'agentCronJobs.interval.6hours',
|
|
122
|
-
'0 0 * * *': 'agentCronJobs.interval.
|
|
123
|
-
'0 0
|
|
124
|
-
'0 0 0 * 0': 'agentCronJobs.interval.weekly',
|
|
127
|
+
'0 0 * * *': 'agentCronJobs.interval.daily',
|
|
128
|
+
'0 0 * * 0': 'agentCronJobs.interval.weekly',
|
|
125
129
|
};
|
|
126
130
|
|
|
127
131
|
return intervalMap[cronPattern] || cronPattern;
|
|
@@ -26,13 +26,15 @@ interface CronJobFormProps {
|
|
|
26
26
|
onSubmit: (data: CronJobFormData) => void;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Standard cron format: minute hour day month weekday
|
|
29
30
|
const CRON_PATTERNS = [
|
|
30
|
-
{ label: 'agentCronJobs.interval.30min', value: '
|
|
31
|
-
{ label: 'agentCronJobs.interval.1hour', value: '0
|
|
32
|
-
{ label: 'agentCronJobs.interval.
|
|
33
|
-
{ label: 'agentCronJobs.interval.
|
|
34
|
-
{ label: 'agentCronJobs.interval.
|
|
35
|
-
{ label: 'agentCronJobs.interval.
|
|
31
|
+
{ label: 'agentCronJobs.interval.30min', value: '*/30 * * * *' }, // Every 30 minutes
|
|
32
|
+
{ label: 'agentCronJobs.interval.1hour', value: '0 * * * *' }, // Every hour
|
|
33
|
+
{ label: 'agentCronJobs.interval.2hours', value: '0 */2 * * *' }, // Every 2 hours
|
|
34
|
+
{ label: 'agentCronJobs.interval.6hours', value: '0 */6 * * *' }, // Every 6 hours
|
|
35
|
+
{ label: 'agentCronJobs.interval.12hours', value: '0 */12 * * *' }, // Every 12 hours
|
|
36
|
+
{ label: 'agentCronJobs.interval.daily', value: '0 0 * * *' }, // Daily at midnight
|
|
37
|
+
{ label: 'agentCronJobs.interval.weekly', value: '0 0 * * 0' }, // Weekly on Sunday
|
|
36
38
|
];
|
|
37
39
|
|
|
38
40
|
const WEEKDAY_OPTIONS = [
|
|
@@ -120,7 +122,7 @@ const CronJobForm = memo<CronJobFormProps>(({ editingJob, formRef, onSubmit }) =
|
|
|
120
122
|
<Form
|
|
121
123
|
form={form}
|
|
122
124
|
initialValues={{
|
|
123
|
-
cronPattern: '
|
|
125
|
+
cronPattern: '*/30 * * * *', // Default to every 30 minutes
|
|
124
126
|
weekdays: [],
|
|
125
127
|
}}
|
|
126
128
|
layout="vertical"
|
|
@@ -1,121 +0,0 @@
|
|
|
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
|
-
}
|