//go:build darwin // clipprobe 是一个只读诊断工具:监听本机剪贴板变化,打印每次变化时 // NSPasteboard 上挂的全部类型(UTI)。它不读取也不上传任何剪贴板内容, // 只看「类型」,用于在真机上经验性地确定各应用(尤其密码管理器)复制时 // 打哪些 UTI,从而让 D4 的敏感内容过滤基于实测数据而非猜测。 // // 用法:go run ./cmd/clipprobe(或 go build 后运行二进制),然后依次从 // 普通文本编辑器 / 1Password / Bitwarden / 浏览器密码自动填充 等复制,观察 // 各自打印的 UTI。含 org.nspasteboard.ConcealedType / TransientType / // AutoGeneratedType 或厂商私有 UTI(如 com.agilebits.onepassword)者,即为 // D4 应跳过上传的敏感内容。 // // 实现用 ebitengine/purego + objc 运行时直调 AppKit,无 cgo、可随 Wails // 交叉编译;调用模式照搬 golang.design/x/clipboard 的 darwin 实现。 package main import ( "fmt" "runtime" "time" "unsafe" "github.com/ebitengine/purego" "github.com/ebitengine/purego/objc" ) func main() { // objc 调用必须钉在同一 OS 线程上(autorelease pool 的语义要求)。 runtime.LockOSThread() // 加载 AppKit / Foundation,使 NSPasteboard / NSAutoreleasePool 等类可解析。 for _, fw := range []string{ "/System/Library/Frameworks/AppKit.framework/AppKit", "/System/Library/Frameworks/Foundation.framework/Foundation", } { if _, err := purego.Dlopen(fw, purego.RTLD_NOW|purego.RTLD_GLOBAL); err != nil { panic(fmt.Sprintf("clipprobe: 无法加载 %s: %v", fw, err)) } } pbClass := objc.GetClass("NSPasteboard") poolClass := objc.GetClass("NSAutoreleasePool") var ( selGeneral = objc.RegisterName("generalPasteboard") selChangeCount = objc.RegisterName("changeCount") selTypes = objc.RegisterName("types") selCount = objc.RegisterName("count") selObjectAt = objc.RegisterName("objectAtIndex:") selUTF8 = objc.RegisterName("UTF8String") selAlloc = objc.RegisterName("alloc") selInit = objc.RegisterName("init") selRelease = objc.RegisterName("release") ) fmt.Println("clipprobe:监听剪贴板,打印每次变化的 pasteboard 类型(UTI)。Ctrl+C 退出。") fmt.Println("依次从 普通文本 / 1Password / Bitwarden / 浏览器密码自动填充 复制,观察各自的 UTI。") fmt.Println("含 org.nspasteboard.ConcealedType / TransientType / AutoGeneratedType") fmt.Println("或厂商私有 UTI(com.agilebits.onepassword 等)者 = 应跳过上传的敏感内容。") fmt.Println("(本工具只读类型、不读内容、不上传。)") fmt.Println("--------") last := -1 for { pool := objc.ID(poolClass).Send(selAlloc).Send(selInit) general := objc.ID(pbClass).Send(selGeneral) cc := int(general.Send(selChangeCount)) if cc != last { last = cc types := general.Send(selTypes) n := int(types.Send(selCount)) fmt.Printf("[changeCount=%d] %d 个类型:\n", cc, n) for i := 0; i < n; i++ { item := types.Send(selObjectAt, i) cstr := item.Send(selUTF8) fmt.Printf(" - %s\n", cString(uintptr(cstr))) } } objc.ID(pool).Send(selRelease) time.Sleep(500 * time.Millisecond) } } // cString reads a NUL-terminated C string (from -[NSString UTF8String]) into a // Go string. func cString(p uintptr) string { if p == 0 { return "(nil)" } var b []byte for i := 0; ; i++ { c := *(*byte)(unsafe.Pointer(p + uintptr(i))) if c == 0 { break } b = append(b, c) } return string(b) }