claude-code-best 7e15974be9 feat: 实现 4 个 NAPI 包 — modifiers/image-processor/audio-capture/url-handler
- modifiers-napi: 使用 Bun FFI 调用 macOS CGEventSourceFlagsState 检测修饰键
- image-processor-napi: 集成 sharp 库,macOS 剪贴板图像读取 (osascript)
- audio-capture-napi: 基于 SoX/arecord 的跨平台音频录制
- url-handler-napi: 完善函数签名(保持 null fallback)
- 修复 image-processor 类型兼容性问题

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 01:07:42 +08:00

126 lines
3.3 KiB
TypeScript

import sharpModule from 'sharp'
export const sharp = sharpModule
interface NativeModule {
hasClipboardImage(): boolean
readClipboardImage(
maxWidth?: number,
maxHeight?: number,
): {
png: Buffer
width: number
height: number
originalWidth: number
originalHeight: number
} | null
}
function createDarwinNativeModule(): NativeModule {
return {
hasClipboardImage(): boolean {
try {
const result = Bun.spawnSync({
cmd: [
'osascript',
'-e',
'try\nthe clipboard as «class PNGf»\nreturn "yes"\non error\nreturn "no"\nend try',
],
stdout: 'pipe',
stderr: 'pipe',
})
const output = result.stdout.toString().trim()
return output === 'yes'
} catch {
return false
}
},
readClipboardImage(
maxWidth?: number,
maxHeight?: number,
) {
try {
// Use osascript to read clipboard image as PNG data and write to a temp file,
// then read the temp file back
const tmpPath = `/tmp/claude_clipboard_native_${Date.now()}.png`
const script = `
set png_data to (the clipboard as «class PNGf»)
set fp to open for access POSIX file "${tmpPath}" with write permission
write png_data to fp
close access fp
return "${tmpPath}"
`
const result = Bun.spawnSync({
cmd: ['osascript', '-e', script],
stdout: 'pipe',
stderr: 'pipe',
})
if (result.exitCode !== 0) {
return null
}
const file = Bun.file(tmpPath)
// Use synchronous read via Node compat
const fs = require('fs')
const buffer: Buffer = fs.readFileSync(tmpPath)
// Clean up temp file
try {
fs.unlinkSync(tmpPath)
} catch {
// ignore cleanup errors
}
if (buffer.length === 0) {
return null
}
// Read PNG dimensions from IHDR chunk
// PNG header: 8 bytes signature, then IHDR chunk
// IHDR starts at offset 8 (4 bytes length) + 4 bytes "IHDR" + 4 bytes width + 4 bytes height
let width = 0
let height = 0
if (buffer.length > 24 && buffer[12] === 0x49 && buffer[13] === 0x48 && buffer[14] === 0x44 && buffer[15] === 0x52) {
width = buffer.readUInt32BE(16)
height = buffer.readUInt32BE(20)
}
const originalWidth = width
const originalHeight = height
// If maxWidth/maxHeight are specified and the image exceeds them,
// we still return the full PNG - the caller handles resizing via sharp
// But we report the capped dimensions
if (maxWidth && maxHeight) {
if (width > maxWidth || height > maxHeight) {
const scale = Math.min(maxWidth / width, maxHeight / height)
width = Math.round(width * scale)
height = Math.round(height * scale)
}
}
return {
png: buffer,
width,
height,
originalWidth,
originalHeight,
}
} catch {
return null
}
},
}
}
export function getNativeModule(): NativeModule | null {
if (process.platform === 'darwin') {
return createDarwinNativeModule()
}
return null
}
export default sharp