feat(stt): 添加桌面端与 WebUI 语音输入 - #529
Conversation
|
PR governance checks passed. Awaiting human review. |
# Conflicts: # crates/agent-gateway/web/src/app/GatewayAppView.tsx # crates/agent-gateway/web/src/app/hooks/useGatewaySettingsSync.ts # crates/agent-gui/src-tauri/src/commands/config/settings/mod.rs # crates/agent-gui/src/lib/settings/storage.ts # crates/agent-gui/src/pages/ChatPage.tsx # crates/agent-ui/src/pages/chat/ChatComposerBar.tsx
su-fen
left a comment
There was a problem hiding this comment.
Review: 暂不建议直接合入
CI 全绿,与当前 main 无冲突,同版本路径下凭据脱敏、鉴权和测试覆盖整体是认真做的。但有几处会破坏本 PR 自己承诺的安全边界或会话生命周期,建议修完再合。
必须先修
- 新桌面连旧 Gateway 时,STT 明文密钥会进入浏览器广播。 桌面无条件附加
sttSecretSync,未检查 ServerHello 是否包含STT_STREAM_V1。同版本新 Gateway 会在广播前剥离,旧 Gateway 没有这段逻辑,会把 API Key / SecretKey 写进设置快照并推给所有已登录 WebUI。Capability 已经加了,请用它做门禁。 - Gateway 适配器 / Manager 向
events的发送不响应ctx.Done()。 WS 写协程退出后 buffer 填满,适配器会永久卡住,Cancel()也救不回来,session 和上游云连接一起泄漏。 ready与stop()竞态。 用户在云连接尚未 ready 时点停止,onEvent("ready")会提前finishProvider(),尾音和最后一截音频可能在 Finish 之后才发出,火山 / DashScope 会视为协议错误。
与 #536 不符
桌面端麦克风按钮仍要求「已选供应商且 configured」,未配置时直接不渲染;WebUI 用 settings.stt.provider ?? "tencent_cloud" 始终显示。Issue 写的是输入框始终显示麦克风、点击后再报错。
同版本下已经做对的部分
- 新 Gateway 广播前删除
sttSecretSync,HTTP GET 返回脱敏配置,错误信息会替换密钥和 URL。 /api/v2/stt/*走 Bearer 中间件;/ws/v2/stt首帧校验 token,有帧大小、序号连续、120 fps 限制。- Proto 只新增消息和 capability,旧桌面只检查
CHAT_INGRESS_V1存在,连新 Gateway 不会因多一个 capability 握手失败。 - 本地跑过 Gateway
internal/stt、internal/session相关测试,以及 STT Node 测试 64/64。
可以跟进、不必卡这次
- 无并发 STT 会话上限。
/ws/v2/stt升级后、首帧 hello 前没有读超时。- 3 秒静音自动停止偏短。
- 后端仍保留
volcengine_v2,前端类型已去掉,容易造成误解。
请先修第 1–3 条,并补上「旧 Gateway 不附加密钥」「取消时适配器退出」「stop 与 ready 竞态」的测试。
| // The cached/browser-visible snapshot remains redacted. Only the | ||
| // authenticated desktop-to-Gateway envelope receives the raw STT | ||
| // sidecar, which Gateway consumes before broadcasting the snapshot. | ||
| let outbound = attach_current_stt_secret_sync(snapshot).await?; |
There was a problem hiding this comment.
Blocking: 这里无条件把原始 STT 凭据挂到 outbound envelope 上,但没有确认对端 Gateway 是否支持 STT_STREAM_V1。
同版本新 Gateway 会在 consumePrivateSTTSettings 里剥离 sttSecretSync,这条路径是对的。桌面可以连远程/自建的旧 Gateway;旧版本没有这段剥离逻辑,会把 API Key / SecretKey 写进 settingsSnapshot 并广播给所有已登录 WebUI。这和 PR 说明里「敏感凭据不进入公开设置广播」冲突。
建议:仅当 ServerHello 的 capabilities 包含 STT_STREAM_V1 时才调用 attach_current_stt_secret_sync。请补一个「新桌面 + 旧 Gateway 不附加 sidecar」的测试。
| go func() { | ||
| err := adapter.Run(ctx, sessionID, cfg, active.commands, events) | ||
| if err != nil { | ||
| events <- Event{ | ||
| Type: "error", | ||
| SessionID: sessionID, | ||
| Code: resultForError(err), | ||
| Message: sanitizeError(err.Error(), cfg), | ||
| } | ||
| } | ||
| events <- Event{Type: "closed", SessionID: sessionID} |
There was a problem hiding this comment.
Blocking: events <- 不响应 ctx.Done()。各 provider adapter(腾讯 / 火山 / DashScope / 百度)里同样是直接发送。
http.go 的写协程一旦因写失败退出,64 的 buffer 填满后这里会永久阻塞。随后 Cancel() 只取消 context,救不了已经卡在 channel send 上的 goroutine,session map 项和上游云 WebSocket 一起泄漏。
请改成 select { case events <- ...: case <-ctx.Done(): },adapter 内部的 events <- / incoming <- 同样处理,并补一个「写端退出后 Run 能结束」的测试。
| if (event.type === "ready") { | ||
| window.clearTimeout(active.connectTimer); | ||
| active.ready = true; | ||
| if (!active.stopping) setState("recognizing"); | ||
| for (const chunk of active.fifo.drain()) sendChunk(chunk.sequence, chunk.pcm); | ||
| if (active.stopping) void finishProvider(active); |
There was a problem hiding this comment.
Blocking: stop() 会先 await capture.stop()。若这段时间内收到 ready,这里会立刻 finishProvider(),把 Finish 发给云端;stop() 恢复后才排队 400ms 尾音和最后一截 flush。
对火山 v2/v3、DashScope 来说,Finish 之后再发音频是协议错误,会话结束可能失败。用户点麦克风后很快再点停止就能走到这条路径。
建议:ready 时如果 active.stopping,只 drain FIFO / 标 ready,不要自己 Finish,把收尾交给 stop()。请补覆盖该竞态的测试。
| sttProvider={ | ||
| settings.stt.provider && | ||
| settings.stt.providers[settings.stt.provider].configured | ||
| ? settings.stt.provider | ||
| : null | ||
| } |
There was a problem hiding this comment.
这里把 sttProvider 设为 null(除非已选供应商且 configured),ChatComposerBar 会因此不渲染麦克风按钮。
#536 和本 PR 说明都写了「输入框始终显示麦克风,不依赖配置测试;点击后再做权限/连接检查并报错」。WebUI 用的是 settings.stt.provider ?? "tencent_cloud",未配置也会显示。两端行为不一致。
请改成始终传入当前/默认 provider,让点击后的 fail(...) 去提示未配置或凭据无效。
Keep recognition from auto-stopping while the cloud is still connecting, cancel in-flight sessions when the conversation or composer view changes, and stop unsaved settings-page provider clicks or leftover clearSecrets from changing live STT behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
Follow-up: 交互问题已修(不含混合版本)已推到本分支: 按上次审核里除混合版本凭据门禁之外的交互问题改完了,混合版本(新桌面连旧 Gateway 时 修了什么
测试node --test \
crates/agent-gui/test/chat/composer-stt-lifecycle.test.mjs \
crates/agent-gui/test/settings/stt-settings.test.mjs
混合版本凭据门禁仍建议修完再合。 |
Keep ScriptProcessor in the graph through a zero-gain node so recognition does not play the microphone through speakers. Make stop/cancel interruptible on both runtimes (queue timeout, force-cancel while stopping, oneshot abort, provider write deadlines, incoming channel cancellation), add a hello read deadline, and ignore stale HTTP STT hydration after a newer settings push. Co-authored-by: Cursor <cursoragent@cursor.com>
Follow-up: 除混合版本门外的问题已修已推到本分支: 混合版本凭据门禁(新桌面连旧 Gateway 时仍无条件附加 修了什么
测试go test ./internal/stt/
node --test \
crates/agent-gui/test/chat/composer-stt-lifecycle.test.mjs \
crates/agent-gui/test/chat/stt-audio.test.mjs \
crates/agent-gui/test/settings/stt-settings.test.mjsGateway STT Go 测试通过;STT Node 测试 混合版本凭据门禁仍建议修完再合。 |
Linked issue
Closes #536
Summary
为桌面端和 Gateway WebUI 增加完整的“语音输入”功能,支持通过实时语音识别将麦克风输入转换为聊天文本。
本次变更解决以下问题:
实现后的主要行为:
Change scope
agent-ui/agent-gui/agent-gateway/src-tauri/ Protocol Buffers / macOS packagingcrates/agent-ui/src/pages/chat/useComposerStt.tscrates/agent-ui/src/pages/chat/ChatComposerBar.tsxcrates/agent-ui/src/pages/settings/SttSection.tsxcrates/agent-ui/src/pages/settings/SettingsPage.tsxcrates/agent-ui/src/lib/stt/crates/agent-ui/src/lib/settings/crates/agent-ui/src/i18n/translations/crates/agent-gui/src/lib/stt/crates/agent-gui/src-tauri/src/services/stt/crates/agent-gui/src-tauri/src/commands/config/settings/crates/agent-gui/src-tauri/src/services/gateway/crates/agent-gui/src-tauri/Info.plistcrates/agent-gui/src-tauri/Entitlements.plistcrates/agent-gateway/internal/stt/crates/agent-gateway/internal/session/crates/agent-gateway/web/src/lib/stt/crates/agent-gateway/web/src/app/hooks/useGatewaySettingsSync.tscrates/agent-gateway/proto/v2/gateway_ws.protodocs/stt-mvp-acceptance.mdScreenshots / preview
桌面端
Gateway WebUI
配置同步
Verification
已执行以下检查:
pnpm exec biome check \ src/i18n/translations/zhCNSettings.ts \ src/pages/settings/SttSection.tsx结果:通过,无格式或静态检查错误。
结果:
49/49项测试通过。覆盖的关键场景包括:
结果:WebUI 和桌面端前端生产构建均通过。
结果:成功生成包含最新 WebUI 的 Linux AMD64 Gateway 可执行文件。
另外已验证:
git diff --check通过;dist、bin、package-lock.json或 macOS._*元数据文件。Pre-submit checklist