Skip to content

About

QQ phone tool experiment: simulated voice calls and bridge protocol; real QQ/NapCat calls are not feasible.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

koishi-plugin-qq-phone

QQ 电话工具插件,提供拨打、接听、挂断、说话和状态查询,并把电话动作注册到 ChatLuna 原生工具与 chatluna-character 的回复工具字段。

给后来者:个人做“真电话”这条路基本走不通。

如果你想要的是“到点手机真的响铃,接起来还能和机器人实时说话”,请不要在这个方向上继续投入。OneBot / NapCat 没有运营商电话 API;AVSDK Hook 方案需要独立侧车、虚拟声卡和版本敏感的外部插件,个人维护成本高且 QQ 账号有风控风险;Twilio 不能拨打中国大陆,阿里云语音通知个人不能开通,个人最后只剩实体 SIM + GSM/VoLTE 网关这条硬件路线,成本和稳定性都不划算。

本仓库保留下来,是作为“语音消息模拟 + 外部 bridge 协议”的实现记录和踩坑说明,不是能打真电话的插件。若你只是想在 QQ 里和机器人语音聊天,simulated 模式够用;若你已经有自己的电话网关,可以按 bridge 协议接进来。

重要限制:OneBot / NapCat 没有运营商电话 API

先说明结论,避免误解:

  • OneBot 11 与 NapCat 的公开 API 只有私聊/群聊消息、语音消息、get_record 等能力,没有“拨打 QQ 电话”或“接听 QQ 电话”的接口。
  • 社区插件 cortico-world-qq-better 也明确写了:OneBot / NapCat 无法真正接入运营商电话;它的默认方案是用 OneBot 语音消息模拟通话。
  • 真正接近实时对讲的方案(如 MaiBot QQ 语音通话适配器)需要在 NapCat 侧安装独立插件、Hook QQ 的 AVSDK、配置 PulseAudio 虚拟声卡,属于版本敏感的外部集成,不是普通 Koishi 插件能独立完成的。

因此本插件提供两种 transport:

  1. simulated(默认,开箱可用)
    用 QQ 语音消息模拟通话:机器人发送语音/文字开场,用户回复语音;插件负责 STT → 业务层 → TTS → 语音消息。不是运营商电话,也不会让手机响铃。
  2. bridge(可选,接真实语音桥)
    把拨号/接听/挂断/音频转发到外部 HTTP 服务;外部服务可以接 NapCat AVSDK、PulseAudio、CABLE 虚拟声卡或其他电话网关。插件本身不包含 AVSDK Hook。

如果你需要“真机响铃、双方实时说话”,必须部署 bridge 侧车;本插件只提供协议和工具层。

功能

  • simulated:拨打、接听、挂断、发送语音/文字、接收用户语音并交给 STT
  • bridge:把通话动作转发给外部语音桥接服务
  • ChatLuna 原生工具 qq_phone
  • chatluna-character 回复工具字段 qq_phone(数组,支持一次多个动作)
  • STT / TTS provider 注册接口,供其他 Koishi 插件接入
  • 通话超时自动挂断、单次通话最长时长
  • 允许用户/群白名单
  • 通话状态查询与列表

安装

cd <你的 Koishi 实例目录>
npm install ./koishi-plugin-qq-phone-0.1.0.tgz

Yarn 4:

corepack yarn add "koishi-plugin-qq-phone@file:./koishi-plugin-qq-phone-0.1.0.tgz"

安装后启用 qq-phone,并确保 koishi-plugin-chatluna 已启用(如果要注册 ChatLuna 工具);要注册 Character 字段还需要 koishi-plugin-chatluna-character。

命令

命令 说明
qqphone.dial <QQ号> 拨打;默认使用 greeting 开场
qqphone.answer [callId] 接听;不填则接听当前用户最近的通话
qqphone.hangup [callId] 挂断
qqphone.say <文本> 在当前通话中合成语音并发送
qqphone.status 查看当前通话
qqphone.calls 列出未结束的通话

dial 的 -g 选项可覆盖开场白:

qqphone.dial 10001 -g 喂,是我,方便说话吗?

配置

字段 默认值 说明
transport simulated simulated 或 bridge
bridgeUrl 空 bridge 服务地址,如 http://127.0.0.1:8787
bridgeToken 空 bridge Bearer Token
greeting 喂,是我。 拨通开场白
maxCallMinutes 30 单次通话最长分钟数
ringTimeoutSeconds 60 呼叫超时秒数
toolEnabled true 注册 ChatLuna 原生工具
characterToolEnabled true 注册 Character 回复字段
toolName qq_phone 工具名
ttsProvider 空 默认 TTS provider;空则不合成语音
sttProvider 空 默认 STT provider;空则不识别语音
allowedUsers [] 允许使用电话命令/工具的 QQ 号;空表示不限制
allowedGuilds [] 允许使用电话命令的群号;空表示不限制
debug false 调试日志

ChatLuna 工具

启用后注册名为 qq_phone 的原生工具,参数:

{
  "action": "dial | answer | hangup | say | status",
  "user_id": "目标 QQ 号,dial 时可用",
  "call_id": "可选,指定通话",
  "text": "say 的文本",
  "greeting": "dial 的开场白"
}

工具只在私聊中授权(authorization: session => !session.guildId),避免在群里误触电话。

Character 回复字段

注册 qq_phone 数组字段,模型可以在 character_reply 中一次填写多个动作:

{
  "qq_phone": [
    { "action": "dial", "user_id": "10001" },
    { "action": "say", "text": "我到楼下了" }
  ]
}

字段由 Character 的 registerReplyToolField 注入;render 会生成 <qq_phone ... /> 供下游 XML 解析。

STT / TTS provider

其他 Koishi 插件可以这样接入:

ctx.on('ready', () => {
  ctx.qqPhone.registerTTS('my-tts', {
    async synthesize(text, { call }) {
      // 返回 Buffer、文件路径、URL,或 { buffer, format } / { file } / { url }
      return { buffer: await myTts(text), format: 'wav' }
    },
  })

  ctx.qqPhone.registerSTT('my-stt', {
    async transcribe(audio, { call }) {
      // audio 可能是 { buffer, format } 或 { file }
      return { text: await myStt(audio) }
    },
  })
})

然后在插件配置里设置 ttsProvider: 'my-tts'、sttProvider: 'my-stt'。

接口约定:

  • TTS:synthesize(text, options) -> string | Buffer | { buffer, format } | { file } | { url }
  • STT:transcribe(audio, options) -> string | { text }
  • registerTTS/registerSTT 返回 dispose 函数

simulated transport 会优先发送合成语音;没有 TTS 或合成失败时降级为文字提示。

Bridge 协议

当 transport: 'bridge' 时,插件向 bridgeUrl 发以下请求:

POST /calls
  { "to": "10001", "greeting": "喂,是我。" }
  -> { "id": "bridge-call-id" }

POST /calls/:id/answer
POST /calls/:id/hangup
  { "reason": "hangup" }

POST /calls/:id/audio
  { "text": "要说的文本", "audio": "<base64 或 URL,可选>", "format": "wav" }

请求头可带 Authorization: Bearer <bridgeToken>。

bridge 侧车需要自己完成:

  • 接 NapCat / QQ AVSDK 或系统音频桥
  • 把对方音频转成 STT 可处理的格式
  • 把 TTS 音频送回通话链路
  • 处理来电、挂断、超时

本插件不包含 AVSDK Hook、PulseAudio 配置或虚拟声卡配置。

安全

  • 电话是打扰性操作,建议设置 allowedUsers,只允许自己或信任用户。
  • 默认不限制 allowedUsers 是为了本地测试;公开机器人应配置白名单。
  • toolEnabled 的原生工具只对私聊授权;群聊中不会暴露。
  • bridge token 请使用独立密钥,不要复用其他服务 token。

限制

  • simulated 不是运营商电话,不会让手机响铃;它只是 QQ 语音消息会话。
  • bridge 需要外部侧车;没有侧车时拨号会失败。
  • 语音消息格式依赖协议端;NapCat 通常需要 SILK/AMR,插件只负责把 provider 返回的音频交给协议端,格式转换由 provider 或协议端完成。
  • 没有内置 STT/TTS;需要接入其他插件或 HTTP 服务。
  • 目前只支持私聊电话;群聊电话未实现。
  • 没有真正的“来电振铃”事件;simulated 由机器人主动发起,用户回复语音即视为通话中。

测试

npm test

测试覆盖:

  • PhoneService 拨号/接听/说话/挂断/STT
  • simulated transport 的文字降级与消息发送
  • ChatLuna 工具注册(mock @langchain/core/tools)
  • Character 回复字段注册、invoke 与 render

真实 QQ 通话需要协议端/侧车支持,无法在单元测试中覆盖。

License

MIT

About

QQ phone tool experiment: simulated voice calls and bridge protocol; real QQ/NapCat calls are not feasible.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages