/node-cq-websocket

A Node SDK for developing QQ chatbots based on WebSocket, which is depending on CoolQ and CQHTTP API plugin.

Primary LanguageJavaScriptMIT LicenseMIT

node-cq-websocket

npm npm license CQHttp Commitizen friendly semantic-release

分支

  • 主線
    • Build Status
    • Coverage Status
  • dev
    • Build Status
    • Coverage Status

關於 Node CQWebSocket SDK

依賴 CQHTTP API 插件的 websocket 接口, 為 NodeJs 開發者提供一個搭建 QQ 聊天機器人的框架。

關於 CQHTTP API 插件,見 richardchien/coolq-http-api

本 SDK 尚處於測試階段,使用上仍有機會碰到Bug,歡迎提交PR或issue回報。

由於付費問題,本 SDK 目前僅針對酷Q Air做測試。

功能特色

  • 輕鬆配置, 快速搭建 QQ 聊天機器人。
  • 自動維護底層連線, 開發者只需專注在聊天應用的開發。若斷線, 可依照配置重新連線
  • 支持消息監聽器內, 快速響應
  • 連線建立時, 可自動獲取機器人QQ號
  • 兼容 CQHTTP API 插件 v3.xv4.x 兩個大版本。

開發日誌

<點擊前往>

使用方式

CDN

如果你在網頁前端上使用,可以通過 CDN 引入。

<script src="https://cdn.jsdelivr.net/npm/cq-websocket/dist/cq-websocket.min.js"></script>

在你的 js 代碼中, 使用全局變數 CQWebSocket 獲取 SDK。

// 全局變數 CQWebSocket 存在於 window 對象下
const bot = new window.CQWebSocket()

NPM

如果你使用打包工具或 NodeJS,可以通過 NPM 安裝。

npm install cq-websocket

將 SDK 導入代碼

const CQWebSocket = require('cq-websocket')

該導入過程引用了一個類別進來,以下將以 CQWebSocket 作為該類別名稱進行說明,實際使用時請依自己的命名編寫。

關於 CQWebSocket 類別

SDK 的主要類別,底下封裝了兩個用於與 CQHTTP API 連線之 socket,分別為 /api/event (詳細功能描述可見 coolq-http-api/websocket)。

創建實例

new CQWebSocket(opt)

  • opt object
屬性 類型 默認值 說明
access_token string "" API 訪問 token 。見 CQHTTP API 之配置文件說明
enableAPI boolean true 啟用 /api 連線
enableEvent boolean true 啟用 /event 連線
protocol string "ws:" 協議名
host string "127.0.0.1" 酷Q伺服器 IP
port number 6700 酷Q伺服器端口
baseUrl string 6700 酷Q伺服器位址 (SDK在建立連線時會依照此設定加上前綴項 ws:// 及後綴項 `/<api
qq number | string -1 觸發 @me 事件用的QQ帳號,通常同登入酷Q之帳號,用在討論組消息及群消息中辨認是否有人at此帳號
reconnection boolean true 是否連線錯誤時自動重連
reconnectionAttempts number Infinity 連續連線失敗的次數不超過這個值
reconnectionDelay number 1000 重複連線的延遲時間, 單位: ms
fragmentOutgoingMessages boolean false 由於 CQHTTP API 插件的 websocket 服務器尚未支持 fragment, 故建議維持 false 禁用 fragment。
fragmentationThreshold number 0x4000 每個 frame 的最大容量, 默認為 16 KiB, 單位: byte
tlsOptions object {} 若需調用安全連線 https.request 時的選項
requestOptions {
timeout: number
}
{} 調用 API 方法時的全局默認選項。
  • 返回值: 一個新配置的 CQWebSocket 類別實例

設定 ws 伺服器位址時, 你可以從以下方式擇一配置。如果以下方式同時存在於配置中, 則採用其中編號最小的方式。

  1. 使用 baseUrl 項指定伺服器 URL。

baseUrl 未以協議名作為開頭, 則自動加上前綴 "ws://"

  1. 使用 protocol, host, port (皆為可選, 默認值見上表) 指定目標伺服器。

protocol 會自動轉為小寫, 且若 protocol 未以 ":" 結尾, 則自動加上後綴 ":"

自動獲取機器人QQ號

若機器人配置 enableAPI 為 true, 且沒有通過 qq 項配置機器人ID的話, 連線建立成功後會主動發送 API 請求向 CQHTTP API 取得酷Q正登錄的QQ號作為機器人QQ號。

此操作為異步操作, 在API響應之前, @me 事件均不會發布 。

除非真的有人QQ號是 -1, 哪尼口雷 Σ(*゚д゚ノ)ノ

自動重新連線說明

reconnection 設定為 true 啟用自動重連, 若發生網路錯誤, 例如無法連線到伺服器端, 連線建立失敗將會觸發重連, 若連續發生連線錯誤, 則重連次數不超過 reconnectionAttempts, 每次重連間隔 reconnectionDelay 毫秒。連續連線失敗將會在下一次連線成功時重新計數。

WebSocket 關閉之狀態碼

若呼叫 CQWebSocket #disconnect() 會對服務器端發送夾帶 1000 狀態碼的關閉訊息, 表示正常關閉, 無需重連。

若發生網路斷線、服務器重啟... 等意外斷線, 通常會獲得 1006 狀態碼, 此狀態表示 websocket 客戶端 (即機器人端) 觀察到服務器關閉。

建立連線

CQWebSocket #connect(wsType)

  • wsType WebsocketType
  • 返回值: this
  • 事件
    • socket.connecting 呼叫後立刻觸發,在任何連線嘗試之前。
    • socket.connect 連線成功。
    • socket.failed 連線失敗。
    • socket.error 連線失敗會一併觸發 error 事件。

socket.connectingsocket.connect 事件中帶有 attempts 參數, 可以用來對照哪次連線是否成功, attempts 的值表示連續 (N - 1) 次失敗後的第 N 次連線嘗試。

attempts 會在連線成功後歸零。

範例:

const CQWebSocket = require('cq-websocket')
const { WebsocketType } = CQWebSocket
const bot = new CQWebSocket()

// 手動連接兩個連線
bot.connect(WebsocketType.API)
bot.connect(WebsocketType.EVENT)

// 上面兩行 connect 代碼等同這一句
bot.connect()

bot.on('socket.connecting', function (wsType, attempts) {
  console.log('嘗試第 %d 次連線 _(:з」∠)_', attempts)
}).on('socket.connect', function (wsType, sock, attempts) {
  console.log('第 %d 次連線嘗試成功 ヽ(✿゚▽゚)ノ', attempts)
}).on('socket.failed', function (wsType, attempts) {
  console.log('第 %d 次連線嘗試失敗 。・゚・(つд`゚)・゚・', attempts)
})

斷開連線

CQWebSocket #disconnect(wsType)

  • wsType WebsocketType
  • 返回值: this
  • 事件
    • socket.close 連線斷開後。

重新連線

CQWebSocket #reconnect(delay, wsType)

  • delay number
  • wsType WebsocketType
  • 返回值: this
  • 事件

    此方法會先呼叫 disconnect() 等待 socket.close 事件觸發後再呼叫 connect(), 可以參考以上兩個方法的事件。

斷開現有連線, 並重新建立連線。

delay單位為 ms,表示socket.close事件觸發後的延遲時間, 延遲時間過後才會呼叫 connect()。

檢測連線

CQWebSocket #isSockConnected(wsType)

若未給定 wsType 則使方法會拋出錯誤。

連線就緒

CQWebSocket #isReady()

  • 返回值: boolean

檢查連線狀態是否就緒。

可透過 ready 事件監聽。

僅檢查已透過 enableAPIenableEvent 啟用之連線。

原 #isConnected() 方法。

方法調用

CQWebSocket(method, params, options)

  • method string
  • params object
  • options object | number
    • timeout number (默認: Infinity)
  • 返回值: Promise<ResObj>

CQWebSocket 的實例可直接作為方法調用,用於透過 /api 連線操作酷Q。
method 為欲調用的行為,透過 params 物件夾帶參數,詳細的規格請見 CQHTTP API 之 API 列表

返回值為一個 Promise 對象, 用作追蹤該次方法調用的結果。Promise 對象實現後第一個參數會拿到 ResObj 對象, 此為 CQHttp API 的回應對象

若有配置 timeout 選項(原先默認為 Infinity, 不會對請求計時), 則發生超時之後, 將放棄收取本次調用的結果, 並拋出一個 ApiTimeoutError

options 除了是一個對象外, 也可以直接給一個數值, 該數值會被直接當作 timeout 使用。

事件處理

事件處理應為機器人的運行過程中最主要的環節,情報收集主要是透過來自 /event 連線的事件上報,判讀事件文本並採取方法調用。

CQWebSocket #on(event_type, listener)

  • event_type string
  • listener function(...args){ }
    • ...args 依事件類型不同,監聽器的參數也有所不同,詳細對應見下表。
    • 返回值: string | Promise <string> | void
  • 返回值: this

註冊常駐監聽器。

若返回值為 string 或一個受理值 (resolved value) 為 string 之承諾 (Promise) 對象,則以該文字訊息作為響應發送。

CQWebSocket #once(event_type, listener)

  • event_type string
  • listener function(...args){ }
    • ...args 依事件類型不同,監聽器的參數也有所不同,詳細對應見下表。
    • 返回值: string | boolean | void
  • 返回值: this

註冊一次性監聽器。

當返回值為 boolean 且為 false ,指涉該監聽器並未完成任務,則保留該監聽器繼續聽取事件,不做移除。下一次事件發生時,該監聽器在調用後會再次以返回值判定去留。若返回值為 boolean 且為 true ,指涉該監聽器處理完畢,立即移除。

若返回值為 string ,則立即以該文字訊息作為響應發送,並移除該監聽器。

CQWebSocket #off(event_type, listener)

  • event_type string
  • listener function
  • 返回值: this

關於判定會被移除的監聽器, 以下條件, 第一個成立即適用。

  1. event_type 不為字串, 則移除所有監聽器, 並且安裝默認的 socket.error 監聽器
  2. listener 不為方法, 則移除所有 event_type 所指定事件下的監聽器, 若 event_type 指定的事件不存在則無事。
  3. 移除 event_type 下的 listener 監聽器(參照 reference 須相同!), 若 listener 不存在則無事。

基本事件

前三個基本事件之說明,可以另外參考 CQHTTP API 的數據上報格式

參數 context 可見事件列表

事件類型 監聽器參數 ...args 說明
message event CQEvent
context object
所有流入的訊息。
event context object [棄用] 群組人數變化...等QQ事件。(此事件不支援子事件, 若需要 notice 子事件, 請將 CQHTTP API 升級至 v4.x)
notice context object 群文件上傳, 群管變動, 群成員增減, 好友添加...等QQ事件。
request context object 好友請求, 群請求/群邀請...等QQ事件。
error err Error 應用層面的錯誤, 如 CQHttp API 消息格式錯誤, 響應超時... 等
ready this 設定中啟用之連線均成功並初始化完成,可以開始調用API (送消息...等操作)。

message 子事件

事件類型 監聽器參數 說明
message.private event CQEvent
context object
私聊消息。
message.discuss event CQEvent
context object
討論組消息。
message.discuss.@ event CQEvent
context object
tags CQAtTag[]
有人於討論組消息中被at。
message.discuss.@.me event CQEvent
context object
有人於討論組消息at機器人。
message.group event CQEvent
context object
群消息。
message.group.@ event CQEvent
context object
tags CQAtTag[]
有人於群消息中被at。
message.group.@.me event CQEvent
context object
有人於群消息at機器人。

message.discuss.@me, message.group.@me 已棄用。

notice 子事件

事件類型 監聽器參數 說明
notice.group_upload context object 群文件上傳。
notice.group_admin.set context object 設置管理員。
notice.group_admin.unset context object 取消管理員。
notice.group_decrease.leave context object 自主退群。
notice.group_decrease.kick context object 被動踢出群。
notice.group_decrease.kick_me context object 機器人被踢出群。
notice.group_increase.approve context object 管理員同意入群。
notice.group_increase.invite context object 管理員邀請入群。
notice.friend_add context object 新添加好友。

request 子事件

事件類型 監聽器參數 說明
request.friend context object 私聊消息。
request.group.add context object 加群請求。
request.group.invite context object 邀請入群。

socket 子事件

底層 socket 連線的事件, 可用於掌握連線狀況。

事件類型 監聽器參數 說明
socket.connecting type WebsocketType
attempts number
開始嘗試連線, 連線成功/失敗之前。
socket.connect type WebsocketType
socket WebSocketConnection
attempts number
連線成功後,尚未初始化之前。
socket.failed type WebsocketType
attempts number
連線失敗。
socket.reconnecting type WebsocketType
attempts number
開始嘗試重新連線, 若存在持續中的連線, 則先斷線。
socket.reconnect type WebsocketType
attempts number
重連成功。
socket.reconnect_failed type WebsocketType
attempts number
重連失敗。
socket.max_reconnect type WebsocketType
attempts number
已抵達重連次數上限。
socket.closing type WebsocketType 連線關閉之前。
socket.close type WebsocketType
code number
desc string
連線關閉。(連線關閉代碼 code 可參照 RFC 文件))
socket.error type WebsocketType
err Error
連線錯誤。

api 子事件

事件類型 監聽器參數 說明
api.send.pre apiRequest object 傳送 API 請求之前。關於 apiRequest 可見 /api/接口說明
api.send.post 傳送 API 請求之後。
api.response result object 對於 API 請求的響應。詳細格式見 /api/接口說明
此為集中處理所有 API 請求的響應, 若需對個別請求追蹤結果, 請參考方法調用中返回的 Promise 對象。
若需追蹤消息快速響應的結果, 請參考 響應結果追蹤

註: socketapi 並未擁有基本事件,在這邊僅作 namespace 用途與其他常用事件作區別。

事件傳播

事件具有向上傳播的機制,一個事件上報之後,該事件之所有親事件也會依序上報。關於事件親子關係的構成,可參考下方事件樹。(換而言之,在事件名稱上就是以 . 將親代串接子代)

舉個例子,群消息有人at某機器人,該機器人則會首先上報 message.group.@me 事件,該事件之親事件由下而上依序為 message.groupmessage ,則這兩個事件也會依照這個順序上報。

快速響應

message 及其子事件的監聽器第一個參數: CQEvent 類別的實例,在這個機制中扮演重要的角色。透過 CQEvent 實例,所有監聽器皆可在自己的運行期間調用 CQEvent #stopPropagation() 方法聲明自己的處理權,以截獲事件並阻斷後續監聽器的調用,並立即以該事件返回之文字訊息(或透過調用 CQEvent #setMessage(msg) 設定之文字訊息,也可以透過 Promise 對象 resolve 之文字訊息)作為響應,送回至 CQHTTP API 。

由於在一次事件傳播中的所有監聽器都會收到同一個 CQEvent 實例,因此對於響應的決定方式,除了 CQEvent #stopPropagation() 所提供的事件截獲機制之外,也可以採取協議式的方式,就是透過每個監聽器調用 CQEvent #getMessage() CQEvent #setMessage(msg) 協議出一個最終的響應訊息。

CQEvent 的方法描述,見 CQEvent

目前僅 message 及其子事件支援 CQEvent 相關機制。

響應結果追蹤

為了追蹤快速響應的結果(成功或失敗), 監聽器在調用上述之 CQEvent #stopPropagation() (原 #cancel()) 獲取響應的處理權之後, 同時還可通過 CQEvent #onResponse() 設置結果監聽器, 並透過 CQEvent #onError() 處理響應的錯誤。若沒有 CQEvent #onError() 進行錯誤處理, 發生響應錯誤時會觸發 error 事件

事件樹

├─ event (棄用) 
├─ notice
│    ├─ group_upload
│    ├─ group_admin
│    │    ├─ set
│    │    └─ unset
│    ├─ group_decrease
│    │    ├─ leave
│    │    ├─ kick
│    │    └─ kick_me
│    ├─ group_increase
│    │    ├─ approve
│    │    └─ invite
│    └─ friend_add
├─ request 
│    ├─ friend
│    └─ group
|         ├─ add
|         └─ invite
├─ error  
├─ ready  
├─ socket ※
│    ├─ connecting  
│    ├─ connect  
│    ├─ failed  
│    ├─ reconnecting  
│    ├─ reconnect  
│    ├─ reconnect_failed  
│    ├─ max_reconnect  
│    ├─ closing    
│    ├─ close    
│    └─ error
├─ api ※
│    ├─ response  
│    └─ send ※
│        ├─ pre    
│        └─ post  
└─ message
     ├─ private
     ├─ discuss
     │    └─ @me
     └─ group
          └─ @me

※: 表示無法在該節點進行監聽

socket.error 默認監聽器

由於 socket.error 屬於連線失誤的事件,如果沒有適當的監聽器配套措施,會造成無防備的狀況下無法順利連線,徒增猿們除蟲困擾。

為此而產生了 socket.error 事件之默認監聽器,當開發者沒有主動監聽 socket.error 事件,則會使用默認監聽器,發生錯誤時會將收到的錯誤實例拋出,而該錯誤實例下有一個 which 字段(內容為 string 類型且必為 /api /event 兩者任一)指出是哪一個連線出了問題。

默認監聽器除了拋出錯誤外, 還會在 stderr 輸出以下警示訊息:

You should listen on "socket.error" yourself to avoid those unhandled promise warnings.

該錯誤可透過在 process 上監聽 unhandledRejection 事件取得。如下所示:

process.on('unhandledRejection', function(err){
  switch(err.which){
    case CQWebSocket.WebsocketType.API:
      // 錯誤處理
      break
    case CQWebSocket.WebsocketType.EVENT:
      // 錯誤處理
      break
  }
})

// CQWebSocket.WebsocketType 下提供兩個常量對應分別至 /api 及 /event

CQEvent 類別

CQEvent #isCanceled()

  • 返回值: boolean

棄用中, 無替代

CQEvent #cancel()

CQEvent #stopPropagation()

  • 返回值: void

截獲事件並停止事件傳播

#cancel() 棄用中, 更名為 #stopPropagation()

CQEvent #getMessage()

  • 返回值: string

取得目前的響應訊息。

CQEvent #setMessage(msg)

  • msg string
  • 返回值: void

設置響應訊息。

CQEvent #appendMessage(msg)

  • msg string
  • 返回值: void

串接響應訊息。

CQEvent #hasMessage()

  • 返回值: boolean

是否有響應訊息。

CQEvent #onResponse(handler, options)

  • handler (res: object) => void
  • options object (同方法調用之 options)

設置響應結果的處理器, 用以追蹤訊息是否傳送成功。

CQEvent #onError(handler)

  • handler (err: ApiTimeoutError) => void

設置錯誤處理器, 可能的錯誤已知有響應超時。

CQWebSocket.WebsocketType 實例

下有兩個常量對應至 /api/event

WebsocketType.API

= "/api" string

WebsocketType.EVENT

= "/event" string

CQTag 類別

作為所有CQ碼的親類別。

new CQTag(type, meta)

  • type string
  • meta object

舉個例子, [CQ:at,qq=123] 這個 tag 等同

new CQTag('at', { qq: '123' })

CQTag #equals(another))

  • another CQTag | string

比較是否為同一個 Tag, 採用兩邊呼叫 toString() 後的結果比較。

another 為一個 string, 則會先將之解析為 CQTag。

new CQTag('at', { qq: '123' }).equals('[CQ:at,qq=123]') // true

CQTag #toString()

  • 返回值: string
new CQTag('at', { qq: '123' }).toString() // [CQ:at,qq=123]

CQAtTag 類別

繼承自 CQTag 類別。

new CQAtTag(qq)

  • qq string|number

CQAtTag #getQQ()

  • 返回值: number
new CQAtTag('123').getQQ() // 123

範例

基本創建一個複讀機器人的代碼範例如下(可參見demo/echo-bot.js):

const CQWebSocket = require('cq-websocket')

// 採用默認參數創建機器人實例
let bot = new CQWebSocket()

// 設定訊息監聽
bot.on('message', (e, context) => {
  // 若要追蹤訊息發送狀況, 須獲取事件處理權, 並使用下面2或3的方式響應訊息
  e.stopPropagation()
  // 監聽訊息發送成功與否
  e.onResponse(console.log)
  // 監聽訊息發送超時與否
  e.onError(console.error)

  // 以下提供三種方式將原訊息以原路送回

  // 1. 調用 CQHTTP API 之 send_msg 方法
  //   (這就是一般的API方法調用, 直接在該方法的返回值之Promise追蹤結果)
  // bot('send_msg', context)
  //   .then(console.log)
  //   .catch(console.error)

  // 2. 或者透過返回值快速響應
  // return context.message

  // 3. 或者透過CQEvent實例,先獲取事件處理權再設置響應訊息
  // e.stopPropagation()
  // e.setMessage(context.message)
})

bot.connect()

SDK 開發環境

下載源碼

git clone https://github.com/momocow/node-cq-websocket.git

安裝依賴

npm install

單元測試

npm test

採用 ava 框架執行測試。

打包 CQWebSocket 至 browser 環境

npm run build

使用 webpack 將 SDK 及所有依賴打包, 並在 /dist目錄下產生一個 cq-websocket.min.js

建置 demo/webpack

npm run build-demo

打包 /demo/webpack/app.js 內容, 在 /demo/webpack/www 目錄下產生一個 bundle.js

Known Issues

  • CQHTTP API 插件尚未支援收發 Fragmant, 暫時禁用
  • 在 Node 10.x 下, Buffer 寫入時的 RangeError (發生在 SDK 調用 API 方法時)。

    這是 Node 的問題, 暫時使用 Node 8.x 以下就沒問題。

RangeError [ERR_OUT_OF_RANGE]: The value of "value" is out of range. It must be >= 0 and <= 4294967295. Received -805456141
    at checkInt (internal/buffer.js:35:11)
    at writeU_Int32BE (internal/buffer.js:625:3)
    at Buffer.writeUInt32BE (internal/buffer.js:638:10)
    at WebSocketFrame.toBuffer (/***/node-cq-websocket/node_modules/websocket/lib/WebSocketFrame.js:257:24)
    at WebSocketConnection.sendFrame (/***/node-cq-websocket/node_modules/websocket/lib/WebSocketConnection.js:857:43)
    at WebSocketConnection.fragmentAndSend (/***/node-cq-websocket/node_modules/websocket/lib/WebSocketConnection.js:793:14)
    at WebSocketConnection.sendUTF (/***/node-cq-websocket/node_modules/websocket/lib/WebSocketConnection.js:733:10)
    at W3CWebSocket.send (/***/node-cq-websocket/node_modules/websocket/lib/W3CWebSocket.js:116:26)

歡迎餵食 ☕

請勿拍打 🤜 無限期掙飯中 🍙

Buy Me A Coffee