npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

lemon-bot

v0.6.0

Published

a qq bot framework

Downloads

336

Readme

一个基于酷Q和CoolQ HTTP API插件的QQ机器人Nodejs开发框架。

  • 支持多命令匹配、命令自定义解析

  • 使用修饰器进行灵活的命令触发控制

  • 支持会话上下文功能

  • 支持多机器人运行

  • and more ...

前言

该项目仍处于早期开发版,故版本变动较为频繁,但会尽可能保证基本开发方式不变,具体变动见 Changelog

准备

  1. 安装 nodejs (该框架基于v10.16.3版本进行开发与测试)

  2. 安装 酷Q 和 HTTP插件:

    • Windows: 首先前往酷Q的版本发布页面下载(Air为免费版,Pro为收费版),下载后解压启动 CAQ.exeCQP.exe 并登陆你的QQ机器人账号。然后根据CoolQ HTTP API插件文档中的"手动安装"部分的教程进行插件安装。
    • Linux / Mac: 查看CoolQ HTTP API插件文档中的"使用Docker"部分的教程进行安装
  3. 修改HTTP插件的配置文件: 每个账号的配置文件存放路径一般为 /path/to/酷Q/data/app/io.github.richardchien.coolqhttpapi/config/QQ号.json (也可能是 .ini 格式)。下面以 .json 格式来说明在使用该框架时必须要进行修改的配置项(其他配置说明可见插件文档):

  4. 安装该node模块: npm i lemon-bot

  5. 由于该框架使用了 decorator 语法:

    • 若你是使用 Javascript 进行开发,则需要配置babel以支持该特性。

    • 若是使用 Typescript,则需要在tsconfig.json中启用decorator:

  6. 该框架使用 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号不能触发该命令。同时使用includeexclude会报错。

@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函数 的参数中有 setNextsetEnd 这两个属性,他们都是异步函数,通过调用它们即可触发上下文功能,下面是函数说明:

  • 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"

安全指南

  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. 实现一个返回值始终为 undefinedparse函数:

    export default class DefaultCommand extends Command {
        parse() {return true;}
        user(){
            return "默认返回"
        }
    }
  2. 将该类的实例对象放在 commands 数组的最后一位

    const robot = new RobotFactory.create({
        // ...
        commands: [new ACommand(), new BCommand(), new DefaultCommand()]
    })

3. 如何实现群组下不同成员共享session函数?

比如我想实现这样一个命令: 当我艾特机器人回复"开始收集反馈"后,接下来群员的发言内容会全部被采集,直到我艾特机器人发送 "收集结束"。

答: 目前的 session函数 的触发条件必须是 "消息是同一用户发送,如果用户位于群内,必须是在同一个群内发送消息",目前暂不支持触发条件是 "消息可以来自同一群组的不同用户"的情况。开发者可以通过使用redis并在默认消息处理命令里进行判断是否处于"消息反馈"状态下并进行处理。

TODO

  • [ ] 完善API接口实现
  • [ ] 添加非消息事件上报的处理
  • [ ] 编写测试😫
  • [ ] 使用文档工具生成更易阅读的文档