Vibe 3.1.6 更新解读:转录滚动跟随交互重构与 Google Meet 检测的屏幕录制权限修复
【免费下载链接】vibeTranscribe on your own!项目地址: https://gitcode.com/GitHub_Trending/vib/vibe
本篇基于 Vibe 开源仓库的 3.1.6 版本更新记录(website/changelog/3.1.6.md)展开,聚焦两个改动:一是“边转录边回看”场景下滚动跟随(follow)机制的彻底重构——让读者真正能自由翻阅仍在实时生成的转录文本;二是 macOS 上 Google Meet 会议检测首次主动请求屏幕录制(Screen Recording)权限,并在用户拒绝时直达系统设置。读完本文,你将理解这两处改动的设计动机、实现细节与源码证据,并掌握 Vibe 转录视图与会议检测模块的内部工作机制。
版本概览
3.1.6 于 2026-08-28 发布,版本主题为"Scroll back through a transcript while it is still being written"(在转录仍持续写入时回翻已生成的内容)。本次更新包含一项体验改进(Improved)和一项修复(Fixed):
| 分类 | 内容 | 摘要 |
|---|---|---|
| Improved | 📜 转录生成过程中的滚动跟随重构 | 跟随实时文本不再“锁死”在底部,滚轮、触控板、滚动条、Page Up 等真实操作即可脱离跟随;滚回底部后自动恢复跟随 |
| Fixed | 🎥 Google Meet 检测请求它所需的权限 | 该改动曾出现在 3.1.5 的更新说明中,但实际代码在 3.1.5 打标签之后才落地,3.1.6 是首个包含它的构建。macOS 上读取浏览器窗口标题需要屏幕录制权限,Vibe 此前只检查、从不请求,现在会请求一次,并在用户拒绝时直链系统设置 |
一、滚动跟随重构:让“正在生成的转录”真正可以回看
1.1 旧机制的问题
在实时转录过程中,转录视图默认会“跟随”最新生成的文本——每来一个新段落就自动滚到底部。这个设计本身合理,但旧实现有一个致命缺陷:
跟随实时文本曾几乎无法脱离:向上滚动,或点击某一行进行播放,都会在下个段落到达时被瞬间拉回底部。
旧实现的关键在于“把控制权交还给读者”的判断方式:它监听一个scroll 事件,并设置了一个700 ms 的忽略窗口——凡是 Vibe 自己引发的滚动,在其后的 700 ms 内的滚动事件都被忽略。然而,只要转录仍在进行,每个到达的新段落都会刷新这个窗口。结果就是:在一次真实的转录过程中,这个窗口永远不会关闭,读者向上翻页后总是被拽回末尾,回看行为形同虚设。
1.2 新机制:由“输入事件”而非“滚动事件”驱动脱离
3.1.6 的核心思路是:不再猜测“这个滚动是不是用户发起的”,而是直接监听用户发起滚动的输入事件本身。驱动逻辑位于 desktop/src/pages/main/components/transcript-view.tsx:
/** Keys that scroll the transcript, so pressing one means the reader is steering. */ const SCROLL_KEYS = ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End'] /** How close to the tail counts as being back at the bottom, in pixels. */ const BOTTOM_STICK_PX = 96releaseFollow是“脱离跟随”的核心回调(transcript-view.tsx):它会先清除following状态,并且如果此时正在播放,则显示一个“跳转到播放位置”的悬浮胶囊(jump pill),3 秒后自动淡出。
const releaseFollow = useCallback(() => { if (followingRef.current) { followingRef.current = false setFollowing(false) } if (!playingRef.current) return setJumpVisible(true) window.clearTimeout(jumpTimer.current) jumpTimer.current = window.setTimeout(() => setJumpVisible(false), 3000) }, [])脱离跟随的触发条件全部是真实输入(transcript-view.tsx):
- 滚轮滚动:
element.addEventListener('wheel', releaseFollow, { passive: true }) - 触控板触屏滑动:
element.addEventListener('touchmove', releaseFollow, { passive: true }) - 拖动滚动条:
pointerdown且event.target === element——注释明确说明只有滚动条本身命中滚动容器时才算用户干预,点击文本行不算 - 键盘翻页:
ArrowUp / ArrowDown / PageUp / PageDown / Home / End(即SCROLL_KEYS)按下即脱离;INPUT / TEXTAREA / SELECT或可编辑元素内的按键会被排除,避免在编辑文本时误触发
与旧方案“700 ms 窗口 + 监听 scroll”的根本区别在于:Vibe 自身滚动产生的 scroll 事件不会再被误判为用户操作,因此流式转录导致的连续滚动不会与用户的上翻动作互相干扰。
1.3 滚回底部,自动恢复跟随
脱离跟随之后,用户滚回末尾时 Vibe 会像“日志查看器粘回底部”一样自动重新进入跟随状态。恢复逻辑同样基于 scroll 事件,但结合了运行状态与阈值判断(transcript-view.tsx):
const onScroll = useCallback(() => { const element = scrollRef.current if (!running || followingRef.current || !element) return if (element.scrollHeight - element.scrollTop - element.clientHeight > BOTTOM_STICK_PX) return followingRef.current = true setFollowing(true) }, [running])即:仅当转录仍在运行(running)、当前未跟随、且距离底部不超过BOTTOM_STICK_PX(96 像素)时才恢复跟随。这里特意“在 scroll 回调中实时测量”而非延迟到下一帧——否则流式段落已经撑高了列表,用户在视觉上不再处于底部,恢复判定就会失效。
1.4 跟随状态的两种滚动行为
恢复或保持跟随后,视图存在两条滚动路径(transcript-view.tsx):
- 播放同步:当播放器广播时间推进(
PLAYER_TIME_EVENT)时,activeIndex指向正在朗读的段落,若following为真,则调用rowVirtualizer.scrollToIndex(position, { align: 'center' })将当前行居中; - 转录跟随:转录运行中且未编辑、未脱离时,
scrollToIndex(visible.length - 1, { align: 'end' })将最新一行对齐到底部。
两者都受following、editing(正在编辑某一行)等状态门控,编辑状态下不会强制滚动。
1.5 配套交互:跳转播放胶囊
当读者已脱离跟随、播放器正在播放时,视图底部中央会出现一个“跳转到播放位置”的胶囊按钮(jumpToPlaying,见 transcript-view.tsx):点击后恢复跟随并scrollToIndex到当前朗读行,同时立即隐藏胶囊。这使得“翻回去阅读 → 点击胶囊回到正在播放的位置”成为完整的闭环操作。值得注意的细节是,点击转录行的时间戳即可播放该行(PLAYER_SEEK_EVENT,按segment.start / 100换算秒数),这也是 changelog 中“clicking a line to play it”所指的交互。
二、Google Meet 检测请求它所需的权限
2.1 背景:为什么只有 macOS 需要这个权限
Vibe 的会议检测模块 crates/meeting-detect 支持 Zoom、Teams、Google Meet 三种会议来源。检测链路分为两层:
- 进程层:通过操作系统 API 枚举正在运行的进程,按进程名 / 可执行文件路径 / Bundle ID 匹配 Zoom、Teams 与浏览器(详见 crates/meeting-detect/src/process.rs 中的
ZOOM_BUNDLE_IDS、TEAMS_BUNDLE_IDS、BROWSER_BUNDLE_IDS及ZOOM_PROCESS_NAMES、TEAMS_PROCESS_NAMES、BROWSER_PROCESS_NAMES); - 窗口标题层:仅用于 Google Meet。当麦克风被浏览器占用时,读取浏览器的窗口标题,通过
is_meet_title判断是否正在开会(crates/meeting-detect/src/window_title.rs):
pub(crate) fn is_meet_title(title: &str) -> bool { let title = title.trim(); title == "Google Meet" || title.starts_with("Meet – ") || title.starts_with("Meet - ") }在 macOS 上,读取其他应用(浏览器)的窗口标题受屏幕录制权限(Screen Recording)管控。权限缺失时,CGWindowListCopyWindowInfo返回的窗口列表完全没有标题,系统层面无法区分“没有会议”和“有会议但读不到标题”。这正是问题的根源——Zoom 和 Teams 走进程列表即可识别,从不依赖此权限;唯独 Google Meet 检测在权限缺失时会“静默失效”。
2.2 3.1.5/3.1.6 的修复
3.1.5 的更新说明中曾写入“Google Meet detection asks for the permission it needs”,但代码实际落在该版本 tag 之后,因此 3.1.6 才是首个包含该行为的构建(changelog 原文明确说明了这一点)。修复包含三个层次:
① 检查与请求分离的权限 API(crates/meeting-detect/src/lib.rs):
pub fn screen_recording_granted() -> bool { #[cfg(target_os = "macos")] { window_title::screen_recording_granted() } #[cfg(not(target_os = "macos"))] { true } } pub fn request_screen_recording() -> bool { #[cfg(target_os = "macos")] { window_title::request_screen_recording() } #[cfg(not(target_os = "macos"))] { true } }底层实现位于 crates/meeting-detect/src/window_title.rs:ScreenCaptureAccess.preflight()做无副作用的检查,ScreenCaptureAccess.request()弹出系统授权框并注册到系统设置;macOS 对同一应用身份只弹一次授权框,之后返回既有结果。非 macOS 平台直接返回true(不适用)。
② Tauri 命令层(desktop/src-tauri/src/cmd/permissions.rs)暴露三个命令给前端:
get_screen_recording_permission_status:macOS 上granted则返回Granted,否则返回NotDetermined——因为preflight无法区分“从未询问”与“曾被拒绝”,而 macOS 授权框只能弹一次,所以 UI 侧先提供“请求”按钮、被拒后降级为“打开系统设置”;request_screen_recording_permission:通过tokio::task::spawn_blocking在阻塞线程中调用request_screen_recording,避免阻塞主线程;open_screen_recording_settings:使用深链open x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture直达“隐私与安全性 → 屏幕录制”设置面板。
③ 前端设置面板(desktop/src/pages/settings/sections/recording.tsx)中的MeetPermissionRow组件:
- 启用会议检测后查询权限状态,并监听窗口
focus事件刷新(macOS 在授权后需要应用重新获得焦点才会返回新答案); - 状态为
not_determined时自动发起一次请求——注释说明这是唯一一次展示系统授权框的机会; - 状态为已拒绝时,渲染一行“打开系统设置”链接,点击调用
open_screen_recording_settings深链; - 已授权或非 macOS(
not_applicable)时整行隐藏。
最终效果正如 changelog 所述:“It now requests it once and links straight to the settings pane if you decline. Zoom and Teams never needed it.”
2.3 三平台行为对照
| 平台 | Meet 识别方式 | 是否需要额外权限 |
|---|---|---|
| macOS | CGWindowListCopyWindowInfo(core_graphics::window::copy_window_info,kCGWindowListOptionOnScreenOnly)读取窗口标题 | 需要屏幕录制权限,缺失时记录tracing::warn!并返回None |
| Windows | EnumWindows+GetWindowTextW+GetWindowThreadProcessId按 PID 匹配浏览器窗口 | 不需要 |
| Linux | x11rb 读取_NET_CLIENT_LIST/_NET_WM_PID/_NET_WM_NAME;Wayland 会话下标题不可读,直接返回None | 不需要 |
三个平台的窗口枚举实现均位于 crates/meeting-detect/src/window_title.rs,并配有单元测试覆盖标题识别规则(如"Meet – Daily standup"命中、"Google Meet - Google Chrome"不命中,见同文件tests模块)。
三、纵深:会议检测的完整判定链路与防抖机制
为更好地理解“权限只是其中一环”,这里把检测链路串起来。核心入口在 crates/meeting-detect/src/lib.rs:
- 麦克风状态:
mic::current_usage()返回当前麦克风是否活跃及占用者;macOS 系统 API 可能只报活跃不报占用者(processes为空),此时退化为process::running_candidates()枚举已知候选进程; - 归属分类:
classify_active按Zoom → Teams → Meet(浏览器标题)的优先级判定。只要进程列表中有 Zoom 即归属 Zoom,Teams 次之;两者都没有且麦克风被浏览器占用时,才扫描窗口标题确认 Meet(crates/meeting-detect/src/lib.rs); - 防抖与轮询:
watch(interval)在后台线程轮询,只在状态稳定变化时才向上游发送MeetingState。三档防抖阈值定义在源码常量中:
| 常量 | 默认值 | 含义 |
|---|---|---|
DEFAULT_ONSET_DEBOUNCE | 400 ms | 会议开始(进入录音)需持续确认的时长 |
DEFAULT_RELEASE_DEBOUNCE | 600 ms | 麦克风关闭导致退出会议时需持续确认的时长 |
DEFAULT_ATTRIBUTION_DEBOUNCE | 5 s | 麦克风仍开、仅“归属”丢失(如会议中短暂切走)时需持续确认的时长 |
UNSETTLED_INTERVAL | 150 ms | 信号未稳定时的加速轮询间隔 |
TITLE_SCAN_INTERVAL | 1000 ms | 窗口标题扫描的限流间隔(枚举窗口开销大,稳定态下不每轮都扫) |
MIN_INTERVAL | 50 ms | 轮询间隔下限 |
- Meet 粘性保持(hold_meet):一旦 Meet 通过防抖确认,只要麦克风会话未结束且浏览器进程仍在,即使浏览器切走标签导致标题暂时不可读,归属也不会降级(crates/meeting-detect/src/lib.rs),并且此状态下可跳过昂贵的窗口扫描。相关行为均有测试覆盖,例如
watcher_holds_confirmed_meet_until_mic_release验证“标题丢失被抑制、直到麦克风释放才发出非录音状态”,a_held_meet_stops_paying_for_the_window_scan验证粘性状态下不再持续扫描窗口。
总结
3.1.6 的两处改动恰好代表了桌面录音工具的两类典型问题:交互层的“自动化过度”与系统层的“权限缺失”。
滚动跟随的重构说明了一个通用设计原则:当程序行为与用户意图冲突时,与其靠时间窗口猜测“这次滚动是不是用户发的”,不如直接监听用户发起滚动的输入事件(wheel / touchmove / 滚动条 / 翻页键),再配合“回到底部自动恢复”的阈值(BOTTOM_STICK_PX = 96)实现无感衔接。Google Meet 权限修复则展示了跨平台检测中“进程识别优先、窗口标题兜底”的架构取舍:Zoom 与 Teams 永不依赖屏幕录制权限,而 Meet 在 macOS 上不仅会主动请求一次权限,被拒后还能一键直达系统设置面板。若需深入代码,可从 desktop/src/pages/main/components/transcript-view.tsx 与 crates/meeting-detect/src/lib.rs 两个文件入手,其内嵌的注释与单元测试对理解这两套机制极具参考价值。
【免费下载链接】vibeTranscribe on your own!项目地址: https://gitcode.com/GitHub_Trending/vib/vibe
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考