GoForum🌐 V2EX

微信 OSX 版+屏幕共享(Screen Sharing.app)的无法向外复制我已经提交 issue,感谢 AI

phpfpm · 2026-03-26 21:19 · 0 次点赞 · 1 条回复

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 )复制文字,本地粘贴正常


三、复现步骤

  1. 本地机器通过屏幕共享连接远程机器,开启剪贴板共享
  2. 在远程机器微信中选中任意文字,执行复制( Cmd+C )
  3. 切换焦点到本地机器任意文本输入框
  4. 执行粘贴( Cmd+V )
  5. 结果:无内容粘贴

四、实验数据

使用自制诊断工具 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 在同步剪贴板时,选择性地丢弃了 NSStringPboardTypepublic.utf8-plain-text,而这两个类型是 macOS 上 Cmd+V 粘贴操作读取的标准类型。

责任归属

责任方:本地 Apple macOS Screen Sharing 6.2 (758.1)(控制端)

理由:

  1. 远程微信的剪贴板数据完整,NSStringPboardType 在远程机器上有完整内容,远程 Screen Sharing 3.0 正确发送了所有类型
  2. 剪贴板同步由控制端(本地)发起并负责接收、反序列化、写入本地 NSPasteboard,数据丢失发生在这一步
  3. 备忘录的剪贴板(不含 trolltech 类型)经同一链路同步后完全正常,说明远程发送侧没有问题
  4. 唯一差异是微信剪贴板含有 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--plainNSStringPboardType 为空时,从 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)
}
1 条回复
phpfpm · 2026-03-26 21:19
#1
添加回复
你还需要 登录 后发表回复

登录后可发帖和回复

登录 注册
主题信息
作者: phpfpm
发布: 2026-03-26
点赞: 0
回复: 0