js-bot聊天機(jī)器人框架及開發(fā)工具
js-bot 是一個(gè)基于 酷 Q Websocket 服務(wù)(CoolQ HTTP API 插件)的瀏覽器端聊天機(jī)器人框架及開發(fā)工具,用 Typescript + React 開發(fā),你可以在“聊天模式”下與好友/群聊天,也可以在其 “控制臺(tái)模式” 下輸入 Javascript 代碼運(yùn)行或調(diào)用 js-bot 及 coolq-http 提供的 api ,并注冊(cè)消息等事件的響應(yīng)函數(shù)來實(shí)現(xiàn)自己的機(jī)器人,還可以在“虛擬聊天模式”下用虛擬賬號(hào)向機(jī)器人發(fā)送消息來測(cè)試自己編寫的機(jī)器人,所有這一切都可以在 https://pandolia.net/js-bot 這一個(gè)網(wǎng)頁上進(jìn)行。
js-bot 控制臺(tái)模式效果如下:
二、系統(tǒng)需求
- Chrome 或 Firefox 瀏覽器
- 酷 Q 及 CoolQ HTTP API 插件
用酷 Q 登錄賬號(hào)后,啟用 cqhttp 插件,之后退出酷 Q ,找到 “data\app\io.github.richardchien.coolqhttpapi\config” 下相應(yīng)賬號(hào)的配置文件,將 websocket 相關(guān)的配置修改如下:
{
"use_ws": true,
"ws_host": "127.0.0.1",
"ws_port": 6700,
"access_token": "mytoken",
}
再次登錄,等酷 Q Websocket 服務(wù)啟動(dòng)后,用瀏覽器打開 https://pandolia.net/js-bot 網(wǎng)址,就可以使用 js-bot 盡情的玩耍了,此時(shí)用其他賬號(hào)給本賬號(hào)發(fā)送 "-joke",本賬號(hào)會(huì)自動(dòng)回復(fù)一則笑話。
如果酷 Q Websocket 服務(wù)是在其他機(jī)器上部署的,可以在 url 參數(shù)中指定其地址及 token ,例如:https://pandolia.net/js-bot/?ws_host=192.168.111.111:6700&token=mytoken 。
對(duì)于 IE/Edge ,由于瀏覽器默認(rèn)禁止 Javascript 代碼連接不同主機(jī)的 WebSocket 服務(wù),解決方案為:Internet選項(xiàng) -> 安全 -> 本地Internet -> 站點(diǎn),把所有勾選取消?;蛘呦螺d本項(xiàng)目代碼,build 之后將 html 文件部署在本地再訪問。
如果不開啟酷 Q Websocket 服務(wù), js-bot 的 “控制臺(tái)模式” 和 “虛擬聊天” 模式仍然是可以使用的,但與 QQ 相關(guān)的功能全都不可用。
三、控制臺(tái)模式
控制臺(tái)模式下,可以直接在輸入框輸入 Javascript 代碼運(yùn)行,例如:
// 打印文本
>>> print('hello')
hello
// 查找賬號(hào)為 3497303033 的好友
>>> buddies.get('3497303033')
[ 好友 feng,3497303033 ]
// ans 中保存上一次命令的運(yùn)行結(jié)果
>>> ans
[ 好友 feng,3497303033 ]
// 向好友發(fā)送消息 "hello"
>>> ans.send('hello')
null
// 調(diào)用 ai.joke() 生成一個(gè)笑話
// 注意: ai.joke() 返回的是一個(gè) Promise 對(duì)象,js-bot 解釋器會(huì)等待其 fullfilled ,將結(jié)果保存到 ans 中再返回
>>> ai.joke()
中午去存錢,排隊(duì)時(shí)一美女在后面問我:“存錢是嗎?”“恩!”“我正好要取錢,反正你要存,不如把錢給我,咋倆就不用排隊(duì)了?!?
我想想覺得有理,于是把錢給她了!
// 向好友發(fā)送笑話
>>> buddies.get('3497303033').send(ans)
null
控制臺(tái)模式下,可使用 js-bot 提供的內(nèi)部變量和方法:
- buddies: 好友列表,ContactTable 對(duì)象,具有 type/name/length 屬性及 get/map/forEach/filter/find 方法
- groups: 群列表,ContactTable 對(duì)象
- ans: 上一次命令的運(yùn)行結(jié)果
- print/clr/debug/info/warn/error: 打印、清屏及日志方法
- showModal/popMoal: 顯示信息框,例如: showModal("xxx")
- BUDDY/GROUP/NOTYPE/CONSOLE/MYSELF/VIRTUAL_BUDDY: 常量,表示聯(lián)系人的類型
- handler.onMessage/handler.onCqEvent: 消息事件及其他事件的響應(yīng)函數(shù),可以改寫這兩個(gè)屬性
buddies/groups 中保存的是 Contact 對(duì)象,具有 type/qq/name 屬性和 send 方法,type 為 BUDDY 表示好友,GROUP 表示群,另外 VIRTUAL_BUDDY 表示虛擬好友。
在控制臺(tái)模式下運(yùn)行代碼,與在瀏覽器自帶的開發(fā)者工具的 Devtool-Console 中運(yùn)行代碼,有兩點(diǎn)不一樣:
- 1) 以上所述的 js-bot 提供的變量,在 Devtool-Console 中是不可見的,需要加 cq. 才能訪問,例如: cq.buddies
- 2) 控制臺(tái)模式下,如果命令返回的是一個(gè) Promise 對(duì)象,js-bot 解釋器會(huì)等待其 fullfilled 再返回,而 Devtool-Console 則不會(huì),例如,對(duì)于 ai.joke() ,在 Devtool-Console 下如果需要打印其返回結(jié)果,則應(yīng)這樣調(diào)用: cq.ai.joke().then(function (t) { console.log(t) })
對(duì)于第二點(diǎn),要注意的是,即便是在控制臺(tái)模式下,也不能將 ai.joke() 的結(jié)果直接傳遞給 send 方法,而應(yīng)該這樣調(diào)用: ai.joke().then(function (t) { buddies.get('3497303033').send(t) })
可以在控制臺(tái)模式下對(duì) handler.onMessage 和 handler.onCqEvent 進(jìn)行重新賦值,從而實(shí)現(xiàn)自己的聊天機(jī)器人,例如:
>>>
handler.onMessage = function (contact, message) {
if (message.content === '-hello') {
contact.send('你好,' + contact.name)
.then(function() {
popModal('發(fā)消息成功');
});
}
}
在控制臺(tái)中運(yùn)行以上代碼后,當(dāng) 本賬號(hào) 收到內(nèi)容為 "-hello" 的消息時(shí),會(huì)自動(dòng)回復(fù): "你好,xx" 。
onMessage 函數(shù)中:第一個(gè)參數(shù) contact 是一個(gè) Contact 對(duì)象 代表此消息的發(fā)送方,可以調(diào)用其 send 方法向其回復(fù)消息;第二個(gè)參數(shù) message 是一個(gè) IMessage 對(duì)象,代表消息體,具有 id、direction、from 和 content 屬性,其中 content 為消息內(nèi)容。
四、虛擬聊天模式
為了測(cè)試自己開發(fā)的機(jī)器人程序,需要利用其它賬號(hào)向本賬號(hào)發(fā)送消息,這顯然很不方便,因此 js-bot 提供 虛擬聊天模式 來快速測(cè)試。
在 js-bot 中的 最近 聯(lián)系人列表內(nèi),點(diǎn)擊第二個(gè)聯(lián)系人 [ yourname ] ,就進(jìn)入了虛擬聊天模式,此時(shí),用戶扮演 虛擬好友 向本賬號(hào)發(fā)送消息。在此模式下輸入文本并發(fā)送時(shí),機(jī)器人會(huì)收到一條來自 虛擬好友 的消息, handler.onMessage 同樣會(huì)被調(diào)用,此時(shí),contact 的 type 屬性為 cq.VIRTUAL_BUDDY , name 屬性為 "虛擬好友" 。
例如,對(duì)于上一節(jié)的 onMessage 函數(shù),在 虛擬聊天 模式下發(fā)送 "-hello" ,則機(jī)器人會(huì)自動(dòng)回復(fù) "你好,虛擬好友" 。
五、普通聊天模式
如果 js-bot 已連接了酷 Q 的 Websocket 服務(wù),那么 js-bot 的普通聊天模式是可用的,可以在頁面上點(diǎn)擊好友和群,然后進(jìn)行聊天。
當(dāng)進(jìn)入到普通聊天模式時(shí), js-bot 會(huì)將頁面上的輸入框上方的模式信息文本顏色調(diào)得更加明顯,提醒用戶已進(jìn)入聊天模式,避免發(fā)送無關(guān)的信息。
如果沒有開啟酷 Q 的 Websocket 服務(wù),普通聊天模式無法使用,但控制臺(tái)模式和虛擬聊天模式仍然是可用的。
六、開發(fā)模式
本項(xiàng)目采用 Typescript + React 開發(fā),可以下載本項(xiàng)目源碼,運(yùn)行 npm install 和 npm start 啟動(dòng)本項(xiàng)目,并按自己的需要進(jìn)行開發(fā)和擴(kuò)展。建議采用 Vs Code (需要安裝 Eslint 和 Tslint 插件)。
開發(fā)和擴(kuò)展 js-bot 時(shí),修改 src/myhandler-ts.ts 文件就可以了,在此文件中導(dǎo)出兩個(gè)事件函數(shù):
import Contact from './cq/Contact';
import cq from './cq';
export default {
onMessage: async (contact: Contact, message: IMessage) => {
if (message.content !== '-joke') {
return;
}
const joke = await cq.ai.joke();
await contact.send(joke);
cq.popModal('發(fā)送笑話成功.');
},
onCqEvent: async (data: any) => {
return;
},
};
如果不會(huì) Typescript ,也可以用 Javascript 開發(fā),修改 src/myhandler-js.js 文件就可以了,需要在 src/index.tsx 文件中改為:import handler from './myhandler.js' 。
本項(xiàng)目中的其他文件,建議不要修改,如果確實(shí)需要修改,請(qǐng)?jiān)?nbsp;項(xiàng)目 github 主頁 上發(fā) issue 或 pull-request 。
好友消息和群消息之外的其他事件會(huì)被傳遞給 onCqEvent 函數(shù),支持的事件列表及各事件的字段說明詳見 cqhttp 事件列表 。
在事件函數(shù)中,除了發(fā)送消息,也可以用 cq.api 方法調(diào)用 cqhttp 提供的 api (例如:發(fā)送好友贊等),示例如下:
await cq.api('send_like', { user_id: 158297369 });
其他 cqhttp-api 見 cqhttp API 列表 ,調(diào)用時(shí)需注意下面兩個(gè)問題:
- 第一個(gè)參數(shù)為 cqhttp-api 名稱,前面不含斜杠 "/"
- 第二個(gè)參數(shù)為 cqhttp-api 參數(shù),與用戶 id 相關(guān)的字段全部采用 number 類型(js-bot 內(nèi)部采用的是 string 類型)
七、 js-bot API
以下列出 js-bot 內(nèi)部可使用的常量、變量和方法,禁止直接修改變量,只能通過調(diào)用方法來改變 js-bot 的內(nèi)部狀態(tài)。
1. 常量(src/cq/CqStore.tsx)
// 聯(lián)系人類型: 好友/群/無/控制臺(tái)/自己/虛擬好友 export const BUDDY = 0; export const GROUP = 1; export const NOTYPE = 2; export const CONSOLE = 3; export const MYSELF = 4; export const VIRTUAL_BUDDY = 5; // 消息方向,LEFT 代表消息畫在左邊,RIGHT 代表消息畫在右邊 export const LEFT = 0; export const RIGHT = 1; // 日志級(jí)別 export const DEBUG = 0; export const INFO = 1; export const WARN = 2; export const ERROR = 3; // 每個(gè)聯(lián)系人保存的消息總數(shù)最大值 export const MAX_MESSAGES_SIZE = 400; // 環(huán)境(在 .env 文件內(nèi)定義),項(xiàng)目名稱, CQ-WEBSOCKET 參數(shù), github 地址 export const PROJECT_NAME: string; export const DEFAULT_WS_HOST: string; export const DEFAULT_TOKEN: string; export const DEFAULT_RECENTS: string; export const GITHUB_URL: string;
2. 類型及接口(src/types.d.ts)
// 消息方向 LEFT/RIGHT
type DirectionType = 0 | 1;
// 消息接口( onMessage 的第二個(gè)參數(shù)為 IMessage 對(duì)象)
interface IMessage {
// 消息 id
readonly id: string;
// 消息方向
readonly direction: DirectionType;
// 消息發(fā)送方名稱
readonly from: string;
// 消息內(nèi)容
readonly content: string;
}
// 聯(lián)系人類型 BUDDY ~ VIRTUAL_BUDDY
type ContactType = 0 | 1 | 2 | 3 | 4 | 5;
// 日志級(jí)別 DEBUG ~ ERROR
type LogLevel = 0 | 1 | 2 | 3;
// 事件處理接口
interface IHandler {
onMessage: (c: Contact, m: Message) => any,
onCqEvent: (data: any) => any,
}
3. Contact 類(src/cq/Contact.tsx)
class Contact {
// 類型 BUDDY/GROUP/VIRTUAL_BUDDY ,代表 好友/群/虛擬好友
type: ContactType;
// 賬號(hào)
qq: string;
// 名稱
name: string;
// 向本聯(lián)系人發(fā)送消息,發(fā)送成功返回 null ,發(fā)送失敗則拋出 Error 錯(cuò)誤
send = async (text: string): Promise<null> => { /* */ }
}
4. ContactTable 類(src/cq/ContactTable)
class ContactTable {
// 類型 BUDDY/GROUP/NOTYPE 代表 好友列表/群列表/最近聯(lián)系人列表
type: ContactType;
// 名稱
get name() { /* */ }
// 分別以數(shù)組和字典保存所有聯(lián)系人,請(qǐng)勿訪問這兩個(gè)屬性
_list: Contact[] = [];
_dict: Map<string, Contact> = new Map();
// 聯(lián)系人個(gè)數(shù)
get length() { return this._list.length; }
// 遍歷、查找聯(lián)系人
map = this._list.map.bind(this._list);
forEach = this._list.forEach.bind(this._list);
filter = this._list.filter.bind(this._list);
find = this._list.find.bind(this._list);
// 查詢聯(lián)系人
// get('3497303033') 返回 qq 為 '3497303033' 的 Contact 對(duì)象
// get(0) 返回第一個(gè) Contact 對(duì)象
// 對(duì)象不存在時(shí)返回 undefined
get(qqOrIndex: string | number): Contact | undefined { /* */ }
}
5. 全局 cq 對(duì)象(src/cq/CqStore.tsx)
// 好友列表/群列表/最近聯(lián)系人列表
export const buddies = new ContactTable(BUDDY);
export const groups = new ContactTable(GROUP);
export const recents = new ContactTable(NOTYPE);
// 事件處理對(duì)象
export let handler: IHandler;
// 控制臺(tái)上次命令運(yùn)行結(jié)果
export let ans;
// 打印、清屏
export function print(line = '') { /* */ }
export function clr() { /* */ }
// 日志
export let level: LogLevel;
export function setLogLevel(_level: LogLevel) { /* */ }
export function debug(_level: LogLevel, msg: any) { /* */ }
export function info(_level: LogLevel, msg: any) { /* */ }
export function warn(_level: LogLevel, msg: any) { /* */ }
export function error(_level: LogLevel, msg: any) { /* */ }
// 模態(tài)對(duì)話框
export async function showModal(msg: any) { /* */ } // 展示
export function closeModal() { /* */ } // 關(guān)閉
export function popModal(msg: any, t = 2500) { /* */ } // 展示,t 毫秒后關(guān)閉
// cqhttp 服務(wù)地址及 token
export const ws_host: string;
export const token: string;
// ai (見 src/ai/index.tsx 目前只有 ai.joke )
export const ai;
// api
export function api(action: string, params: any = null): Promise<any> { /* */ }
// 退出并重啟 js-bot
export function abort(msg: string) { /* */ }
// 重置 cqhttp 服務(wù)地址
export function reset(w = DEFAULT_WS_HOST, t = DEFAULT_TOKEN, r = DEFAULT_RECENTS) { /* */ }
