UNPKG

lemon-bot

Version:
531 lines (344 loc) 23.7 kB
<h1>🍋 Lemon-Bot</h1> 一个基于酷Q和CoolQ HTTP API插件的QQ机器人Nodejs开发框架。 - 支持多命令匹配、命令自定义解析 - 使用修饰器进行灵活的命令触发控制 - 支持会话上下文功能 - 支持多机器人运行 - and more ... ## 前言 该项目仍处于早期开发版,故版本变动较为频繁,但会尽可能保证基本开发方式不变,具体变动见 [Changelog](https://github.com/XHMM/lemon-bot/blob/master/CHANGELOG.md) ## 准备 1. 安装 [nodejs](https://nodejs.org/en/download/) (该框架基于v10.16.3版本进行开发与测试) 2. 安装 酷Q 和 HTTP插件: - Windows: 首先前往酷Q的[版本发布](https://cqp.cc/b/news)页面下载(Air为免费版,Pro为收费版),下载后解压启动 `CAQ.exe``CQP.exe` 并登陆你的QQ机器人账号。然后根据[CoolQ HTTP API插件文档](https://cqhttp.cc/docs/)中的"手动安装"部分的教程进行插件安装。 - Linux / Mac: 查看[CoolQ HTTP API插件文档](https://cqhttp.cc/docs/)中的"使用Docker"部分的教程进行安装 3. 修改HTTP插件的配置文件: 每个账号的配置文件存放路径一般为 `/path/to/酷Q/data/app/io.github.richardchien.coolqhttpapi/config/QQ号.json` (也可能是 `.ini` 格式)。下面以 `.json` 格式来说明在使用该框架时必须要进行修改的配置项(其他配置说明可见[插件文档](https://cqhttp.cc/docs/#/Configuration?id=配置项)): ```metadata json { "port": 5700, // HTTP插件的运行端口,请自行指定 "use_http": true, // 须设为true "post_url": "http://127.0.0.1:8888/coolq", // 这是node服务器的运行地址以及监听的路由,你只可以修改端口号,请勿修改路由地址 "access_token": "", // 可选。若指定此值,则使用框架时也须配置 "secret": "", // 可选。若指定此值,则使用框架时也须配置 "post_message_format": "array" // 请将该选项务必设为array } ``` 4. 安装该node模块: `npm i lemon-bot` 5. 由于该框架使用了 [decorator](https://www.typescriptlang.org/docs/handbook/decorators.html) 语法: - 若你是使用 Javascript 进行开发,则需要[配置babel](https://babeljs.io/docs/en/babel-plugin-proposal-decorators)以支持该特性。 - 若是使用 Typescript,则需要在`tsconfig.json`中启用decorator: ```metadata json { "compilerOptions": { // ... "experimentalDecorators": true } } ``` 6. 该框架使用 `debug` 模块进行日志打印。在开发阶段建议开启日志输出来方便调试和排错:设置环境变量 `DEBUG=lemon-bot*` ,然后运行主程序即可。 ## Demo`index.ts` 文件里写入下述代码: ```js import { Command, RobotFactory, HttpPlugin } from "lemon-bot"; class SimpleCommand extends Command { // 当机器人接收到"测试"或是"test"文本后,会触发该命令 directive() { return ["测试", "test"]; } // 当消息是私发给机器人时,会使用user函数进行响应 user({ fromUser }) { return "你好呀" + fromUser; } // 当机器人在QQ群内并检测到上述指令后,会使用group函数进行响应 group({ fromUser, fromGroup }) { // 返回值为数组时,机器人会连续发送多条消息。 return ["触发群是" + fromGroup, "触发用户是" + fromUser]; } } const robot = RobotFactory.create({ port: 8888, // node应用的运行端口。需要和插件配置文件的post_url对应 robot: 1326099664, // 机器人QQ号 httpPlugin: new HttpPlugin("http://localhost:5700"), // 用于调用HTTP Plugin API commands: [new SimpleCommand()] // 该机器人可处理的命令 }); robot.start(); // 启动 ``` 在运行该代码前,请确保: - 酷Q和HTTP插件处于运行状态,且上述代码中的 `robot` 值为当前登录的机器人QQ - 安装要求修改了HTTP插件的配置文件 然后在命令行内输入 `npx ts-node index.ts` 即可启动机器人。 一对一聊天测试:用一个加了机器人为好友的QQ号向机器人发送 "测试""test" ,会发现机器人返回了 "你好呀[你的QQ号]"。 群聊测试:将该机器人拉入群内,然后在群内发送"测试""test",会发现机器人连续返回了两条消息: "触发群是[机器人所在Q群]""触发用户是[你的QQ号]"## 案例 - [小心机器人](https://github.com/XHMM/bot-xiaoxin) ## API文档 Tips:下述涉及的类型定义和enum定义可直接前往源码内查看,可帮助更好的理解其含义。 ### Class RobotFactory 该类用于机器人的创建。 #### static create(config: CreateParams): CreateReturn `CreateParams`:该函数所需参数是一个对象,接受如下属性: | key | type | description | optional | | ---------- | ---------- | ------------------------------------------------------ | -------- | | port | number | node服务器的运行端口 | | | robot | number | 机器人QQ号 | | | httpPlugin | HttpPlugin | HTTP插件实例 | | | commands | Command[] | 需要注册的命令 | | | session | Session | 传入该参数运行使用session函数 | optional | | secret | string | 须和HTTP插件配置文件值保持一致,用于对上报数据进行验证 | optional | | context | any | 该属性会作为Command继承类的成员属性,默认值为null | optional | `CreateReturn`:该函数的返回值是一个对象,包含如下属性 | key | type | description | | ----- | ----------------- | ----------- | | start | ()=>Promise<void> | 启动机器人 | | stop | () => void | 停止机器人 | Example: ```js const robot = RobotFactory.create({ port: 8888, robot: 1326099664, httpPlugin: new HttpPlugin("http://localhost:5700"), commands: [new SimpleCommand()] }); robot.start(); ``` ### Class Command 该类需要被继承使用,用来创建命令。下面将以继承类的角度进行描述: #### 继承类的基本结构: ```typescript // 导入基类 import { Command} from 'lemon-bot'; // 导入ts类型定义提升开发体验 import { ParseParams, ParseReturn, UserHandlerParams, GroupHandlerParams, SessionHandlerParams, HandlerReturn } from 'lemon-bot' class MyCommand extends Command<C> { context: C; httpPlugin; // 下面的[directive函数]和[parse函数]必须至少提供一个 directive(): string[] parse(params: ParseParams): ParseReturn // 下面的三种函数必须至少提供一个 user(params: UserHandlerParams): HandlerReturn group(params: GroupHandlerParams): HandlerReturn both(params: BothHandlerParams): HandlerReturn // 下面的函数都是以session开头,叫做[session函数],可提供任意多个,详见下方文档描述 sessionA(params: SessionParams): HandlerReturn sessionB(params: SessionParams): HandlerReturn } ``` ##### context属性 该属性的值等同于使用 `RobotFactory.create` 时传给 `context `参数的内容,默认为null。 ##### httpPlugin属性 该属性的值为使用 `RobotFactory.create` 时传给 `httpPlugin` 参数的内容。 ##### **directive**函数 该函数应返回一个字符串数组。假如它返回了 `["天气", "weather"]` ,并且**没有**定义 `parse函数` 时,当接收到用户消息后,会判断消息内容是否等于"天气"或者"weather",若相等,则会执行 `user函数``group函数``both函数` ,若不相等,则会进行下一个命令的判断。(该函数的触发条件同样会受到下述`trigger`修饰器的约束) ##### parse函数 上述的 `directive函数` 无法实现自定义命令解析,比如想要获取 "天气 西安" 这一消息中的城市信息,则需要使用 `parse函数` 手动处理,该函数的返回值信息可在 `user函数``group函数``both函数` 的参数中访问。**提醒:**若提供了该函数,则不会再使用 `directive` 函数进行命令处理。 `directive函数``parse函数` 是允许同时存在的,并且十分建议不要省略 `directive函数` 的声明,因为通过该函数的返回值内容可以提升代码阅读性,方便识别该命令的用途。 ##### user函数 提供该函数表示当前命令支持用户和机器人私聊的场景。 ##### group函数 提供该函数表示当前命令支持群组聊天下的场景。**提醒** :群组消息包括了 匿名 和 非匿名 两种情况,故该函数参数的 `fromUser` 字段可能为QQ号,也可能为一个对象描述了匿名用户信息。可使用该函数参数中的 `messageFromType` 判断。 ##### both函数 当你的命令处理逻辑在私聊和群聊下比较相似时,若同时提供 `user函数``group函数` 会增加代码冗余,故该 `both函数` 就是用来同时处理来自私聊或群聊的消息。**提醒:**若提供了该函数,则 `user函数``group函数` 会无效。此外,你需要手动判断消息源是用户消息、群组非匿名消息还是群组匿名消息,可使用函数参数的`messageFromType` 字段来判断。 ##### session函数 该函数的功能参见下方[session函数](#class-session)的描述。 #### 函数参数说明: 上述函数中,`directive函数` 是无参的,其余类型的函数参数都是一个对象,该对象的属性内容如下(scope列描述了该属性存在于哪些函数,all表示每个函数都有该参数): | key | type | description | scope | | --------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------- | | data | any | 值为 `parse` 函数的返回值 | user,group,both | | message | Message[] | 以二维数组的形式表示的消息内容 | all | | rawMessage | string | 以字符串形式表示的消息内容 | all | | requestBody | any | 原始http请求的body数据,具体内容可查看HTTP插件文档 | all | | fromUser | number\| AnonymousUser | 发送消息者的QQ,为非数字时代表是匿名用户 | all | | fromGroup | number\|undefined | 发送消息者所在的Q群,`user函数`中的该值为 `undefined` | all | | robot | number | 执行该指令的机器人 | all | | isAt | boolean | 是否艾特了机器人 | group | | messageFromType | enum MessageFromType | 消息来自方:group指群聊, anonymous指群内匿名聊, user指私聊 | user,group,both | | setEnd | () => Promise<void> | 是个异步函数,用来设置会话上下文结束 | session | | historyMessage | Record<string, Array<Message[]>> | 一个对象,保存了历史会话消息,key为在 `group函数``user函数` 内调用`setNext`时指定的参数名称 (含‘session’单词前缀) | session | | setNext | (sessionName: string, expireSeconds?: number) => Promise<void> | 是个异步函数,用来设置下一个需要执行的`session函数` | user,group,both,session | #### 函数返回值: ##### parse函数 - 无返回值或是返回了 `undefined`:表示用户消息不满足该命令的处理条件 - 其他任意类型的值:该值在 `user函数``group函数``both函数` 参数的 `data` 属性中访问到 ##### user函数、group函数、session函数 - 无返回值或是返回了`undefined`:表示处理完毕,但不返回任何消息 - `{ atSender:boolean, content: string }`:为一个对象时,`atSender` 表示是否艾特发送者(仅群聊有效),`content` 为响应内容 - `string[]`:表示连续响应多条消息 - `string`:表示响应一条不艾特发送者的消息 #### 装饰器: ##### include( number[] ) 可用于 `group函数``user函数`,表示只有这些QQ群/QQ号才可以触发该命令: ```js @include([ 12312, 21223 ]) user() {} //只有QQ为为它俩的用户可触发该命令 @include([ 3423344 ]) group() {} // 只有QQ群号为它的群可触发该命令 ``` ##### exclude( number[] ) 可用于 `group函数``user函数`,表示这些QQ群/QQ号不能触发该命令。**同时使用`include``exclude`会报错。** ```js @exclude([ 12312, 21223 ]) user() {} // 除了上述两位QQ用户不能触发该命令,其他用户可触发 @include([ 3333 ]) group() {} // 只有群号为3333的群可触发该命令 ``` ##### trigger( triggerType ) 可用于 `group函数``user函数`,表示命令触发方式,可赋值为: - `TriggerType.at` :用户必须艾特机器人并发送消息方可触发命令 - `TriggerType.noAt`:用户之间在群内发送消息可触发命令,艾特机器人即使命令正确也不会触发 - `TriggerType.both` (默认值):艾特或者不艾特机器人都可触发命令 ```js @trigger(TriggerType.noAt) group() {} ``` ##### scope( triggerScope ) 可用于 `group函数``user函数`,表示什么身份的用户可触发该命令,可赋值为: - `TriggerScope.all` (默认值):所有用户都可触发 - `TriggerScope.owner`:仅群主可触发 - `TriggerScope.admin`:仅管理员可触发 - `TriggerScope.member`:仅普通成员可触发 - 使用或运算`|`产生组合值:比如 `TriggerScope.owner | TriggerScope.admin` 表示群主和管理员可触发 ```js @trigger(TriggerScope.all) group() {} ``` ### Class CQMessageHelper 一个用来处理数组格式消息的工具类,该方法接收的 `message` 参数即为 `user函数``group函数``both函数``session函数` 参数的 `message` 属性。 ##### static removeAt(message: Message[]): Message[] 移除消息数组的艾特语句 ##### static isAt(robotQQ: number, message: Message[]): boolean 判断消息数组是否含有艾特robotQQ的语句 ##### static toRawMessage(message: Message[], removeAt?: boolean): string 将消息数组转换为字符串形式,特殊形式的信息则会变成[CQ码](https://docs.cqp.im/manual/cqcode/)形式。 ### Class CQRawMessageHelper 一个用来字符串格式消息的工具类。该方法接收的 `message` 参数即为 `user函数``group函数``both函数``session函数` 参数的 `rawMessage` 属性。 ##### static removeAt(message: string): string 移除字符串消息中的艾特CQ码 ### Class HttpPlugin 该类用于主动调用[HTTP插件提供的API](https://cqhttp.cc/docs/#/API?id=api-列表)。 #### constructor(endpoint: string, config?: PluginConfig) | key | type | description | optional | | -------- | ------------ | ------------------ | -------- | | endpoint | string | HTTP插件的运行地址 | | | config | PluginConfig | 插件配置信息 | optional | `PluginConfig`:一个对象,包含如下属性 | key | type | description | optional | | ----------- | ------ | ---------------------------------------------------------- | -------- | | accessToken | string | 须和HTTP插件配置文件值保持一致。在调用API时会验证该token。 | optional | 该类提供的实例方法名称是[HTTP插件文档](https://cqhttp.cc/docs/#/API?id=api-列表)提供API的 **驼峰式命名**,方法的返回值一个promise,其resolve值等同于HTTP插件文档的json对象,但方法的参数类型请以下述文档为准。 目前提供了如下接口的实现: ##### sendPrivateMsg(personQQ: number, message: string, escape?: boolean) ##### sendGroupMsg(groupQQ: number, message: string, escape?: boolean) ##### sendMsg(numbers: { user?: number; group?: number; }, message: string, escape?: boolean) ##### getGroupList() ##### getGroupMemberList(groupQQ: number) ##### downloadImage(cqFile: string) ### Class Session 该类用于启用上下文功能。 #### constructor(options? any) 该构造函数的参数类型同 [ioredis]( https://github.com/luin/ioredis/blob/master/API.md#Redis ) 库的`Redis` 构造函数的参数类型,example: ```js import { RobotFactory, HttpPlugin, Session } from 'lemon-bot'; const robot = RobotFactory.create({ // ... session: new Session(6379) }); ``` #### 如何启用上下文功能? 通过在 `create` 函数里传入 `session` 参数(如上述代码所示),即可开启使用`session函数`/上下文功能。 #### 什么是session函数? `session函数` 指的是以"session"单词开头的写在Command继承类里的函数,继承类里可以有任意多个 `session函数`。在 `session函数` 未过期前,接下来发给机器人的消息即使满足了其他命令的处理条件,但并不会执行他们,而是直接执行 `session函数`中的逻辑,直到session过期或调用 `setEnd` 手动结束。 #### 如何使用session函数?`user函数``group函数` 的参数中有个 `setNext` 属性,在 `session函数` 的参数中有 `setNext``setEnd` 这两个属性,他们都是异步函数,通过调用它们即可触发上下文功能,下面是函数说明: - `setNext(name: string, expireSeconds?: number): Promise<void>``name`的值为其他 `session函数` 的函数名或是省略"session"单词后的部分 (**警告:** `name` 是大小写敏感的),`expireSeconds` 选填,表示会话过期时间,默认为5分钟。 调用 `setNext` 后,当机器人再次接受到该用户会话后,将直接执行 `setNext` 参数指定的函数。然后你可以继续调用 `setNext` 指定其他函数,每次执行 `session函数` 时,都可以获取到历史消息记录,从而进行自己的逻辑处理。 - `setEnd(): Promise<void>` : 调用该函数表示结束当前会话上下文,当机器人再次接收到消息后,将会按照常规的解析流程处理:即先判断 `directive函数` 的返回值或者是执行 `parse函数`,然后执行 `group函数``user函数` 。**警告:** 请别忘记调用该函数来终止会话,否则在session过期前将会一直执行本次的session函数。 #### session函数demo: 现在我们改造上面Demo部分中的代码,来演示 `session函数` 的使用, **警告:** 下述例子设置了`count` 实例属性,由于不同的HTTP请求会共享命令,以及可能的并发等原因,无法确保`count` 属性的值与预期一致,故强烈不建议在类中设置实例属性,共享属性可使用第三方存储如 `redis` 来进行保存。下述代码仅为演示session函数的使用: ```js import { Command, RobotFactory, HttpPlugin, Session } from "lemon-bot"; class SimpleCommand extends Command { count = 3; directive() { return ["测试", "test"]; } async user({ fromUser, setNext }) { await setNext('A'); return "user run with " + this.count; } async sessionA({ setNext }) { this.count--; await setNext("B", 10); return "sessionA run with " + this.count; } async sessionB({ setNext }) { this.count--; await setNext("sessionC"); return "sessionB run with " + this.count; } async sessionC({ setNext, setEnd }) { this.count--; await setEnd(); return "sessionC run with 结束"; } } const robot = RobotFactory.create({ port: 8888, robot: 834679887, httpPlugin: new HttpPlugin("http://localhost:5700"), commands: [new SimpleCommand()], session: new Session() }); robot.start(); ``` 运行上述代码前,请确保 : - redis处于运行状态并可访问其默认的6379端口 - `robot`字段为你当前酷Q登陆的账号 然后在命令行内输入 `npx ts-node index.ts` 启动机器人,然后开始向你的机器人发送下面的信息: - 发送 "测试":会执行 `user函数` ,返回 "user run with 3" - 发送 任意消息:会执行 `sessionA`,返回"sessionA run with 2" - 10s内发送任意消息:会执行 `sessionB`,返回"sessionB run with 1" - 发送 任意消息:会执行 `sessionC`,返回"sessionC run with 结束" - 发送 "测试":将重新从 `user函数`开始解析,返回 "user run with 0" ## 安全指南 1. 尽可能避免HTTP插件的上报地址(即node服务器地址)可被外网访问,这会导致收到恶意请求。 2. 尽可能避免HTTP插件的运行地址可被公网访问,这会导致攻击者可进行API调用。若必需要公网下可访问,则应在配置文件中配置 access_token ,然后在代码的 `HttpPlugin` 实例中传入 `accessToken`参数,设置后,当在调用API时会自动验证token值。 ## Recipes ### 1. 文件目录如何组织? 建议使用如下结构: ``` +-- commands | +-- SearchQuestionCommand.ts | +-- HelpCommand.ts | +-- WordCommand.ts +-- index.ts ``` ### 2. 如何提供一个默认的消息处理函数? 假如我们目前的 `commands` 数组是 `[ new ACommand(), new BCommand() ]`,现在我希望当用户发来的消息都不满足这些命令的解析条件时,将它交由一个默认的处理命令: 1. 实现一个返回值始终为 **非`undefined`** 的`parse`函数: ```js export default class DefaultCommand extends Command { parse() {return true;} user(){ return "默认返回" } } ``` 2. 将该类的实例对象放在 `commands` 数组的**最后一位** : ```js const robot = new RobotFactory.create({ // ... commands: [new ACommand(), new BCommand(), new DefaultCommand()] }) ``` ### 3. 如何实现群组下不同成员共享session函数? 比如我想实现这样一个命令: 当我艾特机器人回复"开始收集反馈"后,接下来群员的发言内容会全部被采集,直到我艾特机器人发送 "收集结束"。 答: 目前的 `session函数` 的触发条件必须是 "消息是同一用户发送,如果用户位于群内,必须是在同一个群内发送消息",目前暂不支持触发条件是 "消息可以来自同一群组的不同用户"的情况。开发者可以通过使用redis并在默认消息处理命令里进行判断是否处于"消息反馈"状态下并进行处理。 ## TODO - [ ] 完善API接口实现 - [ ] 添加非消息事件上报的处理 - [ ] 编写测试😫 - [ ] 使用文档工具生成更易阅读的文档