更新记录
1.0.0(2026-08-29)
· Android UTS 原生 PCM 流式采集(16kHz/16bit/单声道,100ms一帧) · H5 AudioContext 采集 + 16k 重采样 · 豆包 Seeduplex 全双工协议封装:流式字幕、流式音频、response.cancel 打断 · sayText 文本合成、session.update 热更新 · 静音保活、自动重连(最多 3 次) · 安全模式:rt-ticket HMAC 短期票据 + 服务端代理持 Key · 热词:不内置任何行业默认热词;config.hotwords 外部注入,文档附 5 个行业示例词表
平台兼容性
uni-app(3.8.3)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| × | √ | √ | √ | √ | × | √ | × | × |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | × | × | × |
sm-doubao-realtime
豆包端到端实时语音全双工对话 · uni-app UTS 插件
10 分钟让你的 App/H5 拥有一个能随时被用户打断的语音 AI。
为什么用它,而不是自己接豆包
在 uni-app 里接豆包 Seeduplex(端到端全双工)会撞上 3 个每个开发者都会掉进去的坑,这个插件已经替你踩完并修好了:
| 坑 | 现象 | 本插件怎么解决 |
|---|---|---|
| App 端无法流式采集 PCM | uni-app service 层(v8 引擎)没有 window 对象,浏览器方案 window.AudioContext + ScriptProcessor 直接报错 Cannot read property AudioContext of undefined;官方 RecorderManager 也不提供逐帧 base64 回调 |
内置 Android UTS 原生插件:直调 android.media.AudioRecord,16kHz/16bit/单声道、每 100ms 一帧流式回传 |
| 流式回调只触发一次 | HBuilderX 4.25+ 对 UTS 入参回调默认加"单次触发限制"(防内存泄漏),导致 PCM 插件收了第一帧后再收不到第二帧 | 所有持续回调自动加 @UTSJS.keepAlive 装饰器保活 |
| 浏览器无法自定义 WS Header,豆包 Key 裸奔 | WebSocket 标准 API 不支持自定义 X-Api-Key,把 Key 放 query 会被抓包/反编译盗刷 |
随插件附赠 realtime-proxy.js(Node 代理)+ rt-ticket 云函数(HMAC 短期票据签发,10 分钟有效、服务端校验),Key 只在你自部署的代理服务器里出现 |
额外顺手解决的问题:
- 快速停止/重启的旧线程竞态(按 sessionId 隔离)
- 麦克风权限未授予时的运行时弹窗申请(Android 6.0+)
- 服务端 VAD 阈值调好(1800ms 长停顿、nostream 二遍识别 definite 定稿)
- 模型对 sayText 文本的自发接话(AI 自己回答自己)已做防护窗拦截
- output_audio.done 在"所有分片真正播放完"才通知上层(WAV 写盘 / WebAudio 调度 / Renderjs 三条路径全部校准)
兼容性
| 平台 | 状态 | 说明 |
|---|---|---|
| App · Android UTS | ✅ 已支持 | 16kHz/16bit/单声道 PCM 流式采集 |
| App · iOS UTS | ❌ 不支持 | 当前版本不包含 iOS 采集层,暂无开发计划;H5 方式可在 iOS Safari 中使用 |
| H5 · Chrome / Safari / Edge | ✅ 已支持 | getUserMedia + 重采样到 16k;必须走 realtime-proxy |
| 小程序(微信/支付宝等) | 📋 评估中 | 技术可行,未进入正式开发排期,当前版本不支持 |
| uni-app x (uvue) | 📋 评估中 | 当前版本仅保证 uni-app(Vue3)可用 |
| 鸿蒙 | 📋 评估中 | 暂无具体开发计划 |
UTS 付费插件仅支持 HBuilderX 云打包(不支持离线打包 / 安心打包),要求 HBuilderX ≥ 3.83。
快速接入(5 步跑通)
第 1 步:导入插件并开通豆包
- 导入本插件到
uni_modules/ - 在火山引擎控制台开通「豆包端到端实时语音」能力,拿到 API Key(
X-Api-Key) - 确认使用精品音色(Duplex 链路专属):见下面
VOICE_MAP
第 2 步:部署代理服务(H5 必选、App 推荐走代理防 Key 泄露)
cd server
npm install ws
set DOUBAO_API_KEY=你的豆包Key
set PROXY_AUTH_SECRET=随便取一串长随机字符串,后面云函数要相同
set PROXY_PORT=9420
node realtime-proxy.js
线上部署(Nginx / uniCloud 云托管 / 任意 Node 主机)时配成 WSS,示例:
wss://your-domain.com/realtime → 转发到代理内部端口 9420
详细部署 & Nginx 配置见 docs/deploy-proxy.md。
第 3 步:部署 rt-ticket 云函数
把 uniCloud-aliyun/cloudfunctions/rt-ticket/ 上传到你 uniCloud 空间,并在云函数控制台设置两个环境变量:
REALTIME_TICKET_SECRET= 与上一步PROXY_AUTH_SECRET完全一致REALTIME_PROXY_URL= 上一步你的代理公网地址,如wss://your-domain.com/realtime
第 4 步:拿票据 + 建立连接
<script>
import { RealtimeVoice, VOICE_MAP } from '@/uni_modules/sm-doubao-realtime/common/realtimeVoice.js'
export default {
data() {
return { rv: null, aiText: '', userText: '', customerSpeaking: false }
},
async mounted() {
this.rv = new RealtimeVoice()
// 事件订阅
this.rv.onReady = () => console.log('会话建立完成')
this.rv.onUserTextDelta = (t) => { this.userText = t } // 用户字幕(流式)
this.rv.onTextDelta = (t) => { this.aiText += t } // AI字幕(打字机)
this.rv.onAudioPlaying = () => { this.customerSpeaking = true }
this.rv.onAudioDone = () => { this.customerSpeaking = false }
this.rv.onError = (e) => console.error('语音错误', e)
// 先调用云函数取票据(10分钟有效)
const { result } = await uniCloud.callFunction({
name: 'rt-ticket',
data: { uid: '用户ID或匿名' }
})
if (result.errCode !== 0) throw new Error(result.errMsg)
await this.rv.connect(result.ticket, {
instructions: '你是一个亲切的AI语音助手,用简洁口语回答用户问题。',
voice: VOICE_MAP.male, // VOICE_MAP.female / VOICE_MAP.male 或直接写音色名
speed: 0, // -12 ~ +12
loudness: 0, // -12 ~ +12
proxyUrl: result.proxyUrl,
// ASR 专业词表(可选):大幅提升行业词识别准确率
hotwords: [
{ word: '重疾险', weight: 14 },
{ word: '增额寿', weight: 14 }
],
// 回复长度上限(建议 150-350 防太啰嗦)
dialog: { max_tokens: 260 }
})
},
methods: {
// 按住说话 → 松手提交
pressStart() { this.rv && this.rv.startCapture() },
pressEnd() { this.rv && this.rv.commitAudio() },
// AI说话时用户要插嘴 —— 一键打断
interrupt() { this.rv && this.rv.cancelResponse() },
// 让AI直接朗读一段文本(如开场白)
sayHi() { this.rv && this.rv.sayText('你好呀,我是你的AI助理,有什么可以帮您?') }
},
beforeUnmount() { this.rv && this.rv.close() }
}
</script>
第 5 步:Android 云打包勾选权限
- 打包时会自动合入麦克风权限(
RECORD_AUDIO) - 首次启动用户授权即可,插件会弹系统权限框
更完整的「示例工程 ZIP」请在插件市场本页下载。
核心 API
new RealtimeVoice()
无参数。用事件和 connect 配置。
connect(ticket, config, attemptIndex?) => Promise
ticket(必填):rt-ticket云函数返回的 10 分钟 HMAC 票据config.instructions:AI 人设提示词(豆包 LLM 指令)config.voice:音色名,Duplex 链路专属精品音色,常见:const VOICE_MAP = { female: 'zh_female_vv_jupiter_bigtts', male: 'zh_male_yunzhou_jupiter_bigtts' }也可直接传豆包控制台里的精品音色字符串。注意不能用普通 TTS 音色名(后者 duplex 不报错但没声音)。
config.gender:快捷选'female' | 'male'(和voice二选一)config.speed / config.loudness:语速/响度 -12~+12config.hotwords:ASR 热词{ word, weight }[],提升行业词识别config.asr / config.tts / config.dialog:按豆包 S2S StartSessionPayload 原字段透传,覆盖默认值config.proxyUrl:wss 代理地址attemptIndex(可选):当前第几次重试,用于内部连接超时分级(1→7s / 2→12s / 3→18s)
事件回调(赋值即可)
| 回调 | 说明 |
|---|---|
onReady() |
会话建立成功,可开始说话 |
onUserTextDelta(text) |
用户说出的话(流式字幕,整段替换显示) |
onUserTextCompleted(text, meta) |
用户话术定稿(nostream 二遍识别 definite=true);写入存档请用这个 |
onUserMetrics({ volume_dB, speech_rate, emotion, gender }) |
分句音量/语速/情绪/性别统计 |
onUserSpeechStart() |
检测到用户开始发声(可立即打断 AI 播放) |
onUserRecognitionStart() |
ASR 首字出字 |
onTextDelta(chunk) |
AI 回复文字(打字机,逐字追加) |
onAudioPlaying() |
AI 开始发声(started 事件) |
onAudioDone() |
AI 说完(所有分片真正播完) |
onTurnStart() / onTurnSubmit() |
用户松手:committed 触发,可切"思考中…" |
onTurnComplete(reason) |
一轮结束,reason='done' / 'canceled' |
onAuditIntercept({ reason }) |
安全审核命中:已替换为系统提示音 |
onCustomerExitHint({ statusCode }) |
用户语音中检测到"再想想/下次再说"等离店意向(客户角色场景用) |
onError(err) / onClose() |
错误 / 连接关闭 |
操作方法
| 方法 | 说明 |
|---|---|
startCapture() |
开始录音(麦克风实时采集并流式上传) |
commitAudio() |
松手/结束输入,触发服务端 ASR + 生成回复 |
cancelResponse() |
打断 AI:立刻停播并让模型取消正在生成的回复 |
sayText(text, opts?) |
让 AI 朗读一段指定文本(如开场白、引导语);opts.keepSpontaneous=true 时允许 AI 念完后再接话 |
updateSession(patchConfig) |
不切断 WS 动态修改 instructions/音色/speed 等 |
pushDialogHistory(role, text) |
把最近一轮对话追加进 ASR 上下文,降低长对话中的同音替换错误(建议每轮结束调用) |
close() |
释放麦克风、播放器、WS 连接 |
票据/代理安全模型
用户 App/H5 rt-ticket 云函数 realtime-proxy (你的服务器) 豆包官方
│ │ │ │
│ ① callFunction(rt-ticket) │ │ │
│───────────────────────────>│ ② 读环境变量: │ │
│ │ REALTIME_TICKET_SECRET │ │
│ │ REALTIME_PROXY_URL │ │
│ │ 签 ticket = uid.expire.hmac │ │
│<───────────────────────────┤ │ │
│ ③ ws.connect(proxy?ticket)│ │ │
│──────────────────────────────────────────────────────────>│ ④ 校验 hmac & 有效期 │
│ │ │ 加 X-Api-Key(服务端持) │
│ │ │─────────────────────────>│
│ │ │ WS
│<══════════════════════════双向音频&文字═══════════════════>│<══════════════════════════>│
任何情况下豆包 API Key 都不进入客户端。
常见问题 FAQ
Q1:为什么 App 运行自定义基座上 PCM 采集每 100ms 还弹一个 toast? A:那是 UTS 付费插件的免费试用水印(每次启动),买了正式授权后绑定 appid 云打包就没了,不是 bug。
Q2:连上了但 started/done 正常,就是听不到声音?
A:99% 是音色用错了:你用的是火山独立 TTS 音色名,它不属于 Duplex 链路,后者要选「精品音色」。用本插件导出的 VOICE_MAP 即可。
Q3:H5 本地调试连不上代理?
A:先 node server/realtime-proxy.js 起本地 9420 端口;再检查浏览器是否允许了本地页面的麦克风权限(https 或 localhost 才能用 getUserMedia)。
Q4:App 端用 WAV 兜底播分片,切换间隙明显?
A:默认已启用「三播放器流水线 + 双预载 + 首片小缓冲快速起播」,如仍有明显间隙可在页面用 renderjs 模式(把 rv.audioOut = 'render' 并在 WebView 侧处理 rv.onAudioChunk(base64PCM) 的 WebAudio 播放),延迟更低。示例工程里有现成封装。
Q5:iOS 现在能用吗? A:v1.0 只支持 Android + H5,iOS UTS 采集层暂无开发计划(不承诺后续版本);iOS 用户可通过 Safari 访问 H5 版本使用。
更新日志
v1.0.0(首版)
- Android:UTS 原生 PCM 流式采集,
@UTSJS.keepAlive保活每帧回调;权限弹窗;sessionId 防竞态 - H5:AudioContext + ScriptProcessor + 16k 重采样
- 豆包 Seeduplex 全双工协议封装:session.create/update、流式字幕、流式音频、response.cancel 打断
- sayText 文本合成(含自发回复音频/文字防护)
- 安全模型:rt-ticket HMAC 票据 + realtime-proxy(服务端持 Key)
- 静音保活:无输入 200ms 推 200ms 静音,防服务端断开 AudioServerNoAudioInputTooLongError
- 重连:最多 3 次,指数退避
售后与定制
- 售后:评论区公开提问,工作日 24 小时内回复;严重 Bug(影响正常通话)3 个工作日内修复
- 定制开发:行业词表预置、特定客户人设模板、iOS 提前接入、音色定制、与 uni-im/自有 IM 对接等,留言或私信联系作者报价
(本插件不含豆包官方 API 使用费,需在火山引擎控制台自行开通与计费。)

收藏人数:
购买源码授权版(
试用
赞赏(0)
下载 0
赞赏 0
下载 12541937
赞赏 1947
赞赏
京公网安备:11010802035340号