从 80+ 平台吸附看客户端窗口识别与自动化交互

0 阅读6分钟

客服聊天窗口.png

聊天宝的核心能力之一是"智能吸附"——自动识别当前聊天窗口并悬浮在旁边。本文从技术视角聊聊窗口识别、平台适配与跨平台自动化背后的工程挑战。

一、"吸附"在技术上意味着什么

很多用户觉得吸附是个"小功能",实际上它是客户端软件中复杂度最高的模块之一:

  • 窗口识别:需要知道当前焦点窗口是什么、是什么平台
  • 位置计算:需要计算目标窗口的坐标,把悬浮窗放到合适位置
  • 协议兼容:不同平台的剪贴板操作、快捷键行为各不相同
  • 多实例管理:同一个软件开多个聊天窗口时,需要精确识别当前聊天对象

表面上是"悬浮在旁边",背后是一套完整的窗口管理与进程间通信系统

二、Windows 端窗口识别的技术路径

2.1 Win32 API 基础

Windows 平台窗口识别主要依赖 Win32 API:

# 获取当前焦点窗口句柄
hwnd = win32gui.GetForegroundWindow()

# 获取窗口类名和标题
class_name = win32gui.GetClassName(hwnd)
window_text = win32gui.GetWindowText(hwnd)

# 获取进程ID(用于识别具体应用)
pid = win32process.GetWindowThreadProcessId(hwnd)

**窗口类名(ClassName)**是识别的关键。不同应用有不同的类名:

应用类名示例说明
微信 PC 版WeChatMainWndForPC主窗口
企业微信Qt5QWindowIcon + 进程名判断跨平台框架
QQTXGuiFoundationQQ NT 架构
钉钉ApolloRuntimeContentWindowChromium 内核

2.2 进程枚举:精准识别多人聊天窗口

仅靠窗口类名无法区分"和谁在聊天"。例如微信,同一个 WeChatMainWndForPC 类名下可能有数十个聊天窗口。

解决方案:枚举所有窗口句柄,建立进程 + 类名 + 标题的多维映射

def enum_wechat_windows():
    windows = []
    win32gui.EnumWindows(lambda hwnd, windows: (
        windows.append({
            'hwnd': hwnd,
            'class': win32gui.GetClassName(hwnd),
            'title': win32gui.GetWindowText(hwnd),
            'pid': win32process.GetWindowThreadProcessId(hwnd)[-1]
        }) if 'WeChat' in win32gui.GetClassName(hwnd) else None
    ), windows)
    return windows

枚举后,再结合聊天窗口的特定 UI 元素(如消息列表的控件句柄)来判断当前激活的是哪个具体会话。

2.3 平台特征数据库

80+ 平台,每个平台的识别逻辑都需要单独适配。实践中通常维护一张平台特征表

PLATFORM_SIGNATURES = {
    'wechat': {
        'class_name': 'WeChatMainWndForPC',
        'process_name': 'WeChat.exe',
        'control_patterns': {
            'input_box': 'RichEdit20W',
            'send_button': ['发送(&S)', 'Send']
        }
    },
    'qiwei': {
        'class_name': 'Qt5QWindowIcon',  # Qt 跨平台应用
        'process_name': 'WXWork.exe',
        'control_patterns': {
            'input_box': 'RichEdit20W',
        }
    },
    # ... 80+ 条记录
}

这套机制的好处是:新增平台只需添加一条配置,无需改代码

三、跨平台窗口识别差异

3.1 Windows:Win32 API 的天下

Windows 的窗口体系高度标准化,GetForegroundWindow + EnumWindows 组合基本能覆盖所有场景。但有几个坑:

  • UWP 应用:如微信 UWP 版,窗口句柄获取方式不同,需要用 UI Automation API
  • Electron 应用:如飞书,渲染进程和主进程分离,窗口信息在渲染层
  • 沙盒进程:部分应用运行在 AppContainer 中,部分 Win32 API 会失效

3.2 macOS:Accessibility API + AXUI

macOS 不允许直接访问其他应用的窗口内容,必须通过 Accessibility API

// Swift 示例:获取当前焦点应用
let runningApps = NSWorkspace.shared.runningApplications
let frontApp = runningApps.first { $0.isActive && $0.activationPolicy == .regular }

// 获取应用名称和 bundle identifier
let appName = frontApp?.localizedName ?? ""
let bundleId = frontApp?.bundleIdentifier ?? ""

对于聊天内容读取,macOS 的限制更严格:必须用户显式授权辅助功能权限,否则无法获取任何窗口信息。

3.3 Android/iOS:系统限制与曲线方案

移动端的"吸附"逻辑完全不同——因为应用之间是隔离的,一个 App 无法读取另一个 App 的窗口内容

移动端的实现方案通常是:

  • Android:使用悬浮窗(Float Window)权限,在聊天软件上方覆盖一个透明层拦截点击
  • iOS:通过输入法扩展(Keyboard Extension)实现,点击候选词后粘贴到当前输入框
  • 小程序/H5:通过微信开放能力,在客服助手小程序内嵌入聊天的能力

四、剪贴板交互与发送机制

识别到窗口只是第一步,核心操作是把话术内容发送到聊天输入框

4.1 方案一:剪贴板模拟(最通用)

话术文本 → 写入剪贴板 → 模拟 Ctrl+V 粘贴 → 模拟 Enter 发送

优点:几乎所有应用都支持 Ctrl+V 缺点:会覆盖用户原有剪贴板内容

聊天宝通过剪贴板自动复原机制解决这个问题:内容发送后,记录原剪贴板内容并在 2 秒后自动恢复。

4.2 方案二:直接 API 注入(精度高但风险大)

直接调用目标应用的内部接口(如微信的 EditText.setText()):

  • 优点:不干扰剪贴板,精度高
  • 缺点:需要针对每个应用写适配代码,且存在被安全软件报毒的风险

4.3 方案三:Accessibility Service(Android 无障碍)

Android 上通过无障碍服务可以直接读取和写入其他应用的内容:

// 找到输入框节点
val inputNode = AccessibilityNodeInfoUtils.findNode(
    rootNode,
    ViewsCriteria.getCriteria(ViewsCriteria.EDIT_TEXT)
)
// 模拟输入
inputNode?.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)

这需要用户开启无障碍权限,且每个 Android 版本的手势操作 API 都有变化,维护成本较高。

五、网页端客服的特殊处理

淘宝千牛、拼多多商家版、京东客服工作台这类网页客服,无法通过窗口 API 识别,解决方案是浏览器插件

  • Content Script:注入到目标网页,可以直接访问 DOM
  • 读取聊天输入框document.querySelector('.im-editor-input')
  • 自动填充inputElement.value = content; inputElement.dispatchEvent(new Event('input'))

网页客服的好处是 DOM 结构相对稳定,但平台改版(如淘宝 UI 升级)会导致选择器失效,需要持续维护。

六、多实例管理的工程挑战

同一个软件开多个窗口是最常见的场景。问题在于:

用户在微信开了两个聊天窗口,点聊天宝的话术,应该发给哪个窗口?

目前业界的主流解法:

  1. 最近焦点优先:话术发送给最近一次被激活的聊天窗口
  2. 手动指定:用户在聊天宝内手动切换"当前激活的聊天对象"
  3. 快捷键锚定:用户先在聊天软件内点击目标窗口,再用快捷键激活聊天宝发送

七、性能与稳定性

吸附功能的性能指标:

指标目标值说明
窗口识别延迟< 50ms用户感知不到
发送成功率> 99.5%失败后自动重试
内存占用< 100MB后台常驻
CPU 空闲占用< 0.5%不影响电脑性能

实现低空闲占用的关键是事件驱动而非轮询:只在窗口变化事件触发时重新识别,平时不消耗资源。

八、总结

80+ 平台的吸附适配,本质是一个超大规模的跨平台兼容性工程

  • 底层依赖各平台的原生 API(Win32 / macOS Accessibility / Android AccessibilityService)
  • 中层是统一的特征库抽象,新平台接入成本低
  • 上层是用户体验的精细化:剪贴板保护、多实例判断、发送成功率保障

对于想自建类似系统的团队,最大的工程挑战不是"能不能实现",而是如何在 80+ 个平台的持续迭代中维护这套系统的稳定性——这才是聊天宝十年积累的核心壁垒。

聊天宝快捷回复 副本 (1) - 副本.png