fix(computer-use): 修复权限检查和应用列表获取的问题 (#157)

* docs: update contributors

* fix(computer-use): 修复权限检查和应用列表获取的问题

修复 macOS 平台下权限检查的 JXA 回退逻辑,确保在没有原生模块时仍能正确检测权限
改进应用列表获取方式,使用 mdls 获取真实的 bundleId 而非生成伪 ID

* docs: update contributors

* docs: update contributors

* docs: update contributors

---------

Co-authored-by: mcjjin <8590489+mcjjin@users.noreply.github.com>
Co-authored-by: claude-code-best <claude-code-best@proton.me>
Co-authored-by: claude-code-best <272536312+claude-code-best@users.noreply.github.com>
This commit is contained in:
mcjjin
2026-04-07 19:53:59 +08:00
committed by GitHub
parent 5d7e54751a
commit 0d8f494c4b
4 changed files with 87 additions and 34 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 883 KiB

After

Width:  |  Height:  |  Size: 885 KiB

View File

@@ -159,25 +159,33 @@ export const apps: AppsAPI = {
async listInstalled() { async listInstalled() {
try { try {
const result = await osascript(` // Use mdls to enumerate apps and get real bundle identifiers.
tell application "System Events" // The previous AppleScript approach generated fake bundle IDs
set appList to "" // (com.app.display-name) which prevented request_access from matching
repeat with appFile in (every file of folder "Applications" of startup disk whose name ends with ".app") // apps by their real bundle ID (e.g. com.google.Chrome).
set appPath to POSIX path of (appFile as alias) const dirs = ['/Applications', '~/Applications', '/System/Applications']
set appName to name of appFile const allApps: InstalledApp[] = []
set appList to appList & appPath & "|" & appName & "\\n" for (const dir of dirs) {
end repeat const expanded = dir.startsWith('~') ? join(process.env.HOME ?? '~', dir.slice(1)) : dir
return appList const proc = Bun.spawn(
end tell ['bash', '-c', `for f in "${expanded}"/*.app; do [ -d "$f" ] || continue; bid=$(mdls -name kMDItemCFBundleIdentifier "$f" 2>/dev/null | sed 's/.*= "//;s/"//'); name=$(basename "$f" .app); echo "$f|$name|$bid"; done`],
`) { stdout: 'pipe', stderr: 'pipe' },
return result.split('\n').filter(Boolean).map(line => { )
const [path, name] = line.split('|', 2) const text = await new Response(proc.stdout).text()
const displayName = (name ?? '').replace(/\.app$/, '') await proc.exited
return { for (const line of text.split('\n').filter(Boolean)) {
bundleId: `com.app.${displayName.toLowerCase().replace(/\s+/g, '-')}`, const [path, displayName, bundleId] = line.split('|', 3)
displayName, if (path && displayName && bundleId && bundleId !== '(null)') {
path: path ?? '', allApps.push({ bundleId, displayName, path })
}
} }
}
// Deduplicate by bundleId (prefer /Applications over ~/Applications)
const seen = new Set<string>()
return allApps.filter(app => {
if (seen.has(app.bundleId)) return false
seen.add(app.bundleId)
return true
}) })
} catch { } catch {
return [] return []

View File

@@ -26,7 +26,7 @@ export function registerEscHotkey(onEscape: () => void): boolean {
if (process.platform !== 'darwin') return false if (process.platform !== 'darwin') return false
if (registered) return true if (registered) return true
const cu = requireComputerUseSwift() const cu = requireComputerUseSwift()
if (!(cu as any).hotkey.registerEscape(onEscape)) { if (!(cu as any).hotkey?.registerEscape(onEscape)) {
// CGEvent.tapCreate failed — typically missing Accessibility permission. // CGEvent.tapCreate failed — typically missing Accessibility permission.
// CU still works, just without ESC abort. Mirrors Cowork's escAbort.ts:81. // CU still works, just without ESC abort. Mirrors Cowork's escAbort.ts:81.
logForDebugging('[cu-esc] registerEscape returned false', { level: 'warn' }) logForDebugging('[cu-esc] registerEscape returned false', { level: 'warn' })
@@ -41,7 +41,7 @@ export function registerEscHotkey(onEscape: () => void): boolean {
export function unregisterEscHotkey(): void { export function unregisterEscHotkey(): void {
if (!registered) return if (!registered) return
try { try {
(requireComputerUseSwift() as any).hotkey.unregister() (requireComputerUseSwift() as any).hotkey?.unregister()
} finally { } finally {
releasePump() releasePump()
registered = false registered = false
@@ -51,5 +51,5 @@ export function unregisterEscHotkey(): void {
export function notifyExpectedEscape(): void { export function notifyExpectedEscape(): void {
if (!registered) return if (!registered) return
(requireComputerUseSwift() as any).hotkey.notifyExpectedEscape() (requireComputerUseSwift() as any).hotkey?.notifyExpectedEscape()
} }

View File

@@ -27,6 +27,38 @@ class DebugLogger implements Logger {
} }
} }
// ---------------------------------------------------------------------------
// JXA-based TCC permission probes (fallback when native .node module absent)
// ---------------------------------------------------------------------------
/** Probe accessibility by asking System Events for a process list. */
function checkAccessibilityJXA(): boolean {
try {
const result = Bun.spawnSync({
cmd: ['osascript', '-e', 'tell application "System Events" to get name of every process whose background only is false'],
stdout: 'pipe',
stderr: 'pipe',
})
return result.exitCode === 0
} catch {
return false
}
}
/** Probe screen recording by attempting a 1x1 screencapture. */
function checkScreenRecordingJXA(): boolean {
try {
const result = Bun.spawnSync({
cmd: ['screencapture', '-x', '-R', '0,0,1,1', '/dev/null'],
stdout: 'pipe',
stderr: 'pipe',
})
return result.exitCode === 0
} catch {
return false
}
}
let cached: ComputerUseHostAdapter | undefined let cached: ComputerUseHostAdapter | undefined
/** /**
@@ -47,8 +79,19 @@ export function getComputerUseHostAdapter(): ComputerUseHostAdapter {
ensureOsPermissions: async () => { ensureOsPermissions: async () => {
if (process.platform !== 'darwin') return { granted: true } if (process.platform !== 'darwin') return { granted: true }
const cu = requireComputerUseSwift() const cu = requireComputerUseSwift()
const accessibility = (cu as any).tcc.checkAccessibility() const tcc = (cu as any).tcc
const screenRecording = (cu as any).tcc.checkScreenRecording() // Native Swift .node module provides tcc.checkAccessibility/checkScreenRecording.
// When absent (decompiled/reverse-engineered build), fall back to JXA probes.
if (tcc) {
const accessibility = tcc.checkAccessibility()
const screenRecording = tcc.checkScreenRecording()
return accessibility && screenRecording
? { granted: true }
: { granted: false, accessibility, screenRecording }
}
// JXA fallback: try to query System Events (accessibility) and screencapture (screen recording).
const accessibility = checkAccessibilityJXA()
const screenRecording = checkScreenRecordingJXA()
return accessibility && screenRecording return accessibility && screenRecording
? { granted: true } ? { granted: true }
: { granted: false, accessibility, screenRecording } : { granted: false, accessibility, screenRecording }