日期: 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: NSStringPboardType 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 |