Anarlog 的 Vexa v0.12.18 Google Meet 行为矩阵:准入/运行时分类器与生命周期终止原因全解
【免费下载链接】anarlogOpen source Granola AI Alternative项目地址: https://gitcode.com/GitHub_Trending/hy/anarlog
导读
本文以 Anarlog 企业版 Google Meet Worker(enterprise/google-meet-worker)仓库中固定版本 Vexav0.12.18的行为矩阵(behavior matrix)文档为核心,系统拆解其准入(admission)与运行时(runtime)状态分类器、14 个生命周期(lifecycle)场景及其供应商无关的终止原因(terminal reason)语义。读完本文,你将掌握 Google Meet 会议机器人在"加入→被拒→等待→入会→被移除→断网→会议结束"等全链路状态下的判定逻辑、宽限期(grace)参数、可重试语义,以及这些行为是如何通过可回放(replayable)的 JSON 快照进行自动化验证的。
一、矩阵文档的定位与设计初衷
1.1 Pinned Reference:固定版本的"行为契约"
MATRIX.md 开头明确了这份矩阵的基准:
Pinned reference: Vexa
v0.12.18, commit1b62993e7e97c6ee04a5dcb116f7749ec74169df.
Vexa 是一个开源的会议录制/转录机器人项目,Anarlog 在 enterprise Google Meet Worker 中采纳了其准入与运行时分类器的行为,并将其以归一化(normalized)快照的形式固化为可回放的 fixture。关键点在于文档自己强调的:
These fixtures are replayable snapshots of normalized Anarlog admission/runtime classifiers. They do not require Vexa internals at test time.
也就是说,测试时完全不依赖 Vexa 的内部实现,只需要这些归一化后的快照数据即可驱动 Anarlog 自己的分类器。这种做法把"第三方行为"沉淀为"可复现的测试资产",避免测试因上游变化而漂移。
从实现上看,分类器代码在文件头也明确标注了来源(见 admission.rs 与 runtime.rs 首行注释:Adapted for Anarlog from Vexa v0.12.18),并在 THIRD_PARTY_NOTICES.md 与 VEXA-LICENSE 中保留了完整的第三方版权与许可说明。
1.2 fixture 的目录结构
fixture 快照被组织在 fixtures/vexa-v0.12.18 下:
admission/:准入阶段的页面状态快照(6 个);runtime/:入会并开始捕获后的页面状态快照(10 个);scenarios.json:跨越多个快照、按时间轴回放的生命周期场景(14 个);MATRIX.md:行为矩阵文档本身。
每个快照 JSON 都是一个"页面探针(probe)"输出的归一化结果,例如 admission/host-denied.json:
{ "id": "host-denied", "kind": "admission", "elapsed_ms": 0, "snapshot": { "waiting_room_visible": true, "consent_prompt_visible": false, "explicit_denial_indicator": "denied your request", "ambiguous_error_indicator": null, "visible_recaptcha_challenge": false, "participant_tile_labels": [], "self_name_nodes": 0, "visible_admission_controls": 0 }, "expected": { "outcome": "rejected", "reason": "host_denied", "indicator": "denied your request" } }顶层expected字段声明了该快照应当被分类出的结果,测试直接拿它做断言基准。
二、行为矩阵总览:16 个 fixture 快照
2.1 准入阶段(admission,6 个)
| Fixture | Vexa 模块 | Anarlog outcome | Terminal reason | Retryable |
|---|---|---|---|---|
admission/host-denied.json | join/src/googlemeet/admission.ts | Rejected HostDenied | admission_denied | no |
admission/waiting-room.json | join/src/googlemeet/admission.ts | WaitingForAdmission | — | — |
admission/admitted.json | join/src/googlemeet/join.ts | Admitted | — | — |
admission/captcha-unsolved.json | join/src/googlemeet/admission.ts | Rejected CaptchaUnsolved | authentication_failed | no |
admission/error-page.json | join/src/googlemeet/admission.ts | Rejected ErrorPage | provider_error | no |
admission/consent.json | join/src/googlemeet/admission.ts | ConsentRequired | — | — |
准入阶段覆盖了三种"终态拒绝"(HostDenied / CaptchaUnsolved / ErrorPage)与三种"非终态等待"(WaitingForAdmission / Admitted / ConsentRequired)。其中终态拒绝分别映射为admission_denied、authentication_failed、provider_error,且全部不可重试——它们代表 Google 侧的确定性拒绝,重试没有意义。
2.2 运行时阶段(runtime,10 个)
| Fixture | Vexa 模块 | Anarlog outcome | Terminal reason | Retryable |
|---|---|---|---|---|
runtime/removed.json | join/src/googlemeet/removal.ts | Removed | removed_from_meeting | no |
runtime/meeting-ended.json | join/src/googlemeet/removal.ts | MeetingEnded | meeting_ended | no |
runtime/network-lost.json | join/src/googlemeet/removal.ts | NetworkLost after grace | network_lost | yes |
runtime/active.json | gmeet-capture/src/gmeet-capture.ts | Active | — | — |
runtime/silence.json | gmeet-capture/src/pcm-capture.ts | Active (no tiles besides bot) | no_one_joinedafter grace | yes |
runtime/nobody-joined.json | gmeet-capture/src/pcm-capture.ts | Active until empty-room grace | no_one_joined | yes |
runtime/overlapping-speakers.json | gmeet-capture/src/gmeet-speakers.ts | Active with two named tiles | — | — |
runtime/speaker-renamed.json | gmeet-capture/src/gmeet-speakers.ts | Active with renamed tile | — | — |
runtime/unresolved-speaker.json | gmeet-capture/src/gmeet-speakers.ts | Active with chrome chrome-ui tile | — | — |
runtime/long-duration.json | gmeet-capture/src/gmeet-capture.ts | Active after two hours | — | — |
注意runtime/silence.json的含义:机器人已入会(有 self tile)但没有任何其他参会者画面,此时短时仍判Active,只有当空场宽限期(empty-room grace)耗尽后才转换为no_one_joined,且可重试——网络瞬断或无人入会属于环境问题,重新调度会话是合理的。
三、准入分类器(AdmissionClassifier)源码级解析
行为矩阵中的 6 个 admission fixture 正是 admission.rs 中AdmissionClassifier::classify的输入输出契约。
3.1 快照结构AdmissionSnapshot
AdmissionSnapshot(admission.rs)包含 8 个字段,全部来自浏览器页面探针:
| 字段 | 类型 | 含义 |
|---|---|---|
waiting_room_visible | bool | 等待室(waiting room)是否可见 |
consent_prompt_visible | bool | 同意/授权弹窗是否可见 |
explicit_denial_indicator | Option<String> | 明确的拒绝文案(如 "denied your request") |
ambiguous_error_indicator | Option<String> | 模糊错误文案(如 "Try again") |
visible_recaptcha_challenge | bool | 是否出现 reCAPTCHA 人机验证 |
participant_tile_labels | Vec<String> | 参与者画面上的名字标签列表 |
self_name_nodes | usize | 页面上自身名字节点数量 |
visible_admission_controls | usize | 可见的准入控制组件数量 |
其中real_participant_tiles()会过滤掉 Google Meet 的 "visual_effects" / "Backgrounds and effects" 这类装饰性标签,避免把特效预览误判为真实参会者——这一点有专门单测effects_preview_is_not_a_real_participant验证(admission.rs 测试模块)。
3.2 分类决策顺序
classify(admission.rs)按确定性优先级依次判定,一旦命中即返回:
- 显式拒绝 > 一切:只要
explicit_denial_indicator存在,直接Rejected(HostDenied)。即使同时出现 reCAPTCHA 与等待室副本也照样以拒绝为准(单测explicit_host_denial_wins_over_captcha_and_stale_waiting_copy覆盖)。 - 模糊错误 + reCAPTCHA:进入"验证码宽限期"(
DEFAULT_CAPTCHA_GRACE = 120s)。宽限期内返回CaptchaChallenge { remaining }持续等待,超过 120 秒仍未解决则Rejected(CaptchaUnsolved)。单测captcha_suppression_expires_instead_of_polling_forever验证了"宽限到期前 1 秒仍在等待、到期即拒绝"的边界。 - 模糊错误(无验证码):
Rejected(ErrorPage),对应页面渲染出错。 - 等待室可见:
WaitingForAdmission,机器人滞留等待室。 - 同意弹窗可见:
ConsentRequired,需要用户/管理员授权。 - 存在准入信号(真实参与者画面、自身名字节点、准入控件任一非零):
Admitted。 - 以上皆非:
Unknown,等待下一次探针。
单测waiting_and_consent_guards_suppress_lobby_false_positives还保证了:即使等待室/同意弹窗场景下出现了准入控件或参与者标签,也不会被误判为Admitted。
3.3 fixture 与宽限期的对应
admission/captcha-unsolved.json的elapsed_ms为120000(即 120 秒),正好等于DEFAULT_CAPTCHA_GRACE——fixture 通过elapsed_ms字段模拟"验证码已超时",从而让测试在不真实等待 2 分钟的情况下验证 CaptchaUnsolved 的终态。
四、运行时分类器(RuntimeClassifier)源码级解析
运行时阶段的 10 个 fixture 对应 runtime.rs 中的RuntimeClassifier::classify(runtime.rs)。
4.1 快照结构与判定优先级
RuntimeSnapshot结构更简单:removal_indicator、meeting_ended_indicator、connection_problem_indicator三个 Option 文案 +participant_tile_labels/self_name_nodes/visible_meeting_controls三个活跃信号。判定顺序为:
- 被移除 > 会议结束 > 断网:
removal_indicator(如 "you were removed")优先于meeting_ended_indicator("meeting ended"),再优先于connection_problem_indicator("reconnecting")。单测explicit_removal_wins_over_ended_and_connection_copy验证了这一优先级。 - 断网宽限期:
DEFAULT_CONNECTION_GRACE = 30s。出现connection_problem_indicator后 30 秒内返回ConnectionInterrupted { remaining }(网络恢复则回到 Active),超过 30 秒才升级为NetworkLost。单测transient_reconnection_has_a_bounded_grace_period精确验证了"30 秒整时切换为 NetworkLost"。 - 活跃信号判定:与准入类似,
self_name_nodes/visible_meeting_controls/ 非装饰性参与者标签任一存在即Active。 - 未知状态不能无限轮询:
DEFAULT_RUNTIME_UNKNOWN_GRACE = 30s,连续 30 秒没有任何信号则进入StateLost(单测unknown_runtime_state_cannot_poll_forever覆盖)。
4.2 两个关键 fixture 的对照
runtime/network-lost.json:elapsed_ms = 30000(30 秒),connection_problem_indicator = "reconnecting",正好越过连接宽限期,得出NetworkLost。这与DEFAULT_CONNECTION_GRACE一一对应。runtime/silence.json:elapsed_ms = 0,只有自身 tile(self_name_nodes: 1),没有其他人——立即判Active,空场宽限期在生命周期层处理(见下一节)。
五、生命周期场景:scenarios.json 与 14 个终止原因
scenarios.json 把上述快照按时间轴串成多步场景,通过 lifecycle.rs 的WorkerLifecycle状态机回放,最终断言供应商无关的终止原因。矩阵文档给出的额外场景表:
| Scenario | Terminal reason | Retryable |
|---|---|---|
everyone-left-after-participants | everyone_left | no |
silence-then-nobody-joined | no_one_joined | yes |
host-ended-after-long-capture | meeting_ended | no |
captcha-unsolved | authentication_failed | no |
error-page-before-join | provider_error | no |
stopped-by-request | stopped_by_request | no |
这 6 个场景是"组合行为"而非新的分类器快照:它们复用 admission/runtime fixture,通过WorkerLifecycle的组合逻辑产生更复杂的结果。例如everyone-left-after-participants的步骤序列是:launch → admitted → capture_started → overlapping-speakers(两人在线)→ nobody-joined(全员离开),最终判定everyone_left且不可重试。
5.1 生命周期中的空场宽限期
WorkerLifecycle定义了两个与"空会议室"相关的默认宽限期(lifecycle.rs):
DEFAULT_NOBODY_JOINED_GRACE = 10 * 60(10 分钟):入会后一直没有人加入 →no_one_joined,可重试;DEFAULT_EVERYONE_LEFT_GRACE = 2 * 60(2 分钟):曾有过其他参与者、随后全部离开 →everyone_left,不可重试。
WorkerLifecycle内部用saw_other_participants与empty_since跟踪"是否见过其他人"与"空场起始时间",从而区分no_one_joined(从未有人来)与everyone_left(有人来过又走光)两种语义完全不同的结局。
5.2 供应商无关的终止原因枚举
生命周期层把所有可能的结局归一化为统一的TerminalReasonKind(crates/meeting-capture/src/lifecycle.rs),它通过#[serde(rename_all = "snake_case")]序列化为矩阵中列出的字符串,是整个 worker 与上层控制面通信的"公共语言":
| 枚举变体 | 序列化值 | 语义 |
|---|---|---|
MeetingEnded | meeting_ended | 会议结束 |
StoppedByRequest | stopped_by_request | 上层主动停止 |
AdmissionDenied | admission_denied | 主持人拒绝入会 |
AdmissionTimeout | admission_timeout | 等待室超时 |
NoOneJoined | no_one_joined | 一直无人入会 |
EveryoneLeft | everyone_left | 全员离开 |
RemovedFromMeeting | removed_from_meeting | 被移出会议 |
RecordingPermissionDenied | recording_permission_denied | 录制权限被拒 |
InvalidMeeting | invalid_meeting | 会议无效 |
AuthenticationFailed | authentication_failed | 认证失败(含验证码未过) |
CapacityExceeded | capacity_exceeded | 容量超限 |
NetworkLost | network_lost | 网络中断 |
ProviderError | provider_error | 提供商错误(含错误页) |
WorkerExited | worker_exited | 工作进程崩溃退出 |
Unknown | unknown | 未知原因 |
每个TerminalReason携带kind、可选message与retryable标志(crates/meeting-capture/src/lifecycle.rs)。BotState状态机则约束了合法的状态迁移(Queued → Launching → WaitingForAdmission/Joined → Capturing → Completed/Failed),并强制"终态必须携带终止原因、非终态不得携带",任何非法迁移都会返回TransitionError。
六、测试验证:fixture_replay.rs 如何回放矩阵
fixture_replay.rs 是矩阵文档的"可执行版本",包含两个核心测试:
6.1 回放全部分类器快照
replays_every_vexa_classifier_fixture遍历fixtures/vexa-v0.12.18/admission与runtime目录下的全部 JSON,先断言数量不少于 12 个(保证矩阵 fixture 被完整提交),然后对每个快照:
- 用
elapsed_ms = 0先classify一次(模拟初始观察); - 再以
started + elapsed_ms的时间点classify第二次(模拟宽限期后的观察); - 将两次结果与
expected字段断言:admitted→AdmissionOutcome::Admitted,waiting→WaitingForAdmission,consent→ConsentRequired,rejected→ 检查reason与indicator;runtime 侧则断言active/removed/meeting_ended/network_lost及对应的 indicator 文案。
6.2 回放生命周期场景
replays_lifecycle_scenarios_to_provider_neutral_terminal_reasons逐个执行scenarios.json中的场景,把steps翻译为WorkerLifecycle的方法调用:
launch→launch_started()admission/runtime→ 加载对应 fixture 并调用observe_admission()/observe_runtime()(elapsed_ms > 0时先做一次初始观察)admission_timeout→admission_timed_out()worker_exited→worker_exited(message)stt_unavailable→stt_unavailable(message)stopped_by_request→stopped_by_request()
每一步产生的CaptureEventPayload::Lifecycle(transition)会记录终止原因,场景结束时断言lifecycle.state()为failed/completed/canceled中的期望值,且最后一条reason.kind与retryable与场景声明完全一致。
scenarios.json中每个场景都定义了expected_state、expected_terminal与retryable三个断言目标,例如:
{ "id": "network-lost-after-grace", "expected_state": "failed", "expected_terminal": "network_lost", "retryable": true, "steps": [ { "action": "launch" }, { "action": "admission", "fixture": "admission/admitted.json" }, { "action": "capture_started" }, { "action": "runtime", "fixture": "runtime/network-lost.json" } ] }14 个场景覆盖了 happy path(host-ended-meeting→meeting_ended/completed)、各类失败(host-denied-before-join→admission_denied/failed)、可重试故障(worker-crash-after-join→worker_exited/retryable、stt-outage→provider_error/retryable)以及主动停止(stopped-by-request→stopped_by_request/completed)等边界情况。
七、如何扩展矩阵与复现验证
如果你需要新增一个 Google Meet 行为场景,可以遵循仓库内既有的三步流程:
- 采集快照:在 admission_probe.js、runtime_probe.js(被
ADMISSION_PROBE_EXPRESSION/RUNTIME_PROBE_EXPRESSION编译期内嵌)的基础上,通过 CDP 页面求值得到归一化snapshot; - 登记矩阵:在
fixtures/vexa-v0.12.18/admission或runtime下新增 JSON(含id/kind/elapsed_ms/snapshot/expected),需要多步组合时在scenarios.json增加场景条目; - 跑测试验证:执行
cargo test(测试入口为 fixture_replay.rs),回放测试会自动拾取新 fixture 并断言分类结果;完整的在线验证路径可参考 live_google_meet.rs 与 reliability_gate.rs。
整体而言,这份行为矩阵的设计精髓在于:把外部第三方(Vexa)的复杂页面行为,转化为不依赖其内部实现的归一化快照 + 确定性分类器 + 状态机场景回放,让 Anarlog 的 Google Meet Worker 在离线、可复现、可审计的前提下,稳定地把"页面上发生了什么"翻译成上层控制面能够理解的、供应商无关的终止原因与重试策略。
【免费下载链接】anarlogOpen source Granola AI Alternative项目地址: https://gitcode.com/GitHub_Trending/hy/anarlog
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考