mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 16:25:51 +00:00
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:
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 883 KiB After Width: | Height: | Size: 885 KiB |
@@ -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 []
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
Reference in New Issue
Block a user