Chuyển đến nội dung chính

dsh-ark-readaloud

Đã xác minh

dsh-ark-readaloud · v0.1.0 · MIT · Giao diện web

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

Cài đặt

dsh plugin add dsh-ark-readaloud

Xác nhận layer đã áp bằng dsh --profile default --dump-config — xem hướng dẫn cài plugin.

Mã nguồn

Phát hành lên npm mà không có repository công khai. Hãy kiểm tra nội dung package trước khi cài.

Thẻ

Tác giả

Readme

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.