跳到主要内容

dsh-ark-readaloud

已验证

dsh-ark-readaloud · v0.1.0 · MIT · Web 界面

在每条最终回答旁加一个 🔊:用方舟 Agent Plan 的豆包语音合成 2.0 朗读回复。零依赖。Read assistant replies aloud with Volcengine Ark Agent Plan TTS.

安装

dsh plugin add dsh-ark-readaloud

用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。

源码

发布到 npm 但没有公开仓库。安装前请检查包内容。

标签

作者

说明文档

dsh-ark-readaloud

在 DeepSeek Harness 的每条最终回复旁边加一个 🔊 按钮,点一下就用豆包语音合成 2.0(方舟 Agent Plan)把这条回复念出来。

复制 · 👍 👎 · 🔊 · 分支
                ↑ 就是它
  • 语音由方舟 Agent Plan 的语音模型合成,直接消耗你套餐里的 AFP,不额外买语音服务
  • 用你已经存在 DSH 里的 ARK_API_KEY 凭据,不需要再配第二个 key
  • API Key 不出本机、不进浏览器:合成在 DSH 主机进程里发起
  • 音色是豆包 2.0 系列(Vivi 2.0 / 爽快思思 2.0 / 知性灿灿 2.0 …),不是系统自带的机械音
  • 支持暂停 / 继续 / Esc 停止、倍速、换音色、新回复自动朗读

为什么需要自己写一个插件

2026-09 在 DSH 0.1.7-rc.2 上把 npm 上跟"朗读"沾边的插件全过了一遍,结论是现成的组合不了:

现成插件 为什么不满足
dsh-read-aloud 只加按钮,但用的是浏览器自带语音(speechSynthesis),拿不到豆包音色
dsh-voice-kit、dsh-speech-plugin、dsh-voice-chat、@oadank/dsh-input-tools、@hiye/dsh-voice 都声明依赖 @deepseek-ai/dsh-client-runtime,这个包在当前 DSH 里已经不存在(新版拆成了 dsh-client-store / dsh-client-modules),加载即失败
@nextnowlabs/dsh-ark-toolkit、dsh-plugin-voice 有火山 TTS,但是给模型用的工具(ark_speak),不是"每条回复点一下就读"的按钮;而且走的是「火山引擎语音技术」那条按量计费通道,要另一个 Token
dsh-omi-voice 走第三方 "Omi 引擎" 中转,不是方舟直连

另外两个硬约束,决定了必须有 host 半边:

  1. 浏览器不能直连。 方舟语音端点的 CORS 预检只放行 X-Api-App-Key 一类头,不放行 X-Api-Key,纯前端插件会被浏览器拦下。
  2. Key 不该进浏览器。 合成本来就该在主机侧做。

所以这个插件 = dsh-read-aloud 的按钮/交互骨架 + 一个新的服务端合成引擎。


工作原理

点击 🔊
  └─ 浏览器半边:取出这条消息的 markdown → 清洗(丢掉代码块/表格/URL/路径)
       → 按句子切成 ≤900 字的段
  └─ POST /ark-readaloud/tts  { text, speaker }
       └─ host 半边:从 DSH 凭据库读 ARK_API_KEY
            → POST https://openspeech.bytedance.com/api/v3/plan/tts/unidirectional
            → 拼装 NDJSON 里的 base64 音频分片
       ← 返回 audio/mpeg
  └─ 浏览器:HTMLAudioElement 播放,边播边预取下一段

注意端点里的 /plan/——这是方舟 Agent Plan 专用路径。同一路径去掉 /plan/ 属于「火山引擎语音技术」按量计费产品,拿方舟的 key 打过去会返回 Invalid X-Api-Key(code 45000010)。这是本项目踩过的第一个坑,已实测确认。

实测过的接口契约(DSH 0.1.7-rc.2 + 你的套餐,2026-09):

POST https://openspeech.bytedance.com/api/v3/plan/tts/unidirectional
X-Api-Key: <ARK_API_KEY>
X-Api-Resource-Id: seed-tts-2.0
X-Api-Request-Id: <uuid>
X-Control-Require-Usage-Tokens-Return: *

{"req_params":{"text":"…","speaker":"zh_female_vv_uranus_bigtts",
               "audio_params":{"format":"mp3","sample_rate":24000}}}

← {"code":0,"data":"<base64 mp3 分片>"} … {"code":20000000,"usage":{"text_words":25}}

安装

前提:DSH 0.1.7-rc.2 或兼容版本;profile 里已有 ARK_API_KEY 凭据(你现在这个 desktop profile 就有)。

关于路径:这个项目 2026-09-27 从 Harness工程学习/dsh-ark-readaloud/ 搬到了 Harness工程学习/02-我做的东西/DSH-插件/dsh-ark-readaloud/,并已把 DSH profile 里的 link: 同步改到新路径、删除了原位置的临时软链。因此不要随意再移动这个目录——要移动就必须同时改 ~/.dsh/profiles/desktop/package.json 里那条路径。

方式 A:从 npm 安装(推荐,换机器最省事)

图形界面:侧边栏 → 插件 → 添加插件 → 填包名 dsh-ark-readaloud → 安装 → 立即启用 → 重启 App。

命令行:

dsh plugin --profile desktop add dsh-ark-readaloud

目标机器不需要另装 Node.js:DSH 自带运行时(Node 24 + pnpm),只要能连 npm 就行。 该机器还需要一个 ARK_API_KEY 凭据(见上面「前提」)——key 不在插件里。

方式 B:本地路径安装(改代码时用)

  1. 侧边栏 → 插件

  2. 添加插件 → 把本目录的绝对路径粘进去:

    /Users/gugu/Desktop/顾卓凡的知识库/Harness工程学习/02-我做的东西/DSH-插件/dsh-ark-readaloud
    
  3. 安装 → 立即启用

  4. 重启 App(desktop profile 没开热重载)

本地路径安装的命令行写法

dsh plugin --profile desktop add "/Users/gugu/Desktop/顾卓凡的知识库/Harness工程学习/02-我做的东西/DSH-插件/dsh-ark-readaloud"

装完自检

curl -s http://127.0.0.1:19387/ark-readaloud/health
{"plugin":"ark-readaloud","endpoint":"…/api/v3/plan/tts/unidirectional","credential":"ARK_API_KEY",
 "credentialConfigured":true,"resourceId":"seed-tts-2.0","speaker":"zh_female_vv_uranus_bigtts", …}

credentialConfigured 是 true 就说明 key 读到了。

浏览器半边(那颗按钮)的激活情况看这条——它是浏览器每次加载时回报给主机的:

curl -s http://127.0.0.1:19387/ark-readaloud/diag
# {"entries":[{"stage":"apply"},{"stage":"slots-service"},{"stage":"registered"}]}  ← 全齐就是好的

想直接听一段:

curl -s -X POST http://127.0.0.1:19387/ark-readaloud/tts \
  -H 'Content-Type: application/json' \
  -d '{"text":"你好,这是朗读插件的声音。"}' -o /tmp/试听.mp3 && afplay /tmp/试听.mp3

使用

操作 效果
点 🔊 从开头朗读这条回复
朗读中再点 暂停,图标变成 ▶
暂停时再点 从原处继续
点另一条的 🔊 停掉当前,开始读那条
按 Esc 立刻停止
鼠标悬停 🔊 弹出「倍速 / 音色 / 自动朗读」面板

按钮四态:🔊 待机 → 合成中… → ⏸ 朗读中 → ▶ 已暂停。

倍速是本地播放速率(改完立刻生效,不重新合成);音色改动从下一段生效(已经合成好的那段不会变)。

正文清洗:代码块、表格行、图片语法、URL、文件路径、HTML 标签、emoji 都会被丢掉;正文段落、列表文字、行内代码的内容、链接文字保留。长回复全文读完,不截断。


配置

默认值够用。要改就编辑 profile 的 cordis.patch.yml:

- id: ark-readaloud
  name: 'dsh-ark-readaloud'
  config:
    credential: ARK_API_KEY              # DSH 凭据名
    speaker: zh_female_shuangkuaisisi_uranus_bigtts
    sampleRate: 24000
    maxChars: 6000                       # 单次请求上限,客户端默认 900 字一段
    timeoutMs: 120000

音色(豆包语音合成 2.0)

speaker 说明
zh_female_linjianvhai_uranus_bigtts 邻家女孩 2.0(默认)
zh_female_kefunvsheng_uranus_bigtts 暖阳女声 2.0
zh_female_vv_uranus_bigtts Vivi 2.0 · 多语种女声
zh_female_shuangkuaisisi_uranus_bigtts 爽快思思 2.0
zh_female_cancan_uranus_bigtts 知性灿灿 2.0
zh_female_tianmeixiaoyuan_uranus_bigtts 甜美小源 2.0
zh_female_xiaohe_uranus_bigtts 小何 2.0
zh_female_gaolengyujie_uranus_bigtts 高冷御姐 2.0
zh_male_m191_uranus_bigtts 舟 2.0 · 男声
zh_male_taocheng_uranus_bigtts 小天 2.0 · 男声
en_female_dacey_uranus_bigtts Dacey · 英文女声
en_male_tim_uranus_bigtts Tim · 英文男声

想加音色:改 lib/client.js 顶部的 VOICES 数组(数组第一项就是默认音色),保存后客户端 bundle 会在几秒内自动热更,不用重启。列表里的 ID 必须先确认有效——用 tools/verify-host.mjs 或直接 curl 打一次 /ark-readaloud/tts 带上 {"speaker":"..."} 即可。

计费:语音合成消耗套餐内 AFP,usage.text_words 是字数回执(响应头 X-Ark-Usage 就能看到)。


排障

现象 原因 / 处理
按钮点了没反应,提示「未配置 ARK_API_KEY」 凭据名不对或该 profile 没这个凭据。查 curl …/ark-readaloud/health 的 credentialConfigured
提示 Invalid X-Api-Key / code 45000010 端点被写成了不带 /plan/ 的语音技术通道。确认 endpoint 里有 /plan/
提示 401 / 403 你的套餐或 key 变了。先用 curl 打 /ark-readaloud/health 确认 key 读到了,再去方舟控制台确认 Agent Plan 有效
完全看不到按钮 先跑 node tools/verify-manifest.mjs。然后依次确认:插件页开关是开的、这条回复已定稿(被中断的部分输出没有动作行)、App 是完全退出重开的(不是关窗口)
点喇叭提示「朗读失败:cross-origin」 同源校验把桌面壳自己的请求拒了。页面 origin 是 dsh-app://app,必须放行非 http(s) 协议。跑 tools/verify-host.mjs 的 origin 矩阵确认规则
开关是开的、也重启了,还是没按钮 九成是 dsh.client.inject 里写了非客户端插件(看「设计取舍」那条)。跑 curl -s http://127.0.0.1:19387/ark-readaloud/diag,它会告诉你浏览器半边走到哪一步:apply → slots-service → registered,或 error 带原因
有按钮但一排小字报错 报错文案会直接写在按钮右边,含服务端返回的 message
日志里 Cannot find package '@deepseek-ai/...' 理论上不会出现(本插件零依赖)。真出现说明加载了旧版本,重装即可

本地验证

node tools/verify-manifest.mjs   # 清单自检:客户端半边到底能不能激活(离线,最快)
node tools/verify-client.mjs     # 50 项:真实 client.js 在桩环境里跑完整交互链路(离线)
node tools/verify-host.mjs       # 真打一次方舟接口(花几个 AFP),验证 host 半边
  • verify-manifest.mjs 读 App 的 app.asar,逐条检查:inject 里每个 id 是否真的声明了 dsh.client、bundle 注册的 id 是否等于包名、注册的插槽是否真的存在、exports["./client"] 是否落盘。它专门抓"静默不激活"这一类故障。
  • verify-client.mjs 把真实的 lib/client.js 加载进一套桩环境(模块加载器 / React hooks / DOM / fetch / Audio),把「挂载 → 点击 → 请求 → 播放 → 暂停 → 继续 → 换音色/倍速 → 失败提示」整条链路跑一遍。当前状态:47/47 通过。
  • verify-host.mjs 在临时目录里给两个 DSH 包搭桩,挂载真实的 lib/index.js,然后真打一次线上接口:验证 200 + audio/mpeg、usage.text_words 回执,以及空文本/超长/错方法/跨域四条拒绝路径。它读凭据文件但从不打印 key。

samples/朗读样例-豆包Vivi2.0.mp3 就是 verify-host.mjs 的真实产物(Vivi 2.0 音色),可以直接 afplay 试听。


设计取舍

  • 零依赖。 host 半边一行 import 都没有,package.json 也没有 dependencies / peerDependencies。原因:插件是从工作区目录按本地路径安装的,这种情况下共享包(@deepseek-ai/*)能不能解析取决于运行时机制;一旦解析失败,插件直接加载不起来。所需的两个小东西就地实现了——凭据名校验(正则 + 原样返回,和官方 credentialRef 行为一致,它的 brand 只是编译期类型)和配置默认值合并。
  • 零构建。 lib/client.js 就是产物,按 DSH Web 外壳加载的模块格式手写;没有打包步骤,改完刷新即可。
  • Key 只走主机。 浏览器半边只发文本和音色名。
  • dsh.client.inject 只能写"真正的客户端插件"。 一个包只有在 package.json 里声明了 dsh.client 才是客户端模块图上的一个节点;写了没声明的包(比如 @deepseek-ai/dsh-client-ui-primitives,它是纯库、不是插件),这条依赖永远等不到,插件就会静默不激活——没有按钮、没有报错、什么都没有。本插件第一版就栽在这里(列表是从 dsh-read-aloud 抄的,而它在 0.1.7-rc.2 上同样不工作)。tools/verify-manifest.mjs 会把这条规则查一遍。
  • 依赖只写真正提供服务的插件: ctx.locale 来自 @deepseek-ai/dsh-client-locale,ctx.slots 来自 @deepseek-ai/dsh-client-ui-renderer。
  • 两者是两份清单,缺一不可:package.json 的 dsh.client.inject(包名)只管加载顺序;客户端 bundle 里的 exports.inject = ["slots","locale"](服务名)才是 Cordis 的服务访问许可。少写后者,ctx.slots 会抛 cannot get property "slots" without inject。官方每个客户端 bundle 都导出它。
  • 页面 origin 是 dsh-app://app:桌面壳用自定义协议加载界面,所以到 127.0.0.1 的请求带着一个非 http(s) 的 Origin。同源校验必须放行"非 http(s) 协议 + 任意回环主机名",否则会把自己的请求当跨站拒掉。tools/verify-host.mjs 里有一张 6 行的 origin 矩阵把这条规则钉住。
  • 改动生效范围:客户端 bundle(lib/client.js)由 dsh-client-hmr 轮询时间戳自动热更,几秒生效、不用重启;host 半边(lib/index.js)必须重启 App——所以改 host 逻辑时一次改到位,别来回试。

已知限制

  • 音色切换从下一段生效,不能给已合成的段落换音色
  • 不做逐字高亮 / 跟随滚动
  • 超长回复会拆成多次请求(这是为了避免首段等太久,也避开单次上限)
  • 同一时刻只播一条(点新的会停掉旧的),这符合"读给我听"的直觉

归属

按钮与浮层的交互机械结构(slot 注册方式、文本清洗规则、面板定位、Esc 处理)fork 自 dsh-read-aloud(MIT,© cccc12138),语音引擎部分完全重写。见 LICENSE。

MIT.