diff --git a/README.md b/README.md index 0e7d38b..e71948f 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,16 @@ curl -X POST "http://localhost:30000/openapi/capcut-mate/v1/add_videos" \ 剪映小助手客户端提供了桌面端的便捷操作界面,以下是启动方法: +### macOS 沙箱权限说明 + +在 macOS 上运行时,应用可能会请求访问特定文件夹的权限。请按照以下步骤操作: + +1. 如果首次运行时出现权限提示,请允许应用访问所需文件夹 +2. 如需手动配置,请前往 `系统偏好设置 > 安全性与隐私 > 隐私 > 文件夹访问` +3. 确保 CapCut Mate 应用已被添加到允许列表中 + +更多详细信息,请参阅 [macOS 沙箱权限配置指南](./docs/macos_sandbox_setup.md)。 + 1. 安装依赖 ```bash diff --git a/desktop-client/assets/entitlements.mac.plist b/desktop-client/assets/entitlements.mac.plist new file mode 100644 index 0000000..7d4fa52 --- /dev/null +++ b/desktop-client/assets/entitlements.mac.plist @@ -0,0 +1,43 @@ + + + + + + com.apple.security.app-sandbox + + + + com.apple.security.files.user-selected.read-write + + + + com.apple.security.files.downloads.read-write + + + + com.apple.security.files.documents.read-write + + + + com.apple.security.files.pictures.read-write + + + + com.apple.security.files.music.read-write + + + + com.apple.security.files.movies.read-write + + + + com.apple.security.network.client + + + + com.apple.security.device.audio-input + + com.apple.security.device.camera + + + \ No newline at end of file diff --git a/desktop-client/main.js b/desktop-client/main.js index ec30e0d..fe0538a 100644 --- a/desktop-client/main.js +++ b/desktop-client/main.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow } = require('electron'); +const { app, BrowserWindow, dialog } = require('electron'); const path = require('path'); const logger = require('./nodeapi/logger'); @@ -67,6 +67,23 @@ function createWindow() { return mainWindow; } +// 处理未捕获的异常,特别是与文件权限相关的错误 +process.on('uncaughtException', (error) => { + logger.error('未捕获的异常:', error); + + // 在 macOS 沙箱环境下,某些权限错误可能在这里捕获 + if (process.platform === 'darwin' && error.message && + (error.message.includes('Operation not permitted') || error.message.includes('EPERM'))) { + dialog.showMessageBoxSync({ + type: 'error', + title: '权限错误', + message: '应用缺少必要的文件访问权限,请检查系统偏好设置中的隐私与安全性设置。', + detail: '请在 系统偏好设置 > 安全性与隐私 > 隐私 > 文件夹访问 中允许本应用访问相关文件夹。', + buttons: ['确定'] + }); + } +}); + // 当Electron完成初始化并准备创建浏览器窗口时调用此方法 app.whenReady().then(() => { createWindow(); diff --git a/desktop-client/nodeapi/download.js b/desktop-client/nodeapi/download.js index e9f8dcd..859181a 100644 --- a/desktop-client/nodeapi/download.js +++ b/desktop-client/nodeapi/download.js @@ -187,6 +187,24 @@ async function updateDraftPath(parentWindow) { return { success: false, error: "用户取消了目录选择" }; } try { + // 验证目录权限 + try { + await fs.access(targetDir, fs.constants.R_OK | fs.constants.W_OK); + } catch (accessError) { + logger.error('所选目录无读写权限:', accessError); + // 尝试使用 dialog 显示错误消息 + if (parentWindow) { + const { dialog } = require('electron'); + await dialog.showMessageBox(parentWindow, { + type: 'error', + title: '权限不足', + message: '所选目录没有足够的读写权限,请选择其他目录。', + buttons: ['确定'] + }); + } + return { success: false, error: '所选目录没有足够的读写权限' }; + } + const configPath = getConfigPath(); let config = {}; @@ -215,15 +233,15 @@ async function getTargetDirectory(parentWindow = null, isUpdate = false) { let config = await readConfig(); if (!isUpdate && config.targetDirectory) { try { - await fs.access(config.targetDirectory); + await fs.access(config.targetDirectory, fs.constants.R_OK | fs.constants.W_OK); return config.targetDirectory; } catch (accessErr) { - logger.warn("配置的目录已不存在,将重新选择。"); + logger.warn("配置的目录已不存在或无访问权限,将重新选择。", accessErr.message); } } const dialogOptions = { - properties: ["openDirectory"], + properties: ["openDirectory", "createDirectory"], // 允许创建新目录 title: "请选择目标目录", buttonLabel: "选择此目录", defaultPath: isUpdate ? config.targetDirectory : undefined @@ -238,6 +256,24 @@ async function getTargetDirectory(parentWindow = null, isUpdate = false) { if (!result.canceled && result.filePaths.length > 0) { const selectedDir = result.filePaths[0]; + + // 再次验证目录权限 + try { + await fs.access(selectedDir, fs.constants.R_OK | fs.constants.W_OK); + } catch (accessErr) { + logger.error('所选目录无读写权限:', accessErr); + if (parentWindow) { + const { dialog } = require('electron'); + await dialog.showMessageBox(parentWindow, { + type: 'error', + title: '权限不足', + message: '所选目录没有足够的读写权限,请重新选择。', + buttons: ['确定'] + }); + } + return ''; // 返回空字符串表示失败 + } + config.targetDirectory = selectedDir; await writeConfig(config); return selectedDir; @@ -477,7 +513,16 @@ async function downloadFiles( logger.info("[log] targetDir: " + targetDir); // 3. 确保目标目录存在 - await fs.mkdir(targetDir, { recursive: true }); // recursive: true 可以创建多级目录 + try { + await fs.mkdir(targetDir, { recursive: true }); // recursive: true 可以创建多级目录 + } catch (mkdirError) { + logger.error(`创建目录失败: ${targetDir}`, mkdirError); + await appendDownloadLog( + { level: "error", message: `创建目录失败: ${targetDir} - ${mkdirError.message}` }, + parentWindow + ); + throw mkdirError; + } // return { success: true, message: targetDir }; // 4. 下载文件 diff --git a/desktop-client/scripts/electron-builder-green.config.js b/desktop-client/scripts/electron-builder-green.config.js index 5dfa4d7..92f8ba0 100644 --- a/desktop-client/scripts/electron-builder-green.config.js +++ b/desktop-client/scripts/electron-builder-green.config.js @@ -11,6 +11,10 @@ module.exports = { mac: { icon: "assets/icons/logo.icns", artifactName: "capcut-mate-macos-\${arch}-green.zip", + hardenedRuntime: true, + gatekeeperAssess: false, + entitlements: "assets/entitlements.mac.plist", + entitlementsInherit: "assets/entitlements.mac.plist" }, files: [ "!node_modules/**/*", diff --git a/desktop-client/scripts/electron-builder.config.js b/desktop-client/scripts/electron-builder.config.js index 154f3e6..460f47f 100644 --- a/desktop-client/scripts/electron-builder.config.js +++ b/desktop-client/scripts/electron-builder.config.js @@ -43,7 +43,11 @@ module.exports = { } ], artifactName: "capcut-mate-macos-${arch}-installer.dmg", - category: "public.app-category.productivity" + category: "public.app-category.productivity", + hardenedRuntime: true, + gatekeeperAssess: false, + entitlements: "assets/entitlements.mac.plist", + entitlementsInherit: "assets/entitlements.mac.plist" }, dmg: { background: null,