
日期: 2026-03-26 严重程度: 中(功能性缺陷,影响跨机器工作流)
| 项目 | 远程机器(被控端) | 本地机器(控制端) |
|---|---|---|
| 机型 | MacBook Pro 15-inch, 2017 | MacBook Pro 14-inch, 2021 |
| 处理器架构 | Intel x86_64 (2.8 GHz 四核 Core i7) | Apple M1 Pro (ARM64) |
| 内存 | 16 GB 2133 MHz LPDDR3 | 16 GB |
| macOS 版本 | Ventura 13.7.7 | macOS Tahoe 26.3.1 |
| 主机名(脱敏) | remote-mac.local | local-mac.local |
| 屏幕共享版本 | 3.0 (587.3) | 6.2 (758.1) |
| 微信版本 | 4.1.8.52 (Qt 框架构建) | 不涉及 |
连接方式: 本地机器通过 macOS 自带「屏幕共享」 6.2 (758.1) 远程控制远程机器(被控端运行屏幕共享服务端 3.0 (587.3)),并开启了剪贴板共享功能。
在远程机器的微信( WeChat 4.1.8.52 )中复制文字后,切换到本地机器执行粘贴( Cmd+V ),粘贴无效,无任何内容输出。
同一场景下,从远程机器的备忘录( Notes.app )复制文字,本地粘贴正常。
使用自制诊断工具 capture_once.swift 在两台机器上同时监控剪贴板,复制后各自捕获原始数据。
远程机器剪贴板(复制后原始状态):
=== CLIPBOARD DUMP [REMOTE] === time: 2026-03-26 20:41:27.707 changeCount: 1858 [38B] com.apple.traditional-mac-plain-text → "ai 为啥一段时间以后就会犯蠢" [38B] CorePasteboardFlavorType 0x54455854 → "ai 为啥一段时间以后就会犯蠢" [28B] public.utf16-plain-text [28B] CorePasteboardFlavorType 0x75747874 [38B] public.utf8-plain-text → "ai 为啥一段时间以后就会犯蠢" [38B] NSStringPboardType → "ai 为啥一段时间以后就会犯蠢" [38B] public.text → "ai 为啥一段时间以后就会犯蠢" [38B] com.trolltech.anymime.text--plain → "ai 为啥一段时间以后就会犯蠢" NSStringPboardType: "ai 为啥一段时间以后就会犯蠢" public.utf8-plain-text: "ai 为啥一段时间以后就会犯蠢" 本地机器剪贴板( Screen Sharing 同步后):
=== CLIPBOARD DUMP [LOCAL] === time: 2026-03-26 20:41:29.147 changeCount: 1319 [38B] com.apple.traditional-mac-plain-text → "ai 为啥一段时间以后就会犯蠢" [38B] CorePasteboardFlavorType 0x54455854 → "ai 为啥一段时间以后就会犯蠢" [38B] public.text → "ai 为啥一段时间以后就会犯蠢" [38B] com.trolltech.anymime.text--plain → "ai 为啥一段时间以后就会犯蠢" NSStringPboardType: nil public.utf8-plain-text: nil 修复结果:
检测到 Screen Sharing bug ,执行修复... 修复完成:"ai 为啥一段时间以后就会犯蠢" NSStringPboardType: "ai 为啥一段时间以后就会犯蠢" 远程机器剪贴板:
[476B] public.rtf → (RTF 格式文本) [72B] public.utf8-plain-text → "销售后缀校正的列表有好几个,可以帮助校正一下吗?" [72B] NSStringPboardType → "销售后缀校正的列表有好几个,可以帮助校正一下吗?" [1067B] com.apple.notes.richtext 本地机器剪贴板( Screen Sharing 同步后):
[476B] public.rtf → (RTF 格式文本,完整) [72B] public.utf8-plain-text → "销售后缀校正的列表有好几个,可以帮助校正一下吗?" [72B] NSStringPboardType → "销售后缀校正的列表有好几个,可以帮助校正一下吗?" 无需修复,剪贴板数据正常 微信( WeChat 4.1.8.52 )基于 Qt 框架开发,复制文字时向剪贴板写入了一个 Qt 专有类型:
com.trolltech.anymime.text--plain com.trolltech 是 Qt 框架的历史遗留命名空间( Trolltech 是 Qt 的原始开发公司,2008 年被诺基亚收购)。
对比远程和本地的类型列表:
| 类型 | 远程(原始) | 本地(同步后) |
|---|---|---|
NSStringPboardType | 38B ,有内容 | 缺失 |
public.utf8-plain-text | 38B ,有内容 | 缺失 |
public.utf16-plain-text | 28B ,无文本 | 缺失 |
com.apple.traditional-mac-plain-text | 有内容 | 有内容 |
com.trolltech.anymime.text--plain | 有内容 | 有内容 |
public.text | 有内容 | 有内容 |
Screen Sharing 在同步剪贴板时,**选择性地丢弃了 NSStringPboardType 和 public.utf8-plain-text**,而这两个类型是 macOS 上 Cmd+V 粘贴操作读取的标准类型。
责任方:本地 Apple macOS Screen Sharing 6.2 (758.1)(控制端)
理由:
NSStringPboardType 在远程机器上有完整内容,远程 Screen Sharing 3.0 正确发送了所有类型NSPasteboard,数据丢失发生在这一步com.trolltech.anymime.text--plain 自定义类型,本地 Screen Sharing 6.2 在处理这种混合类型时,未能正确还原标准文本类型主要责任在本地 Screen Sharing 6.2,远程 Screen Sharing 3.0 仅负责发送,已正确完成工作。微信和 Qt 框架行为符合规范,不存在问题。
在本地机器后台运行 clipboard_fix.swift,自动检测并修复损坏的剪贴板状态:
nohup swift ~/scripts/clipboard-sync/clipboard_fix.swift > /tmp/clipboard_fix.log 2>&1 & 修复逻辑:检测到剪贴板含有 com.trolltech.anymime.text--plain 但 NSStringPboardType 为空时,从 trolltech 类型中提取文本,重新写入标准类型。
提交对象: Apple (通过 Feedback Assistant ) 分类: macOS > Screen Sharing / Remote Desktop
标题建议:
Screen Sharing 3.0: NStringPboardType dropped when syncing clipboard containing Qt/Trolltech custom pasteboard types from remote Mac 复现环境:
#!/usr/bin/env swift // 启动后等待剪贴板变化,捕获到一次后立即 dump 并退出 // 用法:swift capture_once.swift [remote|local] import AppKit import Foundation let label = CommandLine.arguments.count >= 2 ? CommandLine.arguments[1] : "unknown" let pb = NSPasteboard.general let initialCount = pb.changeCount let df = DateFormatter() df.dateFormat = "HH:mm:ss.SSS" func ts() -> String { df.string(from: Date()) } func dump() { let now = DateFormatter() now.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" print("") print("=== CLIPBOARD DUMP [\(label.uppercased())] ===") print("time: \(now.string(from: Date()))") print("host: \(ProcessInfo.processInfo.hostName)") print("changeCount: \(pb.changeCount) (初始: \(initialCount))") print("") guard let types = pb.types, !types.isEmpty else { print("(剪贴板为空)"); return } print("--- 所有类型 ---") for t in types { let data = pb.data(forType: t) ?? Data() let utf8 = String(data: data, encoding: .utf8) ?? "" let hasText = !utf8.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty print("\(hasText ? "" : "") [\(data.count)B] \(t.rawValue)") if hasText { print(" \"\(utf8.prefix(200).replacingOccurrences(of: "\n", with: "\\n"))\"") } } print("") print("--- 标准文本读取 ---") if let s = pb.string(forType: .string) { print("NSStringPboardType: \"\(s.prefix(300))\"") } else { print("NSStringPboardType: nil") } if let d = pb.data(forType: NSPasteboard.PasteboardType("public.utf8-plain-text")), let s = String(data: d, encoding: .utf8), !s.isEmpty { print("public.utf8-plain-text: \"\(s.prefix(300))\"") } else { print("public.utf8-plain-text: nil") } print("") print("=== END [\(label.uppercased())] ===") } let trolltechType = NSPasteboard.PasteboardType("com.trolltech.anymime.text--plain") func needsFix() -> Bool { guard let types = pb.types else { return false } let typeSet = Set(types.map(\.rawValue)) return typeSet.contains(trolltechType.rawValue) && pb.string(forType: .string) == nil } func fix() { guard let data = pb.data(forType: trolltechType), let text = String(data: data, encoding: .utf8) ?? String(data: data, encoding: .macOSRoman), !text.isEmpty else { print("[\(ts())] 无法从 trolltech 类型提取文本"); return } pb.clearContents() pb.setString(text, forType: .string) pb.setString(text, forType: NSPasteboard.PasteboardType("public.utf8-plain-text")) pb.setData(data, forType: trolltechType) print("[\(ts())] 修复完成:\"\(text.prefix(80))\"") } print("[\(ts())] [\(label)] 已就绪,等待剪贴板变化(请在微信复制文字)...") while true { if pb.changeCount != initialCount { print("[\(ts())] [\(label)] 检测到变化,捕获中...") dump() if label == "local" { print("\n--- 修复 ---") if needsFix() { print("[\(ts())] 检测到 Screen Sharing bug ,执行修复...") fix() print("\n--- 修复后剪贴板 ---") if let s = pb.string(forType: .string) { print("NSStringPboardType: \"\(s.prefix(300))\"") } else { print("NSStringPboardType: 修复失败") } } else { print("[\(ts())] 无需修复,剪贴板数据正常") } } exit(0) } Thread.sleep(forTimeInterval: 0.1) } #!/usr/bin/env swift // 持续监控本地剪贴板,自动修复 Screen Sharing 同步微信文字后的 bug // 用法:nohup swift clipboard_fix.swift > /tmp/clipboard_fix.log 2>&1 & import AppKit import Foundation let pb = NSPasteboard.general var lastCount = pb.changeCount let trolltechType = NSPasteboard.PasteboardType("com.trolltech.anymime.text--plain") func ts() -> String { let f = DateFormatter(); f.dateFormat = "HH:mm:ss.SSS"; return f.string(from: Date()) } func isBrokenState() -> Bool { guard let types = pb.types else { return false } let typeSet = Set(types.map(\.rawValue)) return typeSet.contains(trolltechType.rawValue) && pb.string(forType: .string) == nil } print("[\(ts())] clipboard_fix 已启动") while true { let currentCount = pb.changeCount if currentCount != lastCount { lastCount = currentCount if isBrokenState() { if let data = pb.data(forType: trolltechType), let text = String(data: data, encoding: .utf8) ?? String(data: data, encoding: .macOSRoman), !text.isEmpty { pb.clearContents() pb.setString(text, forType: .string) pb.setString(text, forType: NSPasteboard.PasteboardType("public.utf8-plain-text")) pb.setData(data, forType: trolltechType) print("[\(ts())] 修复:\"\(text.prefix(80))\"") } } } Thread.sleep(forTimeInterval: 0.1) } 1 phpfpm OP |