f21fa5b5e8
Commilitia Drop:自托管的跨设备剪贴板同步与点对点文件传输。 - 后端 Go(chi / SQLite WAL / SSE Hub / WebRTC signaling + 状态机 / Relay ring buffer),编译进单个 distroless 镜像(前端 go:embed)。 - 前端 React + TanStack Router + Zustand,自实现 SSE + WebRTC P2P,NAT 受阻时回退服务端中继;聚珍(Juzhen)CJK 综合排版。 - 桌面端 Wails v2(macOS / Windows),瘦客户端复用 web。 - 鉴权 OIDC PKCE(自建 Casdoor 等),refresh_token 信封加密存系统密钥库;iOS Shortcut 用 HS256 scoped token。 架构文档与变更记录见 docs 分支(PROJECT_BRIEF / FRONTEND_DESIGN / CHANGELOG)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
102 lines
3.5 KiB
Go
102 lines
3.5 KiB
Go
//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)
|
||
}
|