import Cocoa import Carbon /// 通过剪贴板备份 → 写入 → Cmd+V 粘贴 → 恢复剪贴板的方式注入文本 struct TextInjector { /// 剪贴板备份结构 private struct PasteboardBackup { let items: [[String: Data]] } /// 将文本注入当前焦点输入框(异步完成剪贴板恢复) @discardableResult static func inject(_ text: String) -> Bool { guard !text.isEmpty else { return false } // 备份 let backup = backupPasteboard() // 检测输入法 let isCJK = InputSourceManager.isCJKInputSource() let originalSource: TISInputSource? = isCJK ? InputSourceManager.switchToASCII() : nil if isCJK { Thread.sleep(forTimeInterval: 0.05) } // 写入目标文本 NSPasteboard.general.clearContents() NSPasteboard.general.setString(text, forType: .string) // 模拟 Cmd+V(必须在主线程 run loop 中处理) simulatePaste() // 恢复输入法(不阻塞主线程) if isCJK { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { InputSourceManager.restore(originalSource) } } // 延迟恢复剪贴板,确保粘贴事件已被目标应用处理 DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { restorePasteboard(backup) } print("[Injector] Injected \"\(text.prefix(50))...\" (\(text.count) chars)") return true } // MARK: - 剪贴板备份/恢复 private static func backupPasteboard() -> PasteboardBackup { let pb = NSPasteboard.general guard let items = pb.pasteboardItems, !items.isEmpty else { var fallback: [[String: Data]] = [] if let str = pb.string(forType: .string), let data = str.data(using: .utf8) { fallback = [["public.utf8-plain-text": data]] } return PasteboardBackup(items: fallback) } let backedItems: [[String: Data]] = items.compactMap { item in var typeData: [String: Data] = [:] for type in item.types { if let data = item.data(forType: type) { typeData[type.rawValue] = data } } return typeData.isEmpty ? nil : typeData } return PasteboardBackup(items: backedItems) } private static func restorePasteboard(_ backup: PasteboardBackup) { let pb = NSPasteboard.general pb.clearContents() for typeData in backup.items { let newItem = NSPasteboardItem() for (type, data) in typeData { newItem.setData(data, forType: NSPasteboard.PasteboardType(type)) } pb.writeObjects([newItem]) } } // MARK: - 模拟粘贴 private static func simulatePaste() { let source = CGEventSource(stateID: .combinedSessionState) let keyDown = CGEvent(keyboardEventSource: source, virtualKey: 0x09, keyDown: true) keyDown?.flags = .maskCommand keyDown?.post(tap: .cghidEventTap) usleep(10_000) let keyUp = CGEvent(keyboardEventSource: source, virtualKey: 0x09, keyDown: false) keyUp?.flags = .maskCommand keyUp?.post(tap: .cghidEventTap) } }