screenpipe 会议检测状态机评估框架(screenpipe-meeting-eval)实战:从 TOML 场景回放到真实日志轨迹重放

发布时间:2026/9/13 21:28:16
screenpipe 会议检测状态机评估框架(screenpipe-meeting-eval)实战:从 TOML 场景回放到真实日志轨迹重放 screenpipe 会议检测状态机评估框架screenpipe-meeting-eval实战从 TOML 场景回放到真实日志轨迹重放【免费下载链接】screenpipeYC (S26) | Open Computer History | Record your screen continuously locally and provide context to your agents (Claude, Codex, Openclaw, Hermes, Runner...)项目地址: https://gitcode.com/GitHub_Trending/sc/screenpipe本篇技术指南以仓库中 crates/screenpipe-meeting-eval/evals/README.md 为核心系统讲解 screenpipe 用于回归防护「会议检测状态机」的评估框架如何用 TOML 脚本化场景驱动生产级advance_state状态机、如何解读 meeting_count / flap_count / end_latency 等质量指标、如何用真实日志轨迹重放复现线上 bug如 2026-05-11 的 Arc/Meet Active⇌Ending 抖动问题以及未来如何录制不含隐私内容的真实轨迹。读完你将掌握一套可复制、可扩展的「状态机级时间序列回归测试」方法论与完整 CLI 用法。为什么需要这个评估框架单次调用单元测试的盲区screenpipe 的会议检测状态机经过约 60 次提交的边缘场景修补Zoom 空闲误启动、Discord 浏览器端 Mute 常显、Arc 标签页切换宽限、音频输出扩展等但一直没有覆盖「状态随时间迁移」的端到端回归测试。核心痛点在于现有单元测试每次只调用一次advance_state因此无法暴露跨多个扫描周期才显现的缺陷。2026-05-11 发生的 Arc 浏览器中 Meeting 72/73 的Active⇌Ending 抖动flapbug正是这类问题的典型代表工具栏自动隐藏导致控件信号反复出现与消失状态机在 Active 与 Ending 之间来回震荡用户必须手动切换快捷键才能真正结束会议。这个 bug 对单次调用的单元测试完全不可见却能在几十秒的真实扫描序列中清晰复现。该评估框架crate 名screenpipe-meeting-eval声明于 crates/screenpipe-meeting-eval/Cargo.toml正是为让这一类 bug 变得可见而存在并防止历史修复随时间腐烂回归。从源码结构看它刻意只做一件事把脚本化的扫描轨迹(t, in_call, has_audio)序列回放到生产状态机screenpipe_engine::meeting_watcher::advance_state中再对会议次数、抖动次数、结束延迟等指标与场景预期做断言。测试分层与范围界定只测 Layer 3会议检测管线按层次划分本框架只覆盖其中一层Layer 1 — 信号匹配signal matching由 crates/screenpipe-engine/src/meeting_watcher 内部的 110 个单元测试覆盖不在本框架范围。Layer 2 — AX 树扫描AX-tree scanning未来工作计划采用脱敏 AX 树 fixture 格式加scan_process重放器见 crates/screenpipe-meeting-eval/src/lib.rs 的 crate 级文档注释。Layer 3 — 纯状态机本框架的唯一目标即对advance_state做时间序列级别的回归测试。核心原理用模拟时间驱动生产状态机在 crates/screenpipe-meeting-eval/src/lib.rs 中run_scenario是整个评估流程的心脏。其关键设计有三点直接复用生产代码use screenpipe_engine::meeting_watcher::{advance_state, MeetingState, ScanResult, StateAction}——评估走的是与生产完全相同的状态迁移逻辑不复制任何状态机实现这与 screenpipe-audio-eval 复用 screenpipe-audio 驱动说话人分离链路的做法一致见 Cargo.toml 注释。合成 ScanResult每个 tick 构造一个ScanResult其中app_name来自场景[meta].appis_in_call/signals_found由 tick 的in_call决定。每次扫描都传一个应用条目——这与生产扫描器「每 tick 扫描所有正在运行的会议应用、无论是否发现控件」的行为一致。时间回拨patch_timing状态机内部用Instant计时如Confirming { since }、Ending { since }、Active { last_seen }。评估框架通过Instant::now().checked_sub(elapsed_sim)把时间字段回拨到模拟时刻从而在不修改任何生产代码或常量的前提下用模拟墙钟驱动状态机。assert_uptime会预先校验进程运行时间是否足够回拨不够则大声 panic 并提示「将场景压缩到 ≤600s 模拟时间或换一台长时间运行的机器」而不是静默产出错误指标。生产状态机的四个状态定义在 crates/screenpipe-engine/src/meeting_watcher/shared/state.rs状态含义进入条件源码注释Idle未检测到会议初始态Confirming 超时回落Ending 超时结束应用进程退出Confirming疑似会议等待第二次扫描确认扫描发现通话控件信号15 秒内未获确认则回落 IdleActive会议进行中连续第二次扫描确认CONFIRM_TIMEOUT 15sEnding控件消失等待结束Active 扫描不到控件30s 内控件重现则回到 Active该文件还定义了与场景参数直接相关的生产常量ENDING_TIMEOUT 30s原生应用、ENDING_TIMEOUT_BROWSER 300s浏览器应用因为标签切换会隐藏 AX 控件、REENTRY_HYSTERESIS_SCANS 2重入迟滞需要连续 2 次可见扫描才能从 Ending 回到 Active防止单次瞬时闪烁翻转状态。is_browser_app判断逻辑位于 crates/screenpipe-engine/src/meeting_watcher/shared/ignore.rs它决定了 Ending 宽限是 300s 还是 30s——这也正是场景[meta].app字段的核心作用。如何运行# 运行全部种子场景每个场景在 stdout 输出一行 JSON cargo run --release -p screenpipe-meeting-eval --bin screenpipe-eval-meeting-state -- \ crates/screenpipe-meeting-eval/evals/scenarios/*.toml # 单元测试含内嵌场景 cargo test -p screenpipe-meeting-evalCLI 行为见 crates/screenpipe-meeting-eval/src/bin/eval_state.rs每个场景向 stdout 输出一行 JSONnewline-delimited对应一个 Metrics 对象任何非xfail场景未通过预期断言时CLI 以非零退出码退出用于 CI 门禁传入--no-gate可关闭退出码门禁——文档注释说明这在「收紧预期前采集基线数据」时很有用此时失败场景仍会打印到 stderr但进程保持 0 退出失败时 stderr 会输出FAIL: scenario — N failure(s): [...]xfail场景则标记为xfail (expected)不影响门禁。场景格式TOML详解每个种子场景是一个 TOML 文件完整格式如下来自原文档[meta] name ... description ... app Arc # ScanResult.app_name; determines is_browser scan_interval_seconds 5.0 # matches prod ACTIVE_SCAN_INTERVAL # Tick specs — each expands into zero or more (t, in_call) ticks. # Types: single, constant, flap. [[ticks]] type single t 5 in_call true [[ticks]] type constant from 10 to 600 in_call true [[ticks]] type flap # alternating visible/hidden — models toolbar auto-hide from 10 to 2700 visible_seconds 27 hidden_seconds 8 # Optional: timestamps where output audio is flowing. # Triggers the audio-extension branch in advance_state when in Ending. [[audio_ranges]] from 5 to 1800 [expected] true_hangup_t_seconds 1800 # used to compute end_latency / early_end meeting_count 1 final_state Idle flap_count_max 5 end_latency_seconds_max 360 early_end_max 0 # Optional: declare an in-flight bug. Test reports it but doesnt gate # on it. Flip off once the fix lands — this then becomes a regression # test. [xfail] reason ...各字段的源码级语义[meta]name场景名会出现在输出 JSON 的scenario字段中app作为合成ScanResult.app_name在状态机中决定is_browser_app进而控制 Ending 宽限期浏览器 300s vs 原生 30sscan_interval_seconds模拟扫描器运行频率默认 5.0与生产常量ACTIVE_SCAN_INTERVAL定义于 crates/screenpipe-engine/src/meeting_watcher/ui_scan/mod.rs值为Duration::from_secs(5)保持一致用于展开 Flap/Constant 生成器。[[ticks]]Tick 规格每种展开为零个或多个具体(t, in_call)刻度single { t, in_call }在精确时刻t秒产生一个 tick见TickSpec::Singleconstant { from, to, in_call }在[from, to]区间内按扫描间隔重复恒定in_call值TickSpec::Constantflap { from, to, visible_seconds, hidden_seconds, phase? }在[from, to]区间内交替可见/隐藏控件状态用于模拟浏览器网页会议客户端如 Arc 中的 Google Meet的工具栏自动隐藏。源码实现中每个周期的前hidden_seconds视为隐藏、其余视为可见phase可选默认 0 从隐藏窗口开始用于平移周期相位in_call (offset hidden_seconds)其中offset (t - from phase).rem_euclid(period)。[[audio_ranges]]可选标记输出音频流动的时间段。当状态机处于 Ending 且has_audio为真时会触发advance_state中的音频扩展audio-extension分支——即「输出音频 语音活动让会议保持存活」的 keep-alive 逻辑见state.rs中output_audio_keeps_meeting_alive/audio_or_calendar_keepalive的注释说明recent_output_chunk单独存在会把会议永久钉在活动态因此还必须叠加 RMS 门控的recent_voice_activity。[expected]预期断言全部可选true_hangup_t_seconds用户真实挂断时间用于计算end_latency/early_end不设置则不做延迟检查meeting_count期望的会议数用于发现误启动 / 漏检final_state期望的最终状态取值Idle/Confirming/Active/Endingflap_count_max单场会议内 Ending→Active 抖动次数的上限end_latency_seconds_max结束延迟上限early_end_max提前结束次数的上限正常应为 0。[xfail]可选声明一个尚未修复的在途 bug。带xfail的场景会照常运行并报告断言失败但不会触发 CI 门禁失败一旦修复落地把xfail移除即自动转变为一条回归测试。这种「先立靶子、修好后转正」的模式让已知缺陷也能被持续监控。两个真实种子场景对照evals/scenarios/arc_meet_toolbar_autohide.toml复现 2026-05-11 的 Meeting 72/73Arc 中 Google Meet 通话时工具栏每约 35 秒自动隐藏由于单次扫描发现控件就会无迟滞地把 Ending 翻回 Active状态机在一场通话内反复抖动用户必须手动切换快捷键才能结束会议。真实通话超过 70 分钟场景用 5 分钟抖动窗口压缩flap 计数随时长线性增长5 分钟已足够保守确保 ≤600s 模拟时间、可在冷启动 CI runner 上运行[meta] name arc_meet_toolbar_autohide description Google Meet in Arc, toolbar auto-hides during a 5-min flap window app Arc scan_interval_seconds 5.0 [[ticks]] type single t 0 in_call false [[ticks]] type single t 5 in_call true # 5-min flap loop: 27s visible / 8s hidden, repeating [[ticks]] type flap from 10 to 310 visible_seconds 27 hidden_seconds 8 # user hangs up at 310s; 200s of no-controls after (300s browser grace buffer) [[ticks]] type constant from 315 to 700 in_call false [expected] true_hangup_t_seconds 310 meeting_count 1 final_state Idle flap_count_max 3 end_latency_seconds_max 360 early_end_max 0 [xfail] reason Toolbar auto-hide flap bug; hysteresis fix not yet landed (Meeting 72/73, 2026-05-11)evals/scenarios/zoom_native_clean_call.toml是快乐路径基线原生 Zoom 通话全程控件可见、干净挂断状态机应恰好产生一场会议结束延迟 ≤ 30s 宽限场景设为 60s 上限零抖动[meta] name zoom_native_clean_call description Zoom call, controls visible throughout, clean hang-up app zoom.us scan_interval_seconds 5.0 [[ticks]] type single t 0 in_call false [[ticks]] type constant from 5 to 360 in_call true [[ticks]] type constant from 365 to 600 in_call false [expected] true_hangup_t_seconds 360 meeting_count 1 final_state Idle flap_count_max 0 end_latency_seconds_max 60 early_end_max 0场景加载时的结构校验load_scenario在解析后立即执行validate_scenario见 lib.rs在加载期而非回放中途拦截畸形场景scan_interval_seconds必须 0single.t与flap/constant的from不得为负且to fromflap的visible_seconds、hidden_seconds必须 0audio_ranges的区间必须合法。校验失败会给出带下标的具体错误信息避免产出令人困惑的指标。另外expand_ticks展开时对区间上界加了 1ns 容差确保端点即使在 f64 累加误差下也能确定性落地负的t被钳制为 0因为状态机没有「场景开始之前」的概念。最终 tick 按时间排序后转换为Duration序列。输出指标解读每个场景回放后输出一行 JSON格式如下来自原文档的示例输出{ scenario: arc_meet_toolbar_autohide, meeting_starts: 1, meeting_ends: 0, final_state: Active, flap_count: 77, flap_count_controls: 77, flap_count_audio: 0, end_latency_seconds: null, early_end_count: 0, total_ticks: 542, xfail: Toolbar auto-hide flap bug; hysteresis fix not yet landed, assertion_failures: [ meeting_count: want 1 got 1, final_state: want \Idle\ got \Active\, flap_count: max 3 got 77 ] }字段含义对应Metrics结构体meeting_starts / meeting_ends状态机触发的StartMeeting/EndMeeting动作次数对应StateAction枚举的两个变体final_stateIdle|Confirming|Active|Ending之一由variant_name从状态枚举映射而来见 lib.rsflap_count单场会议内部的 Ending→Active 转移总数合并计数。进一步拆分为flap_count_controls控件重新出现导致的抖动与flap_count_audio音频仍在流动导致的抖动。源码注释给出解读准则controls 抖动高 检测脆弱brittleaudio 抖动高 合法但需跟踪漂移end_latency_seconds用户真实挂断时刻expected.true_hangup_t_seconds与EndMeeting触发之间的墙钟差值若两者任一未声明则为null。负值意味着在用户挂断前就结束了会被计入 early_endearly_end_count在用户真实挂断之前触发的EndMeeting次数正常应为 0total_ticks回放的总 tick 数expand_ticks展开的结果xfail若有携带[xfail].reason文本assertion_failures与[expected]不一致的断言列表。注意Metrics::check只报告不匹配的项全部通过时该列表为空lib.rs 中有专门的回归测试metrics_check_only_reports_mismatches防止自我引入的污染 bug。值得说明的是示例输出中的assertion_failures显示meeting_count: want 1 got 1也被列出——这是预期输出快照的一部分反映了断言失败信息携带「期望值与实际值」的原始格式。种子场景清单[crates/screenpipe-meeting-eval/evals/scenarios](https://link.gitcode.com/i/e06bc9a1c54d6acad71e426c0160cfd4)目录共 11 个种子场景原文档表格列出核心 5 个场景守护点zoom_native_clean_call.toml快乐路径原生 Zoom、控件可见、干净挂断。基线。confirming_drops_no_meeting.toml瞬时误报信号不应创建会议Confirming 超时回落 Idle。browser_tab_switch_with_audio.tomlbe6a6f148/d8ba1dad3的回归标签切换隐藏控件音频保持 Active。一场会议而不是多场。native_zoom_minimized_with_audio.toml4e784f620#2536的回归原生应用最小化音频使其保持存活。arc_meet_toolbar_autohide.tomlXFAIL— Meeting 72/732026-05-11Arc Meet 工具栏自动隐藏导致 Active⇌Ending 抖动。迟滞修复落地后移除 xfail。其余种子场景覆盖更多真实边缘情况bluetooth_dropout_midcall.toml通话中蓝牙断开、call_ends_cleanly_audio_stops.toml干净结束且音频停止、lid_close_audio_continues.toml合盖但音频继续、music_playback_not_a_meeting.toml音乐播放不应判定为会议、rapid_focus_switch_storm.toml快速焦点切换风暴、screen_lock_unlock_midcall.toml通话中锁屏/解锁。lib.rs 中内嵌的单元测试还覆盖了快乐路径clean_native_call_one_meeting、flap 计数拆分flap_pattern_increments_controls_counter、音频扩展保持存活audio_extension_keeps_meeting_alive验证音频 keep-alive 是无抖动的meeting 恰好开始/结束一次、零 flap、瞬时信号不产生会议transient_signal_no_meeting守护 2bbbc6cbd——Google Calendar「Join with Google Meet」文本误报、非法输入校验validator_rejects_bad_inputs、trace 回放链路trace_replay_runs_through_state_machine与音频区间折叠audio_ranges_collapse_correctly。重放真实轨迹把线上日志变成回归场景TOML 场景是手写的理想化模型而真实世界远比模型复杂。为此框架提供第二个二进制screenpipe-eval-meeting-replay-trace消费 JSONL 轨迹文件——每行一个{t, in_call, has_audio?}t为自轨迹起点起的秒数——送入与 TOML 场景完全相同的状态机cargo run --release -p screenpipe-meeting-eval --bin \ screenpipe-eval-meeting-replay-trace -- \ crates/screenpipe-meeting-eval/evals/traces/meeting72_arc_real.jsonl \ --app Arc --name meeting72 --true-hangup-t 4254CLI 参数见 crates/screenpipe-meeting-eval/src/bin/replay_trace.rstraceJSONL 轨迹文件路径空行与#开头的行会被跳过load_trace实现方便内联注释--app合成ScanResult中使用的应用名决定浏览器/原生宽限Arc/Chrome 等为浏览器--name输出 JSON 中scenario字段的标签默认trace--true-hangup-t可选地面真值挂断时刻轨迹起点起的秒数启用end_latency与early_end指标。该二进制输出与 TOML 路径完全相同的 JSON 指标结构因此真实日志重放可以直接与任意手写场景做 diff 对比。底层scenario_from_events会把轨迹事件展开为 TOML 同构的Scenario真实扫描器约每 5s tick 一次且两次迁移之间in_call结果相同而轨迹文件只记录迁移边界若天真重放会跳过喂给重入迟滞REENTRY_HYSTERESIS_SCANS 2的中间扫描。因此每个边界之后都会按扫描间隔补一段Constant区间直到下一个边界——即「densification」让迟滞计数器看到与生产循环一致的扫描节奏。Meeting 72 轨迹分析evals/traces/meeting72_arc_real.jsonl是从~/.screenpipe/screenpipe-app.2026-05-11.log中提取的——正是触发那次 bug 调查的、用户在 Arc 中长达 70 分钟的 Google Meet 通话日志。用今天的状态机重放它可复现{scenario:meeting72,meeting_starts:1,meeting_ends:0, final_state:Ending,flap_count:23,flap_count_controls:23, end_latency_seconds:null}23 次 controls 抖动、从未自然结束——从生产数据确认了 bug 形态。一旦修复落地并重新提取修复后的日志同样的重放应产出flap_count_controls ≤ 3, final_stateIdle, end_latency≈300s。这段描述也印证了REENTRY_HYSTERESIS_SCANS 2这一迟滞常量的设计动机见 state.rs 注释。运行时间限制Uptime caveat评估框架通过回拨Instant::now()在模拟时间上驱动advance_state这意味着进程运行时间必须 ≥ 场景长度TOML 场景被限制在约 600s 以内保证冷启动 CI runner 安全run_scenario开头会调用assert_uptime不足则直接 panic 并提示Meeting 72 轨迹长达 4254s——在开发机上数小时 uptime没问题但在刚启动的 CI runner 上会 panic。因此重放二进制定位为开发/调查工具CI 只门禁短的 TOML 场景。未来方向录制不含隐私内容的真实轨迹为了在不泄露内容的前提下从真实使用中增长覆盖率原文档规划了后续工作在run_meeting_detection_loop中增加一个 debug 构建的轨迹转储器trace dumper每次扫描向~/.screenpipe/meeting_traces/追加一行 JSON。关键约束是只记录规范化的信号类型——绝不记录原始 AX 节点名、URL 或窗口标题。这样用户可以直接分享轨迹文件它通过advance_state重放即可精确复现状态机当时看到的内容。该功能作为后续工作跟踪详见 crates/screenpipe-meeting-eval/src/lib.rs 的 crate 级文档。小结把「时间序列回归」纳入会议检测质量保障从工程方法论角度screenpipe-meeting-eval 提供了一个非常清晰的模式单次调用单元测试之外必须补时间序列级测试——状态机类缺陷抖动、延迟结束、提前结束本质上跨越多个扫描周期只有按时间回放才能暴露复用生产状态机而非重建模型——通过时间回拨驱动真实advance_state测试的就是线上代码本身脚本化场景 真实轨迹双轨制——TOML 场景负责 CI 门禁与理想化边界JSONL 轨迹重放负责还原线上真实形态二者共享同一套指标结构可互相 diffxfail 机制管理已知缺陷——尚未修复的 bug 先立靶持续监控修复落地后转正为回归测试防止历史教训腐烂。这套评估框架让「~60 次边缘修复」的历史资产拥有了可持续的防回归保障也为未来任何会议检测状态机改动提供了快速、可量化、可进 CI 的安全网。【免费下载链接】screenpipeYC (S26) | Open Computer History | Record your screen continuously locally and provide context to your agents (Claude, Codex, Openclaw, Hermes, Runner...)项目地址: https://gitcode.com/GitHub_Trending/sc/screenpipe创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考