V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
工单节点使用指南
• 请用平和的语言准确描述你所遇到的问题
• 厂商的技术支持和你一样也是有喜怒哀乐的普通人类,尊重是相互的
• 如果是关于 V2EX 本身的问题反馈,请使用 反馈 节点
phpfpm
V2EX  ›  全球工单系统

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

  •  
  •   phpfpm · 3 月 26 日 · 445 次点击

    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)
    }
    
    phpfpm
        1
    phpfpm  
    OP
       3 月 26 日
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   2647 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 12:08 · PVG 20:08 · LAX 05:08 · JFK 08:08
    ♥ Do have faith in what you're doing.