OpenScreen Windows 原生光标捕获诊断流水线:从 GetCursorInfo 采样器到 WGC Helper 与真实编辑器预览校验
本文基于 OpenScreen 仓库的 Windows native cursor test pipeline 文档展开,讲解两条专为 Windows 原生光标捕获设计的本地诊断流水线:一条是不启动应用、直接用 GetCursorInfo 采样真实系统指针并产出规范化 CursorRecordingData 与预览视频的独立诊断脚本;另一条是启动真实 Electron 应用、注入 fixture 视频与光标 sidecar 数据、从 OpenScreen 编辑器 UI 逐帧截屏并编码为 WebM 的预览校验脚本。读完本篇,你将掌握这两条流水线的完整命令、环境变量、产物结构与内置断言逻辑,并理解它们背后的 Windows 原生捕获后端(WGC helper)契约、可用性与 Click Bounce 已知缺口,从而在不走完整“录制→编辑→导出”人工循环的前提下快速迭代光标渲染代码。
1. 为什么需要这条诊断流水线
OpenScreen 在 Windows 上把光标从“烧录进视频”改为“独立采样 + 后期重建”:录制时系统指针被排除在画面之外,光标位置、形状、hotspot 与点击事件以旁路数据(sidecar)形式记录,再由编辑器预览和导出管线重新合成。这种架构让 Size、Smoothing、Motion Blur、Click Bounce 等光标效果可以在不重新录制的前提下反复调参,但也引入了新的验证难题——任何采样、坐标归一化或 hotspot 计算上的偏差,都会表现为预览/导出中光标漂移或形状错位。
因此 docs/testing/windows-native-cursor.md 定义了两条刻意保持“本地化、快速迭代”性质的开发者工具:它们只产出短视频与 JSON 报告,让光标改动可以在几分钟内被肉眼与数据双重检查,而不必每次都走一遍完整的人工 record/edit/export 循环。需要强调的是,文档明确指出这两条诊断不能完全替代端到端的人工录制校验——在合入光标相关改动之前,仍应在打包后的应用里做一次真实录制与导出。
2. 诊断一:原生光标采样器(test:cursor-native:win)
2.1 命令与行为概览
npm run test:cursor-native:win
该脚本对应 scripts/test-windows-native-cursor.mjs,在 package.json 中注册为 test:cursor-native:win。它不启动 OpenScreen 应用,而是完全独立地运行一次受控的光标捕获实验。脚本启动后会:
- 启动一个基于 Windows
GetCursorInfo的采样器(PowerShell 内注入 C# P/Invoke 类型); - 用
SetCursorPos驱动真实系统指针沿一条正弦波形路径移动(步长 120ms,在路径中点还会注入一次真实的左键按下/释放,用于验证点击事件捕获); - 捕获原生光标句柄(handle)、hotspot、光标位图资产,并把标准
IDC_*光标类型与句柄做映射识别; - 将样本写成规范化的
CursorRecordingData(sidecar 兼容格式); - 生成一个抽象预览视频(路径 / 资产 / hotspot 叠加渲染);
- 生成一个真实屏幕预览视频:以当前桌面截图为背景,把重建的光标叠加上去。
输出目录会打印在命令结果中,形如 C:\Users\<user>\AppData\Local\Temp\openscreen-cursor-native-...(代码中实际是 os.tmpdir() 下以 openscreen-cursor-native-${Date.now()} 命名的目录)。脚本在启动时强制检查 process.platform === "win32",非 Windows 平台直接报错退出。
2.2 采样器的 Windows API 实现细节
从 scripts/test-windows-native-cursor.mjs 的源码看,采样脚本通过 Add-Type 动态编译一个 OpenScreenCursorDiagnosticInterop 类,核心 P/Invoke 包括:
GetCursorInfo(ref CURSORINFO):每轮采样读取光标可见性标志(flags & 1)、hCursor句柄和ptScreenPos屏幕坐标;GetIconInfo/CopyIcon/DestroyIcon/DeleteObject:把光标句柄转成Icon,再绘制进 32bpp ARGB 位图并导出 PNG(base64 data URL),同时读取xHotspot/yHotspot;采样器对每个新句柄只抓一次资产($lastCursorId去重),并在 finally 中释放 GDI 对象;GetAsyncKeyState(0x01):轮询左键状态(高 16 位为“当前是否按下”,低 1 位为“两个采样之间是否发生过切换”);SetWindowsHookEx(WH_MOUSE_LL, ...):低级鼠标钩子统计WM_LBUTTONDOWN/WM_LBUTTONUP,用于捕获采样间隔之间的快速点击。
标准光标识别使用一张 IDC_* 常量表:arrow = 32512、text = 32513、wait = 32514、crosshair = 32515、up-arrow = 32516、resize-nwse = 32642、resize-nesw = 32643、resize-ew = 32644、resize-ns = 32645、move = 32646、not-allowed = 32648、pointer = 32649、app-starting = 32650、help = 32651,通过 LoadCursor(NULL, IDC_*) 加载后与采样到的句柄比对。对于无法映射到标准类型的光标,还有一段启发式分类(尺寸 24–64px、hotspot 位置区间、不透明像素占比等)尝试区分 closed-hand / open-hand。
真实指针驱动脚本 buildMousePathScript 会把主屏 bounds 内缩 80px,在垂直方向叠加 Sin(t * 2π) 波形(幅度 min(180, height/4)),在路径中点调用 mouse_event(MOUSEEVENTF_LEFTDOWN) + 12ms + MOUSEEVENTF_LEFTUP 制造一次合成点击;桌面截屏脚本则按 CURSOR_TEST_SCREEN_FRAME_INTERVAL_MS 间隔把主屏 CopyFromScreen 到 960 宽的 PNG 帧序列,并输出带时间戳的 frames.json。
2.3 产物结构与内置断言
输出目录中的关键文件:
| 文件 | 内容 |
|---|---|
report.json |
采样数、资产数、唯一光标句柄数、唯一位置数、左键按下/点击样本数、错误数、首尾样本、生成产物路径 |
cursor-recording-data.json |
sidecar 兼容的光标数据(下文详述) |
preview.webm |
抽象路径/资产/hotspot 预览(1x/2x/3x 位图重建 + 矢量替换光标) |
real-capture-preview.webm |
真实桌面截图背景 + 重建光标叠加 |
assets/*.png |
从 Windows 捕获的原始光标位图 |
events.json、report.html、real-capture-report.html |
原始事件流与可视化诊断页(Playwright 无头 Chromium 打开后 captureStream 录制为 WebM) |
cursor-recording-data.json 的结构由 toRecordingData 生成:version: 2、provider 为 "native"(无资产时为 "none");每个样本包含相对首个样本的 timeMs、按主屏 bounds 归一化的 cx/cy、assetId(即光标句柄)、visible、cursorType,以及由左键状态转移推导的 interactionType(click / mouseup / move);每个资产包含 id、platform: "win32"、base64 PNG、宽高、hotspotX/Y 与 scaleFactor: 1。
脚本最后会执行 assertReport,即一组硬性验收条件,任何一条不满足都会以非零退出:
sampleCount >= floor(DURATION_MS / SAMPLE_INTERVAL_MS / 3)(采样连续性底线);- 至少存在可见光标样本、至少捕获到 1 个光标资产 PNG;
uniquePositionCount >= 4(指针确实发生了可观察的移动);errorCount === 0;- 左键点击交互必须被观察到(
leftButtonPressedSampleCount > 0且clickSampleCount > 0)。
2.4 环境变量覆盖
$env:CURSOR_TEST_DURATION_MS = "3000"
$env:CURSOR_TEST_SAMPLE_INTERVAL_MS = "16"
$env:CURSOR_TEST_SCREEN_FRAME_INTERVAL_MS = "80"
$env:CURSOR_TEST_OUTPUT_DIR = "C:\temp\openscreen-cursor-test"
npm run test:cursor-native:win
结合源码,完整的取值与默认值如下(非法或非正值会被警告并回退到默认值):
| 环境变量 | 默认值 | 含义 |
|---|---|---|
CURSOR_TEST_DURATION_MS |
1800 |
指针驱动与桌面截屏的总时长(ms) |
CURSOR_TEST_SAMPLE_INTERVAL_MS |
25 |
GetCursorInfo 采样间隔(ms) |
CURSOR_TEST_SCREEN_FRAME_INTERVAL_MS |
100 |
桌面截图帧间隔(ms) |
CURSOR_TEST_READY_TIMEOUT_MS |
5000 |
等待采样器 ready 事件的超时(ms) |
CURSOR_TEST_OUTPUT_DIR |
系统临时目录下 openscreen-cursor-native-<时间戳> |
产物输出目录 |
3. 诊断二:OpenScreen 真实编辑器预览捕获(capture:openscreen-preview)
npm run capture:openscreen-preview
该脚本对应 scripts/capture-openscreen-preview.mjs,与上一条诊断形成互补:它启动真实的 Electron 应用,注入 fixture 视频与光标 sidecar 数据,打开编辑器,从真实的 OpenScreen 预览 UI 逐帧截图,再编码成 WebM。它回答的问题是:诊断流水线里重建出来的光标行为,在真实编辑器里是否渲染得一模一样。
从源码看,其完整流程是:
- Sidecar 发现:默认扫描系统临时目录中所有
openscreen-cursor-native-*子目录里的cursor-recording-data.json,取 mtime 最新者——即最近一次npm run test:cursor-native:win的产物;找不到时直接报错提示先运行采样诊断。可用CURSOR_RECORDING_DATA_PATH强制指定具体文件(不存在会抛错):
$env:CURSOR_RECORDING_DATA_PATH = "C:\path\to\cursor-recording-data.json"
npm run capture:openscreen-preview
- 构建检查:要求
dist-electron/main.js与dist/index.html存在(即先跑过npm run build-vite);设置OPENSCREEN_PREVIEW_SKIP_BUILD=true会跳过构建并只做存在性检查,否则脚本会自动通过cmd.exe触发npm run build-vite。 - 注入 fixture:把 tests/fixtures/sample.webm 复制为
openscreen-preview-fixture.webm,并把 sidecar 复制为同名的.cursor.json,再通过window.electronAPI.setCurrentVideoPath+switchToEditor让应用直接进入编辑器。 - 逐帧截图:等待编辑器窗口(URL 含
windowType=editor)中的video与canvas选择器就绪,设置 1280×800 视口,静音并把currentTime依次置为index / FPS,每帧等待 40ms 后对页面截图保存为frames/frame-XXXX.png。 - 编码 WebM:启动无头 Chromium,把全部 PNG 帧按设定 FPS 绘到 canvas 上,用
captureStream+MediaRecorder(VP9 WebM)编码出openscreen-preview.webm。
$env:OPENSCREEN_PREVIEW_SKIP_BUILD = "true"
$env:OPENSCREEN_PREVIEW_FRAME_COUNT = "120"
$env:OPENSCREEN_PREVIEW_FPS = "30"
$env:OPENSCREEN_PREVIEW_OUTPUT_DIR = "C:\temp\openscreen-preview"
npm run capture:openscreen-preview
| 环境变量 | 默认值 | 含义 |
|---|---|---|
OPENSCREEN_PREVIEW_SKIP_BUILD |
未设置(即自动执行 build-vite) |
置为 "true" 时跳过构建,仅校验产物存在 |
OPENSCREEN_PREVIEW_FRAME_COUNT |
90 |
从编辑器捕获的帧数 |
OPENSCREEN_PREVIEW_FPS |
30 |
逐帧定位间隔(index / FPS 秒)与输出视频帧率 |
OPENSCREEN_PREVIEW_OUTPUT_DIR |
系统临时目录下 openscreen-real-preview-<时间戳> |
输出目录 |
CURSOR_RECORDING_DATA_PATH |
最新一次采样诊断产物 | 强制指定 sidecar 文件 |
输出目录中的文件:openscreen-preview.webm(真实 OpenScreen 编辑器预览视频)、frames/*.png(逐帧截图)、report.json(含 outputDir、sourceCursorRecordingDataPath、fixtureVideoPath、outputVideoPath、frameCount、fps)。
4. 两条诊断共同验证的能力
按照原文档,两条脚本组合起来可以快速检查:
- Windows 光标样本是否可见且连续(
visibleSampleCount、sampleCount底线断言); - 原生 hotspot 在缩放到
3x时是否仍然锚定正确(预览页同时绘制 1x/2x/3x 位图重建与红色十字 hotspot 标记); - 标准 Windows 光标是否通过
IDC_*被正确识别(采样器句柄比对表); - 高质量 SVG 光标替换是否跟随原生 hotspot(诊断页中的 “pretty 3x” 矢量箭头即以同一 hotspot 为锚点绘制);
- 真实 OpenScreen 预览是否与诊断流水线渲染出一致的光标行为(
capture:openscreen-preview与preview.webm的对比依据)。
再次强调文档给出的边界:这两条诊断不是完整端到端人工录制校验的替代品。光标改动在发布前,还需要在打包应用里完成一次真实 capture session 与导出。
5. 已知缺口:Click Bounce 尚未可见生效
原文档明确将 Windows 原生光标的 Click Bounce 标记为 backlog:Size、Smoothing、Motion Blur 可以通过预览/导出验证,但 Click Bounce 在打包应用的人工测试中没有表现出可见效果。当前诊断只能观察到合成的点击元数据(leftButtonPressed/clickSampleCount),这不足以验证真实 OpenScreen record → preview → export 路径上的弹跳动画。
该问题的完整跟踪项位于 docs/engineering/windows-native-recorder-roadmap.md 的 “Native Cursor Click Bounce Is Not Visibly Applied” 小节(Backlog 部分),其中记录了已尝试的手段(向样本加入 interactionType: "click" | "mouseup" | "move"、GetAsyncKeyState 轮询与低比特位快速点击捕获、WH_MOUSE_LL 鼠标钩子实验、把采样器改为临时 .ps1 文件以规避 Windows 命令行长度限制)以及当前诊断结论:采样元数据可以被记录,但尚未转化为真实打包应用中可见的 bounce 效果;后续排查方向包括检查真实 .cursor.json sidecar 中点击的 timeMs 是否准确、对比原生 DOM 光标路径与旧版 PixiCursorOverlay 的点击视觉状态,以及在必要时把点击事件下沉到小型原生光标 helper。
6. 背景:Windows 原生捕获后端(WGC Helper)
理解上述诊断的价值,需要知道它们所处的捕获架构。应用现已把 Windows 录制从 Electron getDisplayMedia 切换到外部 WGC helper 进程,目的正是消除此前导致重建光标在预览/导出路径中漂移的“坐标与时钟分裂”。
6.1 可用性规则
- Windows 10 build 19041 或更新版本;
- 存在可用的 helper 可执行文件。
前者在 electron/ipc/handlers.ts 中由 isWindowsGraphicsCaptureOsSupported 实现:解析 process.getSystemVersion() 的第三段作为 build 号,判断 build >= 19041,不满足时 IPC 侧会返回 “Windows Graphics Capture requires Windows 10 build 19041 or newer.” 错误。
helper 的可执行文件按以下顺序解析(见 electron/native/README.md):
OPENSCREEN_WGC_CAPTURE_EXE(本地开发与诊断);electron/native/wgc-capture/build/wgc-capture.exe(本地 Ninja 构建产物);electron/native/wgc-capture/build/Release/wgc-capture.exe(多配置构建产物);electron/native/bin/win32-x64/wgc-capture.exe或electron/native/bin/win32-arm64/wgc-capture.exe(打包预构建)。
helper 目前实现:显示器/窗口视频捕获、系统音频环回、默认麦克风捕获、Media Foundation 网络摄像头捕获,以及针对 NVIDIA Broadcast 等特定虚拟摄像头的 DirectShow 回退。摄像头画面以右下角画中画叠加进主 MP4,黑色预热帧会在首个可见帧出现前被忽略。光标采样的原生实现位于 electron/native/wgc-capture/src/cursor-sampler.cpp。
6.2 构建与 smoke 测试
本地构建 OpenScreen 的 helper:
npm run build:native:win
构建会把 CMake 产物写到 electron/native/wgc-capture/build/wgc-capture.exe,并复制到 electron/native/bin/win32-x64/wgc-capture.exe。直接对 helper 做 smoke 测试:
npm run test:wgc-helper:win
npm run test:wgc-helper:win -- --capture-cursor
npm run test:wgc-window:win
npm run test:wgc-audio:win
npm run test:wgc-mic:win
npm run test:wgc-mixed-audio:win
npm run test:wgc-webcam:win
这些脚本名在 package.json 中统一映射到 scripts/test-windows-wgc-helper.mjs 及其不同的 CLI 参数(--window、--system-audio、--microphone、--webcam 等)。如需对某个特定摄像头/麦克风做手动验证,还可分别设置 OPENSCREEN_WGC_TEST_WEBCAM_DEVICE_NAME / OPENSCREEN_WGC_TEST_MICROPHONE_DEVICE_NAME(见 electron/native/README.md)。
6.3 指向自定义 helper 与进程契约
若要在本地诊断中使用另一个兼容 helper,把环境变量指向该可执行文件再启动开发态应用即可:
$env:OPENSCREEN_WGC_CAPTURE_EXE = "C:\path\to\wgc-capture.exe"
npm run build-vite
npm run dev
helper 的进程契约:应用以一个 JSON 配置参数启动该进程;helper 输出 JSON 生命周期事件,并打印遗留的 Recording started 标记;应用通过 stdin 发送 stop(stop\n)来结束录制;helper 最终打印 Recording stopped. Output path: <path>。V2 JSON 配置形态(含 schemaVersion、sourceType/sourceId、outputPath、fps、captureSystemAudio/captureMic、麦克风/摄像头设备与增益参数、outputs.screenPath 等字段)的完整示例与字段说明,见 electron/native/README.md。
7. 实操建议:推荐的验证顺序
结合两条诊断与 helper smoke 测试,一个典型的光标改动验证流程是:
- 修改光标采样/渲染相关代码后,先跑
npm run test:cursor-native:win,让assertReport的连续性、可见性、资产、移动、点击五类断言自动把关,并打开real-capture-preview.webm目测 1x/2x/3x 与矢量替换光标的 hotspot 锚定; - 用同一份最新的
cursor-recording-data.json跑npm run capture:openscreen-preview(可配OPENSCREEN_PREVIEW_FRAME_COUNT=120),对比openscreen-preview.webm与诊断预览是否行为一致; - 涉及捕获链路时,依次执行
test:wgc-helper:win系列命令确认 helper 各通道(含--capture-cursor)正常; - 合入前,在打包应用中完成一次真实录制会话与导出的人工端到端校验;
Click Bounce在未关闭 roadmap 中的 backlog 项之前,不应被视为已交付能力。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00