更新记录

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~+12
  • config.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 使用费,需在火山引擎控制台自行开通与计费。)

隐私、权限声明

1. 本插件需要申请的系统权限列表:

Android 所需权限: - android.permission.RECORD_AUDIO 麦克风录音权限(用于 PCM 音频帧实时采集) - android.permission.INTERNET 网络权限(与豆包实时语音 WebSocket 服务通信) iOS 所需权限: - NSMicrophoneUsageDescription "需要使用麦克风进行语音对话"(插件 app-ios/config.json 里声明,打包时自动合入 Info.plist) H5 端:首次使用 getUserMedia 时浏览器会弹出麦克风授权对话框,无需前端静态声明。

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

1. 采集内容: 插件通过麦克风采集用户的实时语音(16kHz / 16bit / 单声道 PCM 音频帧), 不写入任何本地文件,也不持久化到磁盘——仅用于流式上传到豆包服务端进行 ASR 识别 + LLM 生成 + TTS 合成(端到端全双工)。 插件本身不采集任何用户身份信息(手机号/昵称/设备号), 使用者(插件买方)可在自己的业务层给 rt-ticket 云函数传递可选 uid, 仅用于区分票据签发主体,不会被插件包识别或转发给作者。 2. 发送的服务器地址: ① 豆包官方实时语音服务: wss://openspeech.bytedance.com/api/v3/duplex/realtime/dialogue (App 端可由代理直连转发;H5 端浏览器无法自定义 WebSocket Header, 必须经由买家自部署的代理服务器转发) ② 买家自行部署的 WebSocket 代理(H5 必经路径,App 也推荐走代理以避免 Key 被反编译): 该代理地址由买家在初始化插件时通过 proxyUrl 自行传入, 例如 wss://买家自己的域名/realtime;插件包不内置任何作者侧的服务器地址, 也绝不回传任何语音或文本给作者。 ③ rt-ticket 云函数(部署在买家自己的 uniCloud 空间): 用于签发 10 分钟有效的 HMAC 短期票据,票据只含 uid+过期时间+签名, 不携带任何对话内容。 3. 数据用途: 用户语音 → 豆包端到端模型完成 ASR + LLM + TTS 一体化处理 → 返回文字字幕 与合成语音回播给用户。对话内容仅在豆包服务端按豆包官方隐私政策处理。 插件包本体不采集、不上传、不保存任何用户个人信息,也不会给作者发任何遥测。 4. API Key 安全: 豆包 X-Api-Key 仅保存在买家自行部署的 realtime-proxy 代理服务端环境变量 (或 App 买家选择直连时打包进客户端)。插件包中不固化任何密钥。 代理侧收到 HMAC 票据(10 分钟有效

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

不包含。本插件无任何广告、开屏、横幅、激励视频、插屏、积分墙等商业化展示。 【最后:隐私、插件名、图片、权限等信息均与事实不符的自负】

暂无用户评论。