# 技术说明 · dsh-voice-doubao > 这份是**工程向的深度文档**:协议细节、踩过的坑、每一步怎么验证的、每个可调项。 > 想先了解这个插件是干什么的、怎么装、多少钱,看 [README.md](README.md); > 想要照着一步步装的中文手册,看 [使用说明.md](使用说明.md)。 给 DSH 的语音输入加**豆包(火山引擎)云端识别**,并且可以顺手把口语顺成通顺的句子。 ## 三步就能用(TL;DR) 1. **拿 Key**:打开 → 左侧「语音识别」→ 开通 **录音文件识别模型 2.0**(默认通道要的就是它)→ 点【**试用**】→ 右上角【API 调用】拿 **API Key**。 (注意是 `/speech/new/` 新版控制台,旧版 `/speech/app` 的 Key 用不了。 想用 `transport: "stream"` 的话另外开通「豆包流式语音识别模型 2.0」的**小时版**。) 2. **填进去**:把 Key 填到 `$env:USERPROFILE\.dsh\voice-doubao.json` 的 `apiKey`,保存。 3. **重启** DeepSeek Harness,然后点输入框旁边的麦克风,选「豆包听写 + 顺稿(云端)」。 不确定缺什么,跑一条命令就知道(只读、不联网): ```powershell & "$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\primary-runtime\dependencies\node\bin\node.exe" doctor.mjs ``` > 只想识别、不想再申请方舟 Key 也完全可以——第 2 步填完 `apiKey` 就能用, > 「豆包听写(云端)」已经自带去口水话的顺滑(`enable_ddc`),且不额外收费。 > 详见第二节与第五节。 DSH 自带语音输入(本地 SenseVoice 模型 + 输入框旁的麦克风)。这里不重复造那套 UI—— 只往 `ctx.speechToText` 这个识别器注册表里多注册三个云端识别器: | 识别器 id | 麦克风里显示 | 行为 | 什么时候出现 | | --- | --- | --- | --- | | `doubao` | 豆包听写(云端) | 识别 + 识别侧顺滑(自动去"嗯/那个/就是说") | 总是 | | `doubao-polished` | 豆包听写 + 顺稿(云端) | 再让 `arkModel` 顺一遍稿(补标点、修同音错字、连成完整句子) | **配了 `arkApiKey` 才出现** | 两条用同一次识别,区别只在后面那一步润色。 > **为什么只有两条。** 早先还有第三条"旗舰模型"(`doubao-seed-evolving`),撤掉了: > 实测该模型没在账号里开通时,方舟返回 `404 ModelNotOpen`,插件只能静默回退成原文—— > 又是一条"看着有、其实不干活"的选项。顺稿用哪个模型交给 `arkModel` 一个开关, > 想换旗舰就改它(前提是那个模型确实开通了)。 > > 同理,**没配 `arkApiKey` 时"顺稿"那条根本不注册**:摆一个不做事的选项等于骗用户。 两个都是 `location: "cloud"`,不需要下载模型、没有"下载并准备"这一步。 另外还带一个**客户端插件**:在**空输入框**里**长按空格**说话、松手转文字(豆包电脑版那个手感), 见第四节。识别器沿用你在麦克风里选的那个,两条路径共用同一套配置。 ## 一、走的是哪条通道(这段是实测踩出来的,值得看) 豆包语音有两条能用的路,插件都实现了。**默认 `auto`:按录音长度自动选。** | `transport` | 产品 | 音频怎么送 | 资源 | 单价 | | --- | --- | --- | --- | --- | | `submit` | 录音文件识别 **2.0 标准版** | **base64 直传**,提交后轮询拿结果 | `volc.seedasr.auc` | **0.8 元/小时** | | `stream` | 大模型流式语音识别 | WebSocket 直推音频帧 | `volc.seedasr.sauc.duration` | 1 元/小时 | | **`auto`(默认)** | 上面两条 | **≤15 秒走 stream,更长走 submit** | 两条各自要的资源 | 同上 | ### 为什么不是二选一:真实基准数据 两条通道都在真实服务上量过(`test/bench.mjs`,每档 3 轮、交替顺序排除顺序偏差): | 素材 | 时长 | 提交+轮询 | 流式 | 谁快 | | --- | --- | --- | --- | --- | | 短句 | 4.3s | 1234ms | **1174ms** | 流式略快 | | 普通句 | 9.1s | 2343ms | **1714ms** | **流式快 27%** | | 长句 | 21.0s | **3187ms** | 3462ms | 提交略快 | | 十句话 | 43.1s | **4150ms** | 7619ms | **提交快 84%** | 交叉点在 15 秒附近。所以 `auto` 让短句省几百毫秒、长录音避免慢一倍多。 实测效果(同一批素材): | 素材 | 只用提交 | `auto` | | --- | --- | --- | | 4.3 秒 | 1.28s | **0.87s** | | 9.1 秒 | 2.44s | **1.37s** | | 43 秒 | 4.15s | 4.36s | 想强制走某一条:把 `transport` 设成 `submit` 或 `stream`。 ### 中间踩的三个坑(都是实测才暴露的) **坑一:文档说必须公网 URL,其实不是。** 录音文件识别的文档里 `audio` 只写了 `url`, 官方 cURL / Go / Python 示例也都传 `url`,看着像"必须先把音频托管到公网"。 但那个结论是在**账号没有任何资源授权**的时候下的——所有请求都停在 403,根本问不到正文。 后来资源一授权,我直接把 9 秒 WAV 的 base64 塞进 `audio.data`: ``` 提交结果: HTTP 200 X-Api-Status-Code=20000000 X-Api-Message=OK 查询结果: 嗯,那个就是我想说一下,我们是不是应该把这个语音识别的接口先接上,然后再说后面的事情? ``` **坑二:流式通道的音频格式声明写错了。** 流式会话帧里我声明 `format: "wav"`, 但音频帧推的是**剥掉 WAV 头的裸 PCM**。声明和内容对不上,服务端直接回: ``` The audio could not be decoded using the declared format. Check the format and audio data. ``` **必须声明 `pcm`。** 这个 bug 只有真实服务能抓到——本地 mock 会把错误的声明也当成正确, 所以那条测试一直是绿的。现在测试里的期望值也改成了 `pcm`。 **服务端是收 base64 的。** 于是完全不需要对象存储、不需要隧道、不需要公网。 **坑二:开通「并发版」会得到一个看不懂的错。** 如果开通时落到并发版, 第一次请求会回 `quota exceeded for types: concurrency`。它读起来像"额度用完了", 实际意思是"买错了计费方式"——并发版要按路数单独买(默认 0 路,500 元/路/月)。 个人用请开通**小时版/按量后付费**。 ## 二、配密钥(通常只填一个字段) 打开 `$env:USERPROFILE\.dsh\voice-doubao.json`: ```json { "apiKey": "豆包语音控制台签发的 API Key", "arkApiKey": "火山方舟的 API Key", "arkModel": "方舟控制台里看到的模型 ID" } ``` - `apiKey`:**必须**。来自「豆包语音」控制台,用来做识别。 - `arkApiKey`:可选。来自「火山方舟」控制台,用来做顺稿。不填就是原样识别,不会报错。 - `arkModel`:顺稿用的模型 ID,照抄方舟控制台里的那个。 > ⚠️ **两个 Key 是两套东西,不能混用。** 火山方舟(Ark)的 Key 不能用于语音识别; > 语音识别的 Key 也不能用来调方舟的大模型。这是两个控制台分别签发的。 控制台地址: | 用途 | 地址 | | --- | --- | | 豆包语音(新版,**用这个**) | `https://console.volcengine.com/speech/new/` | | 豆包语音 API Key 管理 | `https://console.volcengine.com/speech/new/setting/apikeys` | | 旧版语音控制台 | `https://console.volcengine.com/speech/app` | | 火山方舟(顺稿用) | `https://console.volcengine.com/ark` | ### 开通步骤(从零到能跑) 1. 注册[火山引擎](https://www.volcengine.com/)并完成**实名认证**(未认证无法继续)。 2. 打开[豆包语音控制台(新版)](https://console.volcengine.com/speech/new/)—— 注意是 `/speech/new/`,不是旧版 `/speech/app`。 3. 左侧「语音识别」→ 找到**豆包流式语音识别模型 2.0** → 点【**试用**】领 **20 小时**免费额度。 (这一步是手动的,不点就没有额度;"服务开通为正式版后试用额度失效",所以先领试用。) 4. 在「开通管理」里确认该模型已开通。 5. 右上角【API 调用】→ 获取 API Key,或直接进 [API Key 管理](https://console.volcengine.com/speech/new/setting/apikeys)。 6. 把 Key 填进 `voice-doubao.json` 的 `apiKey`,保存。 7. 重启 DeepSeek Harness(见第四节)。 > **想用「+ 顺稿」那两条识别器才需要方舟**,只想识别的话跳过下面这步: > 打开[火山方舟控制台](https://console.volcengine.com/ark) → 「开通管理」里开通 > `doubao-seed-2-1-lite-260915` → 「API Key 管理」创建长效 Key → > 填进 `voice-doubao.json` 的 `arkApiKey`。**方舟的 Key 和上面那个是不通用的。** 旧版控制台给的是 App ID + Access Token,那就填 `appId` + `accessToken`、`apiKey` 留空。 **两个 Key 对应两档效果,可以只用第一个:** | 你有的 Key | 效果 | | --- | --- | | 只有豆包语音 | 识别 + **识别侧顺滑**(官方 `enable_ddc`:删掉停顿词、语气词、语义重复词)。麦克风里选「豆包听写(云端)」 | | 再加火山方舟 | 在上一档基础上**再顺一遍稿**:补标点、修同音错字、把半截话连成完整句子。选「豆包听写 + 顺稿(云端)」 | 所以**不填 `arkApiKey` 也能用**,只是少最后那道润色。 也可以用环境变量:`DOUBAO_SPEECH_API_KEY`、`DOUBAO_SPEECH_APP_ID`、`DOUBAO_SPEECH_ACCESS_TOKEN`、 `DOUBAO_SPEECH_RESOURCE_ID`、`DOUBAO_SPEECH_ENDPOINT`、`DOUBAO_ARK_API_KEY`、`DOUBAO_ARK_MODEL`、 `DOUBAO_ARK_ENDPOINT`。 ## 三、选对资源(这一步直接决定能不能用) 默认通道(`transport: "submit"`)只要一个资源: | 配置项 | 默认值 | 说明 | | --- | --- | --- | | `submitResourceId` | `volc.seedasr.auc` | 录音文件识别模型 2.0 **标准版**,0.8 元/小时 | 想改用流式(`transport: "stream"`)时,看这一组: | `resourceId` | 含义 | 后付费 | | --- | --- | --- | | **`volc.seedasr.sauc.duration`**(默认) | 流式 **2.0** 小时版 | 1 元/小时 | | `volc.seedasr.sauc.concurrent` | 流式 2.0 并发版 | 500 元/并发/月 | | `volc.bigasr.sauc.duration` | 流式 **1.0** 小时版 | 4.5 元/小时 | **一定要选小时版**(按音频时长付费)。**并发版要按路数单独买,默认 0 路, 第一次请求就会被 `quota exceeded for types: concurrency` 拒掉**——这是实测踩过的坑。 填错资源会在握手/提交阶段被拒,报 `requested resource not granted`。 `node probe-resources.mjs` 会把四种流式资源 + 四种录音文件资源的授权状态一次列清楚。 ## 四、长按空格说话(客户端插件) 除了点麦克风按钮,还支持**在空输入框里长按空格说话、松手转文字**——就是豆包电脑版那个手感。 | 操作 | 结果 | | --- | --- | | 输入框**为空**时光标在里面,按住空格 | 开始录音(约 0.2 秒后启动) | | 松手 | 停止录音 → 识别 → 顺稿 → 文字填进输入框 | | 按下后**很快松手**(< 0.2 秒) | 不录音,照常打出一个空格 | | 按住时按 `Esc`、或窗口失焦 | 取消这次录音,不填任何东西 | | 输入框里**已经有字** | 本插件完全不插手,空格就是空格 | **冲突面是刻意做小的**:只有「焦点在空输入框」才拦截空格。一旦有字,长按空格也不会触发。 ### 它是怎么实现的 没有改官方那个麦克风插件(那是编译好的 bundle,组件和录音类都不导出)。走的是两条**对外可用**的接口: 1. `ctx.slots` 的 `conversation.input.activity` 插槽 —— 注册进去就能拿到 Host 传下来的 `inputActions`(用来把文字填进输入框); 2. `ctx.remote.speech` —— 官方 bundle 已经把这个 remote 挂上了,直接注入就能用 `transcribe()` / `catalog()`,不用自己重新声明远程协议。 录音(`MediaRecorder` → `decodeAudioData` → `OfflineAudioContext` 重采样到 16 kHz → PCM16 WAV) 是照着官方那套写的,重采样交给浏览器自带的 Web Audio,不自己写滤波器。 **识别器沿用你在麦克风里选的那个**(读 `catalog().selection`),所以长按空格和点按钮走的是同一条通道、同一套配置。 > 已知限制:它是按「焦点在空输入框」判断的,没有进一步确认那个输入框一定是聊天框。 > 在别的**空白**输入框(比如某个空搜索框)里长按空格,同样会触发录音。 > 正常人不会在空框里长按空格,所以没为此加更复杂的判定。 ## 五、重启 DSH 生效 这个 profile 的插件清单是**启动时组合**的,装完之后必须重启一次 DeepSeek Harness。 客户端那一半(长按空格)还要**刷新一下页面**才会加载。 **别猜,跑体检**——一条命令告诉你缺什么、下一步做什么(只读、不联网): ```powershell & "$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\primary-runtime\dependencies\node\bin\node.exe" doctor.mjs ``` 它会逐项检查:密钥文件是否存在且 JSON 合法、识别凭据填没填、插件自带依赖在不在、 profile 里的链接/依赖/bundle 声明是否就位、以及**插件是否真的被加载了**, 最后打印下一步(或"全都就绪")。 **手动确认加载成功**:看这个文件在不在—— ``` $env:USERPROFILE\.dsh\voice-doubao.status.json ``` ```json { "loaded": true, "plugin": "dsh-voice-doubao", "providers": ["doubao", "doubao-polished"], "credentials": "apiKey", "polish": true, "at": "2026-10-14T12:34:56.789Z" } ``` `credentials` 是 `none` 就说明密钥没读到。没有这个文件=插件没加载。 然后点输入框旁边的麦克风,识别器列表里会多出上面两条;选中的会被 `speech-to-text` 服务记进 profile,下次直接生效。想回本地 SenseVoice 就再选回来。 ## 五、价格 **识别(豆包语音)**——下面全部出自官方[计费说明](https://docs.volcengine.com/docs/DoubaoVoice/Billinginstructions-21?lang=zh) 和[计费概述](https://docs.volcengine.com/docs/DoubaoVoice/BillingOverview-15?lang=zh)(正文已从服务端渲染的页面逐字核对): | 服务 | 后付费 | 资源包最低档 | | --- | --- | --- | | **录音文件识别模型 2.0**(本插件默认通道) | **0.8 元/小时** | 30 小时 23 元(≈0.77 元/小时) | | 豆包流式语音识别模型 2.0 | 1 元/小时 | 30 小时 28 元(≈0.93 元/小时) | | (旧)大模型流式语音识别 1.0 | 4.5 元/小时 | 30 小时 132 元 | | (旧)大模型录音文件识别·极速版 | 4.5 元/小时 | 30 小时 132 元 | **免费试用额度:20 小时 / 半年**,流式 2.0 和 1.0 都有。需要你在控制台**手动点【试用】**领取; 「试用额度在额度用尽、试用到期或服务开通为正式版后失效」。按你这种用量(20 分钟/月), 20 小时相当于 **60 个月**——半年试用期内基本就是白用。 **所以套餐怎么选**: - **别买资源包。** 30 小时 28 元只比后付费省 2 元,却锁一年、过期作废。 - **别买并发。** 默认给 50 并发,个人用永远用不到。 - 就一句话:**先点【试用】领 20 小时,到期后按量后付费 1 元/小时。** - 参考量级:60 秒 × 20 次/月 = 20 分钟/月 = 0.33 小时 ≈ **0.27 元/月**(按 0.8 元/小时)。 **顺稿(火山方舟)**:按 token 计费,可选模型差 10 倍价,但绝对金额都很小。 | 模型 | 输入(元/百万 token) | 输出 | 单次顺稿约 | 100 次/月约 | | --- | --- | --- | --- | --- | | `doubao-seed-2-1-lite-260915`(默认) | 0.8 | 2.7 | 0.06 分 | 6 分钱 | | `doubao-seed-2-0-mini-260428` | 0.2 | 2.0 | 0.03 分 | 3 分钱 | | `doubao-seed-evolving`(旗舰档) | 6 | 30 | 0.6 分 | 6 毛钱 | > `doubao-seed-evolving` 是方舟的"能力优先"别名,官方说它周级更新、当前版本等同 > `doubao-seed-2.1-pro`,价格也与 pro 档一致(6 / 30)。**它是大语言模型,做不了语音识别**—— > 识别那一端必须用「豆包语音」的 Key,两笔钱是分开的。 > > 想换成它,改一个字段就行:`"arkModel": "doubao-seed-evolving"`。 > 具体字符串建议照抄方舟控制台「模型广场 → 该模型 → 调用」页里显示的 model ID。 > (单价来自[官方模型价格页](https://docs.volcengine.com/docs/ark/model-pricing?lang=zh)的口径与 > [第三方聚合页](https://ai.360.com/open/en/models/bytedance/doubao-seed-evolving)的对照, > 官方页正文是 JS 渲染的抓不到,**最终以控制台计费页为准**。) ## 六、我验证到哪一步了 **四套验证,全过。** 都是本机跑的,不联网、不花钱。 | 套件 | 结果 | 覆盖 | | --- | --- | --- | | `test/offline.test.mjs` | **168 项全过** | 两条通道、通道自动选择、WAV 契约、协议分帧、失败分类(含静音)、时序、取消、并发、顺稿护栏、回退、配置、状态文件实时性、识别留痕 | | `test/client.test.mjs` | **70 项全过** | 长按空格整条链路:按键拦截规则、短按补空格、WAV 编码、调 transcribe、填进输入框、麦克风被拒的提示 | | Cordis 集成测试 | **22 项全过** | 真实 cordis 4.0.4 + 真实 speech-to-text 服务 | | Loader 挂载测试 | **13 项全过** | 真实 cordis-plugin-loader 走 bundle 清单 → 挂载 | | 顺稿提示词检查 | **21 项全过** | 提示词实测输出 + 护栏是否放行 | | **真实接口** | **跑通** | 真实 Key + 真实服务 + 生产代码路径,拿到正确转写 | 细节: - **协议分帧**按官方协议页逐字段核对:首字节 `0x11`(版本 1|头长 1)、消息类型、 JSON 序列化、payload size 大端、音频帧无序列号、负包 flag `0b0010` 且 payload size 为 0。 服务端返回的两种布局(带 / 不带 sequence 字段)都能解析。 - **端到端**用 `test/mock-sauc.mjs` 做本地假服务。这个 mock **自己独立实现了一遍分帧**, 没有复用插件的编解码器——否则只能证明"两边自洽",证明不了"跟官方协议一致"。 它校验:握手头、会话参数(16k/16bit/单声道)、推送的 PCM 字节数与录音完全一致、 2.5 秒音频按 200 ms 分包得到 13 个音频包、最后一包是空 payload 的负包。 - **收尾时序**(这块修掉了两个真 bug):流式输入模式在音频超过 15 秒时会先返回一段 **中间结果**。原来的逻辑"收到带文字的返回就收尾"会把这段中间结果当成最终答案, 长录音会被截断。现在以**负包标记**为准,关闭连接兜底,另有一个足够长的静默安全阀。 测试覆盖了两种到达顺序(中间结果早于负包 / 晚于负包),并且**额外用一条断言证明 这些测试不是空跑**——把安全阀调到比在途响应还短,就真的会截断。 - **顺稿提示词**用一个**代理模型**实测过(不是豆包本身,所以只是提示词可读性的证据): 给提示词 4 条带口水话的识别结果,包括一条**提问**。结果是——提问被原样整理成提问 **没有被回答**(这是最关键的一条护栏),三条口水话都被清掉,长度全部变短,没有被 包装文字污染。第 4 条纯应答句保留了"嗯嗯/好的好的"(这两个词本身承载同意的意思, 保留说得通)。实测输出已经固化成测试夹具。 - 这次实测还**反查出护栏太松**:原来的长度上限是 `3×原文+40`,一个中等长度的提问被 回答成一百多字也照样放行。已收紧到 `1.6×原文+20`。护栏是粗网,拦的是"明显跑去回答 问题"的膨胀输出;真正的第一道防线是提示词里的"不回答问题"。 - **Loader 挂载测试**用**真实的 cordis-plugin-loader**(从 app.asar 抽出)跑了一遍重启时会走的 路径:解析 bundle 清单 → 用 js-yaml 解析 `cordis.patch.yml` → 把 `insert` 行交给 `loader.create()`(baseUrl 指向 profile)→ 断言两个识别器注册成功、行配置真的下发到了 `apply()`、并且经 loader 挂载的插件能完成一次识别。**这证明重启后确实会挂上。** - **集成测试**用真实 cordis 运行时挂载本插件,走服务自己的 `resolve()` / `transcribe()`。 它能抓到假 ctx 抓不到的问题:`location: "cloud"` 能否通过服务 schema、语言校验、 未知 providerId 拒绝、返回三段式是否满足服务的严格校验。 - **取消与并发**:麦克风界面上有停止/取消,用户也可能连着说几段,这两条都测了。 取消要在毫秒级中断、不能干等到 60 秒超时,并且要把 WebSocket 关掉不泄漏; 三段同时识别要各开各的连接、各拿各的文字、互不串台(假服务按连接编号返回不同文字来验证)。 这里还修掉一个会让**整个进程挂掉**的 bug:`signal` 在连接建立前就已取消时,原来会在 socket 的错误监听还没绑上就提前返回,而 `ws` 在握手完成前被 close 会 emit `error`—— 没有监听者的 `error` 就是未捕获异常。修法是先把所有监听绑好、再处理已取消的信号。 这条是先写测试复现(`WebSocket was closed before the connection was established`)才修的。 - **回退路径**:顺稿接口**连接被拒**或**卡住不回** → 都保留识别文字(后者受 `timeoutMs` 约束, 不会把已经拿到的结果一起拖死);服务端错误帧 → 带出错误码; 握手被拒 → 按服务端原话区分两种完全不同的原因(见下节);服务端不回 → 超时中断; 缺密钥 / 非法音频 → **在联网前就拒绝**(断言了假服务一次连接都没收到)。 ### 真实接口验证到哪一步了 用真实 Key 打过一次真实服务(`openspeech.bytedance.com`),结论如下: **已经被真实服务确认的**: - 网络、TLS、WebSocket 握手都能到服务端; - **握手头格式正确**——服务端准确识别了 `X-Api-Key`,并且能区分"无效 Key"和"有效但无权限"。 头名或格式写错的话只会看到一种错误,不会出现这种区分; - `X-Api-Resource-Id` 被正确解析(错误信息里回显了传入的 resource id); - 错误能从服务端**原话**透传到用户可见的提示里。 ### 真实接口验证:两条都跑通了 用真实 Key、真实服务、插件自己的生产代码路径,**识别和顺稿都跑通了**: ``` === 素材 === test/sample-zh.wav 合法:16 kHz / 1 声道 / 16 位,时长 9.05 秒,289806 字节 === 识别 === (豆包语音,2.86 秒) 原文:那个就是我想说一下,我们是不是应该把这个语音识别的接口先接上,然后再说后面的事情? === 顺稿 === (火山方舟 doubao-seed-2-1-lite-260915,1.38 秒) 整理后:我想说一下,我们是不是应该把这个语音识别的接口先接上,再说后面的事情? ``` 素材是用 Windows 自带 Microsoft Huihui 中文语音合成的(内容就是一段带口水话的口语)。 识别逐字吻合;顺稿把"那个就是"这类口水话和"然后…再"的拖沓去掉,正是想要的效果。 **这一路上被真实服务确认的事**: - 网络、TLS 都能到 `openspeech.bytedance.com` 和 `ark.cn-beijing.volces.com`; - **鉴权头格式正确**——服务端能区分"无效 Key"和"有效但无权限"。这是用**对照实验**确认的: 同一个接口只换 Key,乱编的 UUID 得到 `401 Invalid X-Api-Key`,真 Key 得到 `403 requested resource not granted`。头名写错的话不会出现这种区分; - **`audio.data`(base64)确实被接受**(见第一节的坑一),提交返回 `20000000`,轮询拿回 `result.text`; - **顺稿的完整链路**(方舟 chat completions + 关深度思考 + 提示词护栏)在真实模型上成立; - 错误能从服务端**原话**透传到用户可见的提示里。 **还没验证的只有一件事**:在 DSH 界面里点麦克风做一次真实录音(需要重启让插件挂载后, 用户自己说一句)。插件侧的挂载、注册、配置下发都已经用真实 loader 验证过了。 ## 七、可调项 写进 `cordis.patch.yml` 里插件行的 `config`,或写进上面那个 JSON 文件都行: | 字段 | 默认值 | 说明 | | --- | --- | --- | | `transport` | `auto` | 走哪条通道:`auto` / `submit` / `stream`,见第一节 | | `autoStreamMaxSeconds` | `15` | `auto` 模式下,不超过这个秒数走流式 | | `submitEndpoint` | `…/api/v3/auc/bigmodel/submit` | 提交通道:提交 | | `queryEndpoint` | `…/api/v3/auc/bigmodel/query` | 提交通道:查询(复用同一个任务 id) | | `submitResourceId` | `volc.seedasr.auc` | 提交通道的资源 id | | `pollIntervalMs` | `150` | 提交通道:轮询间隔(第一次查询不等待) | | `maxPolls` | `100` | 提交通道:最多轮询次数(默认约等 15 秒) | | `endpoint` | `wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_nostream` | 流式通道:接口 | | `resourceId` | `volc.seedasr.sauc.duration` | 流式通道的资源 id,见「选对资源」 | | `streamFormat` | `pcm` | 流式会话帧声明的音频容器格式。**必须是 `pcm`**(推的是裸 PCM),写成 `wav` 会被服务端拒绝——见第一节坑二 | | `modelName` | `bigmodel` | 模型档 | | `chunkMs` | `200` | 每个音频包的时长,厂商建议 100~200 ms | | `semanticSmoothing` | `true` | 识别侧的口语顺滑(`enable_ddc`):官方文档说是"删除或修改 ASR 结果里的停顿词、语气词、语义重复词"。**不额外收费、不需要第二个 Key**,所以默认开;想要逐字原样就设 `false`。它和「顺稿」是两回事,可以叠加 | | `ackTimeoutMs` | `1000` | 等会话帧回执的上限,等不到也开始推音频 | | `settleMs` | `1500` | 收尾安全阀:音频发完后服务一直不说话时的静默窗口。必须比任何在途响应都长 | | `arkEndpoint` | `https://ark.cn-beijing.volces.com/api/v3/chat/completions` | 方舟 chat 接口 | | `arkModel` | `doubao-seed-2-1-lite-260915` | 顺稿用的模型。换别的(如旗舰别名 `doubao-seed-evolving`)必须先在该账号开通,否则 404 ModelNotOpen | | `polish` | `true` | 是否允许顺稿 | | `timeoutMs` | `60000` | 单次请求超时 | | `logTranscripts` | `false` | 打开后把识别结果写进 DSH 日志 | ## 八、常见报错 | 现象 | 原因 | | --- | --- | | `HTTP 401 {"error":"Invalid X-Api-Key"}` | Key 不对:用了方舟的 Key,或用了旧版控制台(`/speech/app`)的凭据 | | `requested resource not granted` | **Key 有效,但资源没授权给你。** 去控制台开通对应模型(默认提交通道要「录音文件识别模型 2.0」);已开通就检查 Key 与模型是否在同一个项目下 | | `quota exceeded for types: concurrency` | **开通的是「并发版」**,而并发要单独按路数买(默认 0 路,500 元/路/月)。个人用请改开通**「小时版」**(按量后付费) | | 握手被拒但上面几种都不是 | `resourceId` 和你实际开通的模型不匹配(2.0 与 1.0 的 id 不同) | | `code=45000001` 之类 | 请求参数问题;打开 `logTranscripts` 看会话参数 | | 提示没配密钥 | `voice-doubao.json` 没读到;看状态文件里的 `credentials` | | 识别出空文字 | 静音音频;检查麦克风 | | 顺稿没生效 | 没填 `arkApiKey`(会在日志里写「顺稿失败,已回退」) | > **选计费方式这一条是实测踩出来的**,不是从文档推的: > 只开通「豆包流式语音识别模型 2.0」而没注意计费方式时,如果落到并发版, > 第一次请求就会收到 `quota exceeded for types: concurrency`。它读起来像"额度用完了", > 实际含义是"买错了计费方式"。`probe-resources.mjs` 会把四种资源的授权状态一次列清。 ## 九、装在哪、怎么卸 装的位置(profile = `desktop`): - 代码:`(插件目录)\`(`node_modules\ws` 也在这里,插件自带) - 链接:`$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-voice-doubao`(Junction) - 声明:`$env:USERPROFILE\.dsh\profiles\desktop\package.json` 的 `dependencies` 和 `dsh.profile.bundles` 卸载:把 `dsh-voice-doubao` 从 `bundles` 和 `dependencies` 里删掉,重启即可。 装之前那份 `package.json` 有备份:`package.json.bak-voice-doubao-<时间戳>`,覆盖回去就是原样。 ## 十、自测命令 统一用 DSH 自带的 Node: ```powershell $node = '$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\primary-runtime\dependencies\node\bin\node.exe' # 0) 体检:现在能不能用、缺什么(推荐先跑这个) & $node doctor.mjs # 1) Host 侧:单元 / 协议 / 端到端(153 项,不联网) & $node test\offline.test.mjs # 2) 客户端侧:长按空格那条链路(70 项,不联网、不需要浏览器) & $node test\client.test.mjs # 3) 两条通道的真实耗时基准(联网、花钱极少) & $node test\bench.mjs 3 # 4) 只验证脚本本身(不联网、不需要 Key) & $node test\live.mjs --mock # 5) 真实接口(需要 Key) & $node test\live.mjs & $node test\live.mjs 你自己的录音.wav # 必须 16kHz 单声道 PCM16 & $node test\live.mjs --asr-only # 只验证识别 # 6) 资源授权诊断(握手失败、或想确认开通了哪个资源时跑) & $node probe-resources.mjs ``` 第 3 条会打印识别原文和顺稿后的句子,一眼看出效果对不对。 插件本身不依赖它们,那两套是开发时的验证现场:从 DSH 的 `app.asar` 里抽出真的 `@deepseek-ai/cordis` 和 `cordis-plugin-loader`,在独立目录里执行(需要 `--expose-internals`): ```powershell & $node integration.test.mjs & $node --expose-internals loader.test.mjs # loader 要跟启动器一样带这个开关 & $node prompt-check.mjs # 顺稿提示词的实测输出走一遍护栏 ``` 这个目录是临时产物,可以删。