跳到主要内容

录制引擎:一段示范到底记了什么、怎么记

本章讲 legacy/openadapt/record.pylegacy/openadapt/models.py:录一次示范,系统内部如何把「人的动作」变成数据库里的行。

1. 它要解决的小问题

录制看似简单,难点在速度完整性冲突:鼠标移动每秒几十上百个事件,截图和窗口无障碍(a11y)信息又慢又大。如果边采边写盘,采集线程会被 I/O 卡住,丢事件。OpenAdapt 的答案是经典的生产者/消费者:采集只管往队列塞,写盘另开线程慢慢消费。

2. 思路/直觉:一个入口队列,多个出口队列

读线程(生产者,各管一类源) 处理线程 写线程(消费者,各管一张表)
┌───────────────────────┐ ┌────────────────────────┐
│ read_keyboard_events │──┐ ┌─►│ write_action_event │
│ read_mouse_events │──┤ ├─►│ write_screen_event │
│ read_screen_events │──┼──► event_q ──► process_events ─┼─►│ write_window_event │
│ read_window_events │──┤ (单一入口) (分类分发) ├─►│ write_browser_event │
│ run_browser_event_server──┘ └─►│ write_video_event │
└───────────────────────┘ └────────────────────────┘

怎么读这张图: 左边每个源一个线程,全部塞进同一个 event_q;中间 process_events 按类型拆开,分发到右边每类一条写队列;写线程各自落 SQLite。采集和写盘从此互不阻塞。

3. 真实实现

3.1 主函数 record 把这些线程都拉起来

record() 创建一个 event_q 和多个 SynchronizedQueue,然后为每个源起一个 threading.Thread:

  • legacy/openadapt/record.py:1312-1387 —— 建 event_qscreen_write_qaction_write_qwindow_write_qbrowser_write_qvideo_write_q,再依次 start()read_window_events / read_screen_events / read_keyboard_events / read_mouse_events(浏览器事件受 config.RECORD_BROWSER_EVENTS 开关控制)。
  • 处理线程见 record.py:1394 起的 process_events,它是分发中枢。

3.2 鼠标/键盘怎么进队列

鼠标读线程本质是把 pynput 的回调接到 event_q:on_move/on_click/on_scroll(record.py:578:598:633)把参数打包成 dict,经 trigger_action_event(record.py:555)塞进队列。键盘同理走 read_keyboard_events 里的 on_press/on_release(record.py:943:994)。

3.3 每个动作最终落成一行 ActionEvent

数据模型是 SQLAlchemy ORM。ActionEvent(legacy/openadapt/models.py:129)是核心行,字段覆盖:动作名 name、时间戳、鼠标坐标 mouse_x/mouse_y、按键 key_name/key_char/key_vk,以及和 Screenshot/WindowEvent/BrowserEvent 的外键。

一个值得注意的字段:active_segment_description(models.py:152)—— 后面视觉重定位(第 4 章)就靠它记「这次点击点的是哪个元素」。

4. 关键细节/坑

  • 按键的三重表示。 一个键同时存 key_name(命名键如 cmd)、key_char(字符)、key_vk(虚拟键码),_key()(models.py:238)按优先级挑一个还原成 pynput 的 Key 对象——因为不同平台/不同键给的信息不一样,存全了才好跨平台还原。
  • 父子事件(children)。 ActionEvent 有自引用关系 children(models.py:221)。后处理时被合并掉的原始事件不会删,而是挂到合并后事件的 children 下(见第 2 章),保留可追溯性。
  • 截图可以来自视频。 Screenshot(models.py:924)既能存 PNG 二进制,也能通过 FrameCache(models.py:833)从录制视频里按时间戳抽帧(RECORD_VIDEO vs RECORD_IMAGES,见 record.py:1292 的断言),省存储。

5. 代码地图

主题文件路径符号
录制主流程legacy/openadapt/record.pyrecord
事件分发legacy/openadapt/record.pyprocess_eventsprocess_event
鼠标回调legacy/openadapt/record.pyon_moveon_clickon_scrolltrigger_action_event
键盘回调legacy/openadapt/record.pyread_keyboard_events(内 on_press/on_release)
写盘消费者legacy/openadapt/record.pywrite_action_eventwrite_screen_eventwrite_window_event
动作行模型legacy/openadapt/models.pyActionEvent_key
截图/抽帧legacy/openadapt/models.pyScreenshotFrameCache