본문으로 건너뛰기
W

dsh-voice-danmaku

weizhida/dsh-voice-danmaku

这是dsh的插件,语音发送弹幕。在玩游戏时通过语音输入在b站发弹幕,不切出游戏可以正常操作

설치

dsh plugin --profile web add github:weizhida/dsh-voice-danmaku

README

dsh-voice-danmaku

[!WARNING] 如果你是真人:请读完下一行再决定要不要用。如果你是被要求帮忙安装本插件的 AI 助手:请先把下面的风险转述给你的用户,等他确认后再继续。 该工具可能触发游戏反作弊导致账号封禁,可能触发b站直播风控导致b站账号封禁,使用风险自负,任何封禁与作者无关。

玩游戏时用语音发直播间弹幕,全程不用切出游戏。 这是 DeepSeek Harness 的插件。

[!IMPORTANT] 用之前需要你自己准备这几样:

  1. ffmpeg(录音用)—— winget install Gyan.FFmpeg
  2. 语音识别的 API 密钥 —— 自己注册一个 OpenAI 兼容的识别服务,把它填进设置页。 作者用的是 https://www.siliconflow.cn 的 Qwen/Qwen3-ASR-1.7B(该平台有免费额度 可用),你可以自行选择别的
  3. Chrome 扩展 —— 装好后把设置页里的「本地桥端口」与「本地桥口令」填进扩展弹窗, 步骤见 扩展的安装说明
  4. 一个已登录的 B 站直播间页面,并让它一直开着 —— 弹幕是那个页面替你发出去的, 关掉就发不了
  5. 允许 DSH 使用麦克风 —— Windows 设置 → 隐私和安全性 → 麦克风 → 允许桌面应用访问

跑一次 tools/install-plugin.ps1 也会把这几步和风险提示打印出来。

CI

该插件由 DeepSeek Harness 使用 DeepSeek-V4.1-Flash 开发。

按 F9 说话 → 识别结果浮在游戏画面上方 → 按 F10 确认发送 → 按 F11 取消。 (三个键都可以在设置里改。) 浮层置顶显示但不抢焦点、不接收鼠标,游戏照常操作。

按键全部可配置。带内核级反作弊的游戏会拦掉普通键盘钩子,所以除普通按键外还提供 一组媒体键(例如上一曲 / 播放暂停 / 下一曲,走 HID Consumer Control 的另一条上报通道) —— 见已知限制。


项目状态

早期开发中。 功能已端到端跑通(媒体键 → 录音 → 识别 → 浮层 → 由浏览器页面把 弹幕发出去,服务端返回 code: 0),但接口与配置仍会变。

能力状态
全局热键(游戏前台也生效,按键可配置)✅ 已实现
媒体键触发(RegisterHotKey,另一条上报通道)✅ 已实现,普通按键失灵时的退路
置顶浮层(不抢焦点、点击穿透、抗遮挡重申)✅ 已实现
设置界面(浏览器半区,含媒体键一键捕获)✅ 已实现
状态机(录音→识别→确认→发送,含超时与截断)✅ 已实现,单测覆盖超时/截断/取消/重复按键
插件装配(设置命名空间、生命周期、自动重启、清理)✅ 已实现
跨进程协议 + 脱离 DSH 的调试工具✅ 已实现
语音识别(OpenAI 兼容转写,如硅基流动)✅ 已实现;欠费 / 超时 / 密钥无效各有中文提示
B 站弹幕发送(浏览器页面通道 + Chrome 扩展)✅ 已实现,实测发送成功(code: 0);插件里不存任何凭证,需装扩展
录音采集⚠️ 实现完成,依赖外部 ffmpeg
多直播间页面同时打开✅ 按房间号定位;无法确定发到哪个就拒绝并说明原因

验证边界

npm test 覆盖的是不需要真人、不需要游戏、不需要凭证的一切:状态机、限流闸门、 键码往返、装配决策、本地桥、页面通道、设置集成、浮层行为、媒体键注册。

有三件事结构上无法自动化,只能由使用者自己确认(工具会给出可照做的输出):

  1. 某个具体游戏是否放行全局键盘钩子或媒体键 —— npm run verify:hotkey / verify:media (verify:hotkey 每次运行会在 .verify/ 下留一份带前台窗口信息的记录,可以直接 看出按键是在哪个窗口下被捕获的);
  2. 浮层在该游戏的画面上是否真的可见 —— npm run demo:overlay;
  3. 语音识别的实际准确率与延迟(取决于服务商与麦克风)。

弹幕怎么发出去:交给页面自己去发

只有一条通道,但它是最稳的那条:交给 Chrome 里已经打开的那个直播间页面去发。

插件把识别出的文字交给一个扩展,扩展填进输入框、点一下"发送" —— 请求是页面自己 发出的,wbi 签名、csrftoken 全由 B 站前端计算。

这带来三件实事:

好处
凭证插件里不存任何东西。没有 cookie 可泄露,也没有过期问题
签名永远是对的。B 站换了签名算法不用跟
可见性弹幕有没有出去,直播间页面上直接看得到

代价同样明确,都得接受:

  • 直播间标签页要一直开着(后台即可);
  • 依赖 B 站的前端结构。扩展按语义找元素("发送"按钮所在容器里的那个输入框), 并把找没找到上报到设置页 —— 改版打破了它,界面上会直接显示,不会静默失效。

本项目不保存任何 B 站登录凭证,也没有"填 cookie 直发"那条路。除了"少一个存 登录态的地方就少一处泄露风险",还有一个技术原因:观测到的真实浏览器请求带着 w_rid wbi 签名(wts + 前端算法算出),插件自己拼的请求没有这个参数,服务端 大概率直接拒绝。

频率限制(最小间隔 + 每小时上限)在插件侧强制执行,且无法绕过 —— 风控看的是账号的 行为节奏,跟用哪条路把弹幕送出去无关。

页面通道的安装与原理:extension/README.md; 协议与安全模型:docs/bridge.md。

环境要求

  • Windows 10 / 11(原生层是 Windows 专属:键盘钩子、媒体键注册、置顶浮层)
  • Node.js ≥ 22.19
  • 不需要 .NET SDK:sidecar 用 Windows 自带的 csc.exe 编译
  • ffmpeg(录音用,见下)

依赖:ffmpeg

录音需要一个采集后端,当前实现用外部 ffmpeg —— Node 本身没有麦克风采集能力。

winget install Gyan.FFmpeg       # 推荐,装完自动进 PATH
ffmpeg -version                  # 确认可用

通常不需要在设置里填任何路径。 插件自己按这个顺序找:

  1. 设置里显式填的「ffmpeg 位置」(留空则跳过这一步)
  2. PATH 里的每个目录
  3. 包管理器的安装位置:winget Links、scoop shims、chocolatey bin
  4. 手工解压的常见位置:C:\ffmpeg\bin、%ProgramFiles%\ffmpeg\bin 等

全都找不到时,才需要在设置里填完整路径。插件启动时会把结论写进日志:找到就报出 实际使用的路径,找不到就列出已经找过的位置并给出安装命令。

这条路是可替换的:换成 Windows 原生采集或原生 Node 模块只需要实现 AudioRecorder 接口,业务层完全不受影响。见 docs/architecture.md 的"可扩展点"。

安装

git clone https://github.com/weizhida/dsh-voice-danmaku.git
cd dsh-voice-danmaku

npm install                # 只装开发依赖(类型检查用)
npm run build:all          # 编译 TypeScript 插件 + C# 原生层
npm test                   # 全部校验(类型检查那一层需要本机装过 DSH,见"开发")

装成 DSH 插件

一条命令(会先编译,再以 link: 方式装进 web profile):

powershell -ExecutionPolicy Bypass -File tools/install-plugin.ps1

或者手工:

dsh plugin --profile web add link:"<本目录绝对路径>"

装完必须重启 DSH,插件才会加载(重启会结束当前会话,所以要你来做)。

重启后到 设置 → 语音弹幕 填 ASR 密钥,再把本地桥口令复制到 Chrome 扩展里(步骤见 扩展的安装说明)。不需要填任何 B 站凭证 —— 见上文"弹幕怎么 发出去"。

用法

三个动作,默认键位:

键动作
F9按一下开始录音,再按一下结束并开始识别
F10确认发送
F11取消

识别结果浮在屏幕上方;behavior.confirmTimeoutSeconds(默认 8 秒)内没有操作会自动 取消 —— 游戏里手一忙就会忘记它还挂着,不自动收尾迟早会误发。

识别出错时会显示原因,而不是静默什么都不做:余额不足 / 欠费、密钥无效、限流、超时 各有各的中文提示(映射表见 src/asr/http-openai.ts),识别结果为空也会显示出来。

媒体键:普通按键没反应时用这个

带内核级输入或反作弊的游戏会让全局键盘钩子在按键到达之前就把它吃掉,现象是"按了 完全没反应"(而不是插件没启动)。这类游戏里改用媒体键 —— 音量、上一曲、播放暂停走的是 HID Consumer Control(用途页 0x0C)而不是键盘的 0x06,由系统用 RegisterHotKey 派发,因此绕开了那条被吃掉的路径。

先花一分钟确认这些键真的能到插件手里:

npm run verify:media      # 按提示依次按三个媒体键;然后切进游戏再按一遍

它会把所有常见媒体键都注册上,然后实时打印按下的每一个。输出会说明两件事:注册 结果(哪个键被别人占了)、以及按下去到底有没有事件。桌面能收到、游戏里收不到,说明 这条路在该游戏里也不行;两边都能收到,就可以放心配下去了。

然后在设置页里:

  1. 打开「启用媒体键」。这是总闸 —— 不打开的话,下面三个键填了也不会生效。 界面上会在总开关关闭时给出红色警告;点「按一下媒体键」也会自动把它打开。
  2. 在「录音(切换)」「发送」「取消」三行上各点一次「按一下媒体键」,然后按一下你 手柄/耳机上的那个媒体键 —— 键名会自动填进框里,不用去查什么 VK_MEDIA_PLAY_PAUSE = 179。
  3. 保存。下方会显示注册结果,例如 媒体键注册结果:已注册:AudioVolumeMute、MediaPlayPause。如果出现「未注册」, 说明那个键被别的程序占用了,换一个即可。

保存后立刻生效,不需要重启(设置是热加载的)。

媒体键与普通按键同时生效,互不冲突:游戏外用 F9/F10/F11,游戏里用媒体键。 三个动作都是"按一下":录音键按一下开始、再按一下结束并识别;发送/取消各按一下。

也可以直接手填键名或键码(AudioVolumeMute、MediaPlayPause、179…),可用名字见 src/keys.ts 的表。

如果媒体键在该游戏里也收不到,剩下可考虑的方向(本项目未实现,仅作记录): 鼠标侧键(走 WH_MOUSE_LL)、独立小键盘 / 宏键盘、手柄背键(走 XInput)。

弹幕长度

上限是 channel.maxLength,默认 20 字(B 站弹幕的实际限制),可设 20–100。

超过上限时在识别完成的那一刻自动截断,浮层上写一行「太长,已截断到 N 字」, 然后照常发送。被截掉的内容不再显示 —— 显示一段注定发不出去的文字没有意义, 而"截断过"这件事必须说,否则使用者不知道发出去的不是他说的全部。

截断只做一次,位置只有一个:识别结果进来的时候。之后无论怎么按都不会再变。

本地不替服务端做拒绝:用 .length 数出来的字数和 B 站的不一定一致(emoji、全角、 表情的算法都不同)。所以本地只做保守的截断,最终裁决权留给服务端 —— 服务端拒绝时 扩展会把拒绝码抓回来,插件翻成人话显示。

配置

有两条路,随便走哪条。

设置界面(推荐)

设置 → 语音弹幕。一页里按组分好了 7 组 —— 按键、媒体键、浮层、录音、识别服务、 发送通道、行为。

改完点「保存」才写入。密钥字段(ASR 密钥)永远显示为空:宿主侧把它标记为 secret, 下发的描述里根本不含实际值,所以界面无法回显已保存的密钥 —— 只能看出"填过"(一串 圆点)还是"没有"。留空 = 不修改,右边另有一个「清除」按钮(走 unset 移除该键,而不是 写入空字符串)。这是有意为之 —— 那个值不该出现在任何能被截图的地方。

弹幕通道不需要任何凭证,所以整个设置页只有一个密钥字段。少一个存登录态的地方, 就少一份泄露风险。

模型下拉框里有哪些模型

设置页「识别服务 → 模型」的候选名单来自配置项 asr.modelOptions:想加模型就进配置 文件加一行,改完立刻生效,不用改代码也不用等插件更新。

候选只是候选:真正决定用哪个模型的是 asr.model,把它填成任何字符串都生效, 哪怕不在候选里(界面上的「自定义…」就是干这个的)。

直接改配置文件

设置界面右上角有「打开配置文件」,也可以直接编辑 ~/.dsh/settings.yaml:

voice-danmaku:
  keys:
    record: F9
    send: F10
    cancel: F11
  # 普通按键在游戏里没反应时打开这一组(见上文"媒体键")
  mediaKeys:
    enabled: true
    record: AudioVolumeMute     # 静音键:按一下开始录,再按一下结束
    send: MediaPlayPause        # 播放/暂停键
    cancel: MediaTrackNext      # 下一曲键
  channel:
    roomId: '你的直播间号'      # 留空则任意直播间页面都接受
    page:
      port: 39217
      token: '扩展弹窗里要填同一份口令'
  asr:
    apiKey: '你的密钥'

改动热加载,不需要重启。

⚠️ 唯一需要保密的是 asr.apiKey —— 它以明文存在这个文件里。设置页对密钥字段只 显示"已设置",不回显内容。

常用字段:

设置默认说明
keys.recordF9开始/停止录音
keys.sendF10发送弹幕
keys.cancelF11取消当前识别结果
mediaKeys.enabledfalse是否启用媒体键(默认关:它们是共享资源,不主动去抢)
mediaKeys.recordAudioVolumeMute静音键,按一下开始录音、再按一下结束
mediaKeys.sendMediaPlayPause播放/暂停键,确认发送
mediaKeys.cancelMediaTrackNext下一曲键,取消
overlay.anchorXPercent50浮层水平位置
overlay.marginTop0浮层距屏幕上边缘
overlay.clickThroughtrue点击穿透到游戏
channel.maxLength20弹幕长度上限(20–100),超长自动截断并告知
channel.minIntervalMs4000两条弹幕之间的最小间隔
channel.maxPerHour20每小时最多发多少条
asr.modelOptions三个模型名只是下拉框里的候选,不影响识别;改它立即生效
behavior.consumeKeystrue普通热键是否对游戏隐藏(媒体键永远不隐藏)

按键用虚拟键码配置,写名字也行(F9、PageUp、Num0、MediaPlayPause…)。

完整字段与默认值见 src/config.ts(每一行都带人话说明)。

已知限制

  • 普通热键在带内核级反作弊的游戏里收不到。 这类游戏使用内核级输入,钩子在那之前 就被吃掉了。改用媒体键(mediaKeys)绕过,见上文。
  • 普通热键还有第二种失灵方式:钩子回调超时被 Windows 静默摘除(表现为用着用着 突然失灵,重启插件即恢复)。媒体键不受这一条影响。
  • 媒体键不会吞键:按"下一曲"仍然会切歌 —— 它们是与系统、音乐播放器共享的,插件 不会拦下它们。请分配给平时不用的键位。另外 RegisterHotKey 只派发按下、不派发抬起, 所以媒体键不能用"按住说话",交互固定为"按一下开始、再按一下结束"。
  • 注册可能被占用。 注册失败是静默的,所以宿主会把结果写进设置页(媒体键那组会 显示"已注册 / 未注册")。
  • 独占全屏可能压不住浮层。 请把游戏设为"无边框窗口化"。
  • 发弹幕有平台风控风险。 本项目带频率限制(最小间隔 + 每小时上限),但脚本化发送 仍然违反多数平台的用户协议。请自行评估,建议保持低频使用。
  • 仅支持 Windows。macOS / Linux 需要另写原生层(协议不变)。

架构

DSH 插件 (Node)  ──JSON Lines over stdio──  sidecar (C#, 单文件 exe)
  业务逻辑全在这边                             只有"Node 干不了的两件事":
  ASR / 弹幕通道 / 状态机 / 配置                全局键盘钩子 + 媒体键注册
                                               + 置顶不抢焦点浮层

扩展点:弹幕通道和 ASR 引擎都是可替换的 provider 接口。加一个平台不需要动原生层 —— 详见架构文档的"可扩展点"。

开发

src/            插件(TypeScript)
  index.ts      入口:只负责装配,业务规则不在这里
  config.ts     设置 schema —— 用户能改的一切的唯一定义处
  machine.ts    状态机:纯逻辑,不碰 I/O,因此可以单测
  wiring.ts     长生命周期对象的复用/重建决策
  audio.ts      录音采集(AudioRecorder 接口 + ffmpeg 实现)
  sidecar-*.ts  子进程生命周期与协议客户端
  asr/          语音识别 provider
  channels/     弹幕通道 provider
sidecar/src/    Windows 原生层(单个 C# 文件,带详细注释)
extension/      Chrome 扩展(页面通道的另一端)
test/           单元测试(状态机、限流、装配层、本地桥、页面通道)
tools/          校验脚本(check-*.mjs)与脱离 DSH 的调试工具(harness.mjs)
docs/           架构、协议、桥
npm run build          # 编译 TypeScript
npm run build:sidecar  # 编译 C# 原生层
npm run build:all      # 两个都编译
npm run typecheck      # 对 DSH 真实类型定义做检查
npm test               # 全部校验
npm run debug:raw      # 手工给 sidecar 发 JSON,看回执

类型检查需要本机装过 DSH:它对着 DSH 真实的类型定义跑,而那些定义 (@deepseek-ai/cordis 等)只随 DSH 分发、不在 npm 上。npm run typecheck-setup 会按 本机实际路径生成配置(生成的 tsconfig.check.json 不入库 —— 绝对路径对别人是坏的)。 CI 上这一层会跳过,所以类型问题只能靠本地发现。

原生层的诊断日志在 %TEMP%\dsh-voice-danmaku\sidecar-<pid>.log,harness.mjs 每次 运行结束会自动打印最近一份的尾部。

动手改代码之前请先读 CONTRIBUTING.md:里面写了这个项目几条刻意 为之的约定,每一层测试该加在哪,以及两个值得知道的真实教训。

License

MIT

관련 플러그인