Skip to content

feat(stt): 添加桌面端与 WebUI 语音输入 - #529

Merged
su-fen merged 13 commits into
Stack-Cairn:mainfrom
loto585:feature/voice
Aug 19, 2026
Merged

feat(stt): 添加桌面端与 WebUI 语音输入#529
su-fen merged 13 commits into
Stack-Cairn:mainfrom
loto585:feature/voice

Conversation

@loto585

@loto585 loto585 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Linked issue

Closes #536

Summary

为桌面端和 Gateway WebUI 增加完整的“语音输入”功能,支持通过实时语音识别将麦克风输入转换为聊天文本。

本次变更解决以下问题:

  • 桌面端和 WebUI 缺少统一、可用的语音输入流程;
  • WebUI 麦克风按钮依赖连接测试结果,导致未测试或测试失败时无法发起语音输入;
  • 桌面端语音识别配置无法安全同步至 Gateway WebUI;
  • WebUI 进入语音配置页面或切换供应商时会重复加载、保存配置,造成页面闪动和意外写入;
  • 已保存的 API Key、AccessToken、SecretKey、SecretID 等敏感字段缺少一致的掩码和查看行为;
  • 火山引擎同时展示 v2/v3 配置,容易造成接口选择混淆。

实现后的主要行为:

  • 桌面端和 WebUI 聊天输入框均提供常显的麦克风按钮;
  • 麦克风按钮不再依赖配置测试结果,点击后才执行权限、录音和服务连接检查,并在失败时显示错误;
  • 支持腾讯云、火山引擎、阿里云 DashScope、百度智能云实时语音识别;
  • 桌面端配置通过 Gateway 安全同步至 WebUI,敏感凭据不进入公开设置广播;
  • 进入语音输入页面或切换供应商时仅更新显示状态,不主动保存配置;
  • WebUI 在应用启动时完成一次权威配置加载,避免每次进入页面重复请求造成闪动;
  • 补充语音输入配置、音频处理、会话生命周期、Gateway 同步及敏感字段保护相关测试和验收文档。

Change scope

  • Modules: agent-ui / agent-gui / agent-gateway / src-tauri / Protocol Buffers / macOS packaging
  • Key paths:
    • crates/agent-ui/src/pages/chat/useComposerStt.ts
    • crates/agent-ui/src/pages/chat/ChatComposerBar.tsx
    • crates/agent-ui/src/pages/settings/SttSection.tsx
    • crates/agent-ui/src/pages/settings/SettingsPage.tsx
    • crates/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.plist
    • crates/agent-gui/src-tauri/Entitlements.plist
    • crates/agent-gateway/internal/stt/
    • crates/agent-gateway/internal/session/
    • crates/agent-gateway/web/src/lib/stt/
    • crates/agent-gateway/web/src/app/hooks/useGatewaySettingsSync.ts
    • crates/agent-gateway/proto/v2/gateway_ws.proto
    • docs/stt-mvp-acceptance.md

Screenshots / preview

桌面端

image image

Gateway WebUI

image image

配置同步

image

Verification

已执行以下检查:

pnpm exec biome check \
  src/i18n/translations/zhCNSettings.ts \
  src/pages/settings/SttSection.tsx

结果:通过,无格式或静态检查错误。

node --test \
  crates/agent-gateway/test/webui/web-settings.test.mjs \
  crates/agent-gui/test/chat/composer-stt-lifecycle.test.mjs \
  crates/agent-gui/test/settings/stt-settings.test.mjs

结果:49/49 项测试通过。

覆盖的关键场景包括:

  • WebUI STT 配置同步和敏感凭据隔离;
  • 桌面端配置通过私有 sidecar 同步至 Gateway;
  • 麦克风权限、音频采集、云端连接和停止流程;
  • 音频尾帧发送及识别结果顺序;
  • 过期会话事件不会修改当前输入框;
  • 四个供应商默认配置和迁移兼容性;
  • 已保存凭据的掩码与查看行为;
pnpm build:webui
pnpm build:gui

结果:WebUI 和桌面端前端生产构建均通过。

make build-linux-amd

结果:成功生成包含最新 WebUI 的 Linux AMD64 Gateway 可执行文件。

另外已验证:

  • Gateway STT 和设置同步相关 Go 测试通过;
  • 桌面端私有 STT 凭据同步相关 Rust 测试通过;
  • Rust 格式检查通过;
  • git diff --check 通过;
  • 暂存内容未包含 API Key、AccessToken、SecretKey、SecretID 等真实凭据;
  • 未提交 distbinpackage-lock.json 或 macOS ._* 元数据文件。

Pre-submit checklist

  • A requirement issue is linked (or this is a trivial fix that needs no issue, as explained in the summary).
  • Synced with the target branch; no merge conflicts.
  • The change is focused, with no unrelated modifications.
  • No secrets, tokens, or personal data included.
  • Docs are updated for changes affecting user behavior, deployment, or configuration.

@StackCairn
StackCairn marked this pull request as draft August 17, 2026 13:44
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

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
@loto585
loto585 marked this pull request as ready for review August 18, 2026 02:44
@StackCairn
StackCairn marked this pull request as draft August 18, 2026 02:44
@loto585
loto585 marked this pull request as ready for review August 18, 2026 02:45
@StackCairn
StackCairn marked this pull request as draft August 18, 2026 02:45
@loto585
loto585 marked this pull request as ready for review August 18, 2026 02:47
@StackCairn
StackCairn marked this pull request as draft August 18, 2026 13:33
@su-fen
su-fen marked this pull request as ready for review August 18, 2026 13:56

@su-fen su-fen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: 暂不建议直接合入

CI 全绿,与当前 main 无冲突,同版本路径下凭据脱敏、鉴权和测试覆盖整体是认真做的。但有几处会破坏本 PR 自己承诺的安全边界或会话生命周期,建议修完再合。

必须先修

  1. 新桌面连旧 Gateway 时,STT 明文密钥会进入浏览器广播。 桌面无条件附加 sttSecretSync,未检查 ServerHello 是否包含 STT_STREAM_V1。同版本新 Gateway 会在广播前剥离,旧 Gateway 没有这段逻辑,会把 API Key / SecretKey 写进设置快照并推给所有已登录 WebUI。Capability 已经加了,请用它做门禁。
  2. Gateway 适配器 / Manager 向 events 的发送不响应 ctx.Done() WS 写协程退出后 buffer 填满,适配器会永久卡住,Cancel() 也救不回来,session 和上游云连接一起泄漏。
  3. readystop() 竞态。 用户在云连接尚未 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/sttinternal/session 相关测试,以及 STT Node 测试 64/64。

可以跟进、不必卡这次

  • 无并发 STT 会话上限。
  • /ws/v2/stt 升级后、首帧 hello 前没有读超时。
  • 3 秒静音自动停止偏短。
  • 后端仍保留 volcengine_v2,前端类型已去掉,容易造成误解。

请先修第 1–3 条,并补上「旧 Gateway 不附加密钥」「取消时适配器退出」「stop 与 ready 竞态」的测试。

Comment on lines +384 to +387
// 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?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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」的测试。

Comment on lines +81 to +91
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}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 能结束」的测试。

Comment on lines +156 to +161
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: stop() 会先 await capture.stop()。若这段时间内收到 ready,这里会立刻 finishProvider(),把 Finish 发给云端;stop() 恢复后才排队 400ms 尾音和最后一截 flush。

对火山 v2/v3、DashScope 来说,Finish 之后再发音频是协议错误,会话结束可能失败。用户点麦克风后很快再点停止就能走到这条路径。

建议:ready 时如果 active.stopping,只 drain FIFO / 标 ready,不要自己 Finish,把收尾交给 stop()。请补覆盖该竞态的测试。

Comment on lines +1976 to +1981
sttProvider={
settings.stt.provider &&
settings.stt.providers[settings.stt.provider].configured
? settings.stt.provider
: null
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里把 sttProvider 设为 null(除非已选供应商且 configured),ChatComposerBar 会因此不渲染麦克风按钮。

#536 和本 PR 说明都写了「输入框始终显示麦克风,不依赖配置测试;点击后再做权限/连接检查并报错」。WebUI 用的是 settings.stt.provider ?? "tencent_cloud",未配置也会显示。两端行为不一致。

请改成始终传入当前/默认 provider,让点击后的 fail(...) 去提示未配置或凭据无效。

Eric and others added 4 commits August 19, 2026 01:16
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>
@su-fen

su-fen commented Aug 19, 2026

Copy link
Copy Markdown
Member

Follow-up: 交互问题已修(不含混合版本)

已推到本分支:bcfab97a fix(voice): stabilize STT silence, session switch, and settings save

按上次审核里除混合版本凭据门禁之外的交互问题改完了,混合版本(新桌面连旧 Gateway 时 sttSecretSync 仍无 STT_STREAM_V1 门禁)这次没有动。

修了什么

  1. 连接中被 3 秒静音自动停掉
    静音计时改为从云端 ready 开始;buffering 阶段的静音不再 stop()。避免「点麦克风 → 转圈 → 无提示回到空闲」。

  2. 设置页点供应商卡片会改掉聊天框正在用的服务
    Composer 只认已保存的 settings.stt.provider。浏览未配置的供应商不再把麦克风切过去报「配置不完整」。

  3. 录音中切会话 / 进轨迹页
    sttSessionKey + hidden 在 layout 阶段取消当前识别,并保留已转写文本,避免结果写进已拆掉的 composer。

  4. 清空密钥后立刻保存会再次清掉新密钥
    save() 不再带上残留的 clearSecrets。清空仍走单独的清空路径。

测试

node --test \
  crates/agent-gui/test/chat/composer-stt-lifecycle.test.mjs \
  crates/agent-gui/test/settings/stt-settings.test.mjs

9/9 通过,覆盖了 buffering 静音、ready 后静音停止、切会话/隐藏取消、以及 clearSecrets 不会进入普通保存。

混合版本凭据门禁仍建议修完再合。

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>
@su-fen

su-fen commented Aug 19, 2026

Copy link
Copy Markdown
Member

Follow-up: 除混合版本门外的问题已修

已推到本分支:967ddb82 fix(voice): mute mic playback and unblock STT cancel paths

混合版本凭据门禁(新桌面连旧 Gateway 时仍无条件附加 sttSecretSync)这次仍然没动,其余上次深度审查里的问题已修。

修了什么

  1. 麦克风被接到扬声器
    ScriptProcessor 改为经 gain = 0 的节点进图,识别时不再把麦克风实时播放出来。

  2. 停止识别卡在 stopping

    • finishProvider 等待发送队列最多 10 秒,不再无限挂起;
    • stopping 时再次点击麦克风会强制 cancel
    • 桌面端 cancel 走 oneshot,不再排在已满的音频队列后面,并中止卡住的供应商写入;
    • 各云适配器写入加了 10 秒超时。
  3. Gateway 读协程泄漏
    适配器内部 incoming 发送改为与 emitEvent 一样响应 ctx.Done(),供应商写入同样加了写超时。

  4. /ws/v2/stt 首帧无读超时
    hello 前 10 秒 deadline;Start 失败时回 error 帧,而不是静默关连接。

  5. WebUI 启动时陈旧 HTTP STT 覆盖更新的推送
    若 GET settings / GET STT 飞行期间已有更新的 WS 推送,则丢弃这次 HTTP 结果。

测试

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.mjs

Gateway STT Go 测试通过;STT Node 测试 20/20 通过。覆盖了 hello 超时、incoming 取消、stopping 时强制取消、以及静音输出节点。

混合版本凭据门禁仍建议修完再合。

@su-fen
su-fen merged commit 0b448a0 into Stack-Cairn:main Aug 19, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 为桌面端和 WebUI 增加统一的“语音输入”功能

2 participants