微信 OSX 版+屏幕共享(Screen Sharing.app)的无法向外复制我已经提交 issue,感谢 AI
phpfpm
·
2026-03-26
·
via V2EX
Bug Report: macOS Screen Sharing 剪贴板同步丢失标准文本类型 日期: 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 )复制文字,本地粘贴 正常 。 三、复现步骤 本地机器通过屏幕共享连接远程机器,开启剪贴板共享 在远程机器微信中选中任意文字,执行复制( Cmd+C ) 切换焦点到本地机器任意文本输入框 执行粘贴( Cmd+V ) 结果:无内容粘贴 四、实验数据 使用自制诊断工具 capture_once.swift 在两台机器上同时监控剪贴板,复制后各自捕获原始数据。 实验 A:从远程微信复制文字 远程机器剪贴板(复制后原始状态): === 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 为啥一段时间以后就会犯蠢" 实验 B:从远程备忘录复制文字(对照组) 远程机器剪贴板: ✅ [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 年被诺基亚收购)。 问题所在:Screen Sharing 的剪贴板同步逻辑 对比远程和本地的类型列表: 类型 远程(原始) 本地(同步后) 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 ,数据丢失发生在这一步 备忘录的剪贴板(不含 trolltech 类型)经同一链路同步后完全正常,说明远程发送侧没有问题 唯一差异是微信剪贴板含有 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 类型中提取文本,重新写入标准类型。 七、建议提交 Feedback 提交对象: 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 复现环境: 控制端:Apple Silicon Mac ,macOS 26.x (Tahoe) 被控端:Intel Mac ,macOS Ventura 13.7.7 屏幕共享版本:3.0 (587.3) 触发应用:WeChat 4.1.8.52 ( Qt 框架) 附录:诊断工具源码 capture_once.swift #!/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) } clipboard_fix.swift (后台常驻修复守护进程) #!/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) }
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。