lemon-bot
v0.6.0
Published
a qq bot framework
Downloads
217
Readme
一个基于酷Q和CoolQ HTTP API插件的QQ机器人Nodejs开发框架。
支持多命令匹配、命令自定义解析
使用修饰器进行灵活的命令触发控制
支持会话上下文功能
支持多机器人运行
and more ...
前言
该项目仍处于早期开发版,故版本变动较为频繁,但会尽可能保证基本开发方式不变,具体变动见 Changelog
准备
安装 nodejs (该框架基于v10.16.3版本进行开发与测试)
安装 酷Q 和 HTTP插件:
- Windows: 首先前往酷Q的版本发布页面下载(Air为免费版,Pro为收费版),下载后解压启动
CAQ.exe
或CQP.exe
并登陆你的QQ机器人账号。然后根据CoolQ HTTP API插件文档中的"手动安装"部分的教程进行插件安装。 - Linux / Mac: 查看CoolQ HTTP API插件文档中的"使用Docker"部分的教程进行安装
- Windows: 首先前往酷Q的版本发布页面下载(Air为免费版,Pro为收费版),下载后解压启动
修改HTTP插件的配置文件: 每个账号的配置文件存放路径一般为
/path/to/酷Q/data/app/io.github.richardchien.coolqhttpapi/config/QQ号.json
(也可能是.ini
格式)。下面以.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 }
安装该node模块:
npm i lemon-bot
由于该框架使用了 decorator 语法:
若你是使用 Javascript 进行开发,则需要配置babel以支持该特性。
若是使用 Typescript,则需要在
tsconfig.json
中启用decorator:{ "compilerOptions": { // ... "experimentalDecorators": true } }
该框架使用
debug
模块进行日志打印。在开发阶段建议开启日志输出来方便调试和排错:设置环境变量DEBUG=lemon-bot*
,然后运行主程序即可。
Demo
在 index.ts
文件里写入下述代码:
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号]"。
案例
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 | 启动机器人 | | stop | () => void | 停止机器人 |
Example:
const robot = RobotFactory.create({
port: 8888,
robot: 1326099664,
httpPlugin: new HttpPlugin("http://localhost:5700"),
commands: [new SimpleCommand()]
});
robot.start();
Class Command
该类需要被继承使用,用来创建命令。下面将以继承类的角度进行描述:
继承类的基本结构:
// 导入基类
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函数的描述。
函数参数说明:
上述函数中,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 | 是个异步函数,用来设置会话上下文结束 | session |
| historyMessage | Record<string, Array<Message[]>> | 一个对象,保存了历史会话消息,key为在 group函数
或 user函数
内调用setNext
时指定的参数名称 (含‘session’单词前缀) | session |
| setNext | (sessionName: string, expireSeconds?: number) => Promise | 是个异步函数,用来设置下一个需要执行的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号才可以触发该命令:
@include([ 12312, 21223 ])
user() {} //只有QQ为为它俩的用户可触发该命令
@include([ 3423344 ])
group() {} // 只有QQ群号为它的群可触发该命令
exclude( number[] )
可用于 group函数
和 user函数
,表示这些QQ群/QQ号不能触发该命令。同时使用include
和exclude
会报错。
@exclude([ 12312, 21223 ])
user() {} // 除了上述两位QQ用户不能触发该命令,其他用户可触发
@include([ 3333 ])
group() {} // 只有群号为3333的群可触发该命令
trigger( triggerType )
可用于 group函数
和 user函数
,表示命令触发方式,可赋值为:
TriggerType.at
:用户必须艾特机器人并发送消息方可触发命令TriggerType.noAt
:用户之间在群内发送消息可触发命令,艾特机器人即使命令正确也不会触发TriggerType.both
(默认值):艾特或者不艾特机器人都可触发命令
@trigger(TriggerType.noAt)
group() {}
scope( triggerScope )
可用于 group函数
和 user函数
,表示什么身份的用户可触发该命令,可赋值为:
TriggerScope.all
(默认值):所有用户都可触发TriggerScope.owner
:仅群主可触发TriggerScope.admin
:仅管理员可触发TriggerScope.member
:仅普通成员可触发- 使用或运算
|
产生组合值:比如TriggerScope.owner | TriggerScope.admin
表示群主和管理员可触发
@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码形式。
Class CQRawMessageHelper
一个用来字符串格式消息的工具类。该方法接收的 message
参数即为 user函数
、group函数
、both函数
、session函数
参数的 rawMessage
属性。
static removeAt(message: string): string
移除字符串消息中的艾特CQ码
Class HttpPlugin
该类用于主动调用HTTP插件提供的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插件文档提供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 库的Redis
构造函数的参数类型,example:
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函数的使用:
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"
安全指南
- 尽可能避免HTTP插件的上报地址(即node服务器地址)可被外网访问,这会导致收到恶意请求。
- 尽可能避免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() ]
,现在我希望当用户发来的消息都不满足这些命令的解析条件时,将它交由一个默认的处理命令:
实现一个返回值始终为 非
undefined
的parse
函数:export default class DefaultCommand extends Command { parse() {return true;} user(){ return "默认返回" } }
将该类的实例对象放在
commands
数组的最后一位 :const robot = new RobotFactory.create({ // ... commands: [new ACommand(), new BCommand(), new DefaultCommand()] })
3. 如何实现群组下不同成员共享session函数?
比如我想实现这样一个命令: 当我艾特机器人回复"开始收集反馈"后,接下来群员的发言内容会全部被采集,直到我艾特机器人发送 "收集结束"。
答: 目前的 session函数
的触发条件必须是 "消息是同一用户发送,如果用户位于群内,必须是在同一个群内发送消息",目前暂不支持触发条件是 "消息可以来自同一群组的不同用户"的情况。开发者可以通过使用redis并在默认消息处理命令里进行判断是否处于"消息反馈"状态下并进行处理。
TODO
- [ ] 完善API接口实现
- [ ] 添加非消息事件上报的处理
- [ ] 编写测试😫
- [ ] 使用文档工具生成更易阅读的文档