cdrop — 跨 OS 剪贴板与文件传输服务

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)。

本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
This commit is contained in:
2026-06-15 21:38:28 +08:00
commit f21fa5b5e8
239 changed files with 29010 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
//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("或厂商私有 UTIcom.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)
}