/sillyGirl

傻妞机器人

Primary LanguageGo

傻妞

一个不太有用的机器人,不生产消息,只搬运消息。

特性

  • 简单易用的消息搬运功能。
  • 简单强大的自定义回复功能。
  • 完整支持 ECMAScript 5.1 的插件系统,基于 otto
  • 支持通过内置的阉割版 Express / fetch ,接入互联网。
  • 内置 Cron ,轻松实现定时任务。
  • 持久化的 Bucket 存储模块。
  • 支持同时接入多个平台多个机器人,自己开发。

快速上手

安装

releases 中找到合适自己系统版本的程序运行带 -t 可以开启终端机器人,直接与程序进行交互。

./sillyplus -t
2023/05/24 14:12:01.859 [I]  默认使用boltdb进行数据存储。
2023/05/24 14:12:01.950 [I]  Http服务已运行(8080)。

开发第一个插件

/**
 * @title HelleWorld
 * @rule raw ^你好$
 */

s.reply("Helle World!");

怼着程序输入 你好 ,就可以看到机器人回复的 Helle World!

你好
2023/05/24 14:15:48.350 [I]  匹配到规则:^你好$
Helle World!

插件注释 @rule raw ^你好$ 中的正则表达式被消息匹配时插件脚本就会被触发。

添加和销毁定时任务

不同于HelleWorld,插件注释 @on_start true 时是作为傻妞系统服务持续运行的。

/**
 * @title 定时任务
 * @on_start true
 */

const task = Cron();
let taskId = 0;
let times = 5;
const { id } = task.add("*/5 * * * * *", () => {
  // 同样支持分钟级任务,如:*/5 * * * *
  times--;
  console.log(
    `每5秒执行一次任务,${
      times ? `${times}次后结束任务` : "这是最后一次任务"
    }。`
  );
  if (times == 0) {
    task.remove(taskId); //移除任务
  }
});
taskId = id;

程序输出:

2023/05/27 19:57:00.000 [I]  每5秒执行一次任务,4次后结束任务。
2023/05/27 19:57:05.001 [I]  每5秒执行一次任务,3次后结束任务。
2023/05/27 19:57:10.001 [I]  每5秒执行一次任务,2次后结束任务。
2023/05/27 19:57:15.001 [I]  每5秒执行一次任务,1次后结束任务。
2023/05/27 19:57:20.000 [I]  每5秒执行一次任务,这是最后一次任务。

接入机器人

接入一个机器人首先 initAdapter,然后再通过 receive 持续接收消息和设置setReplyHandler以发送消息。

/**
 * @title 第一个机器人
 * @on_start true
 */

const task = Cron();
const qq_1700000 = initAdapter("qq", "1700000"); //初始化机器人,参数分别是平台、机器人ID

//模拟场景:每5秒用户100009给机器人1700000发送消息你好
task.add("*/5 * * * * *", function () {
  let message = {
    user_id: 100000, //用户ID,这里是假的,其他也是假的
    content: "你好", //消息内容ID
    // chat_id: "",    //聊天ID,注意,群聊默认不回复,在对应群聊使用口令listen和reply口令激活群聊
    // message_id: "", //消息ID
    // chat_name: "", //群聊名
    // user_name: "", //用户名
  };
  qq_1700000.receive(message); //机器人收到消息
});

qq_1700000.setReplyHandler(function (message) {
  console.log(`给用户${message.user_id}发消息:${message.content}`); //回复用户
});

程序每 5 秒都会输出该机器人收到的消息以及同时触发规则运行插件的日志。

2023/05/24 14:36:50.001 [I]  接收到消息 qq/100000@:你好
2023/05/24 14:36:50.001 [I]  匹配到规则:你好
2023/05/24 14:36:50.002 [I]  给用户100000发消息:Hello World!

与用户交互

/**
 * @title 用户交互插件
 * @rule 猜拳
 */

s.reply("你先出,请在10秒内出拳!");
ns = s.listen({
  rules: ["[出拳:剪刀,石头,布]"], // []中出拳是参数名,剪刀,石头,布是参数可能值
  timeout: 10000, // 超时设置
  handle: (s) => {
    let choose = s.param("出拳");
    s.reply(
      `我出${
        choose == "石头" ? "剪刀" : choose == "布" ? "剪刀" : "石头"
      },我赢了。`
    );
  },
});
if (!ns) {
  s.reply("你没出拳,算我赢了!");
}

开发 HTTP 接口

/**
 * @title 第一个web服务
 * @on_start true
 */

const app = Express(); //导入HTTP服务,傻妞默认开启,端口8080
app.get("/helloWorld", function (req, res) {
  res.send("Hello world!");
});

打开浏览器访问 http://127.0.0.1:8080/helloWorld ,当然地址根据实际情况,理论上可以看到接口返回的 Hello world!

实现一个 HTTP 请求

/**
 * @title 实现一个HTTP 请求
 * @on_start true
 */

let api = "/testRequest"; //接口地址

//第一步,实现一个原样返回请求数据的接口
const app = Express();
app.post(api, (req, res) => res.json(req.json()));

//第二步,请求第一步实现的接口
const port = Bucket("app").port ?? "8080"; // 获取http服务端口
const url = `http://127.0.0.1:${port}${api}`;
fetch({
  url,
  method: "POST",
  body: { value: "test" },
})
  .then((resp) => resp.json())
  .then((data) => console.log(`value is ${data.value}`))
  .catch((e) => console.log(e));

持久化存储

/**
 * @title 持久化存储
 * @rule raw ^我是谁$
 * @rule 我是[姓名]
 */

const user = Bucket("user"); //初始化存储桶user
let name = s.param("姓名");

if (user.name == "") {
  s.reply(`我不知道你是谁!`);
} else if (name == "谁") {
  s.reply(`你是${user.name}`);
} else {
  user.name = name;
  s.reply(`好的,你的姓名更新为${user.name}`);
}

插件实现了记名字的功能,其中姓名是方括号里匹配到的值,本质还是正则匹配到的。

我是谁
2023/05/24 15:43:40.121 [I]  匹配到规则:^我是谁$
我不知道你是谁!
我是小千
2023/05/24 15:43:49.735 [I]  匹配到规则:^我是([\s\S]+)$
好的,你的姓名更新为小千
我是谁
2023/05/24 15:43:53.727 [I]  匹配到规则:^我是谁$
你是小千

有了 Bucket 才有了傻妞从不认识小千到认识小千的过程。

管理员

const masters = Bucket("qq")["masters"];

masters 是管理员账号通过"&"拼接起来的,系统默认依此判断用户是否是管理员。

群组消息

默认不监听不回复任何群组,监听口令 listenunlisten,回复口令 replynoreply,需要管理员在对应群组发送口令。

深入了解

插件注释

字段 举例 用法
title HelloWorld 插件标题
rule raw ^我是([\s\S]+)$ 可写多行,取括号内参数 s.param(1) ,多个参数类推
priority 1 插件优先级,越高则优先处理
on_start true 插件后台任务执行脚本,避免重复运行
disable true 禁用脚本
form {title: "姓名", key:"user.name"} 插件表,key 值对应 存储桶.键名
public true 公开插件
create_at 2023-05-24 15:14:53 插件创建时间
description 本插件用于每天向女友问好 插件描述
author cdle 插件作者
version v1.0.0 插件版本
icon url 省略... 给插件增加图标

Sender

傻妞搬运的核心对象,在插件中为全局变量 s or sender。

interface Sender {
  getUserId(): string; //获取用户ID
  getUserName(): string; //获取用户昵称
  getChatId(): string; //获取群聊ID
  getChatName(): string; //获取群聊名称
  getMessageId(): Promise<string>; //获取消息ID
  getContent(): string; //获取消息内容
  continue(): void; //使消息继续往下匹配正则,消息正常第一次被匹配就会停止继续匹配
  setContent(content: string): void; //修改接收到的消息内容,可配合`continue`被其他规则匹配
  param(index: string | number): string; //获取`rule`匹配参数,可取[]内参数,?型参数从1开始取,例 `@rule 回复 ?` 对应 `s.param(1)`
  holdOn(content: string): string; //持续监听
  listen({
    rules: string[]; //匹配规则
    timeout: number; //超时,单位毫秒
    handle: (s: Sender): string;//如果匹配成功,则进入消息处理逻辑。如果将 holdOn(content) 的结果作为返回值,会继续监听
    listen_private: boolean; //监听用户群内消息时,同时监听用户消息
    listen_group: boolean; //监听用户群内消息时,同时监听群员消息
    allow_platforms: string[]; //平台白名单
    prohibit_platforms: string[]; //平台黑名单
    allow_groups: string[]; //群聊白名单
    prohibit_groups: string[]; //群聊黑名单
    allow_users: string[]; //用户白名单
    prohibit_users: string[]; //群聊白名单
  }): Sender; //超时,返回undefined
  isAdmin(): boolean; //判断消息是否来自管理员
  getPlatform(): string; //获取消息平台
  getBotId(): string; //获取机器人ID
  reply(content: string) Promise<string>; //回复消息,媒体消息推荐使用CQ码实现,返回消息ID
  recallMessage(meesageId: string | string[]): Promise<boolean>; //撤回消息
  kick(user_id: string): Promise<boolean>; //移出群聊
  unkick(user_id: string): Promise<boolean>; //取消移出群聊
  ban(user_id: string, duration: number): Promise<boolean>; //禁言,并指定时长
  unban(user_id: string): Promise<boolean>;  //取消禁言
}

Express Request / Response

只能说是够用,有需求可联系作者。插件中通过 Express() 返回一个对象,或者require("express")()

interface Request {
  body(): string; //获取请求体
  json(): any; //将请求体解析为JSON
  ip(): string; //获取客户端IP地址
  originalUrl(): string; //获取原始请求URL
  query(param: string): string; //获取查询参数
  param(i: number): string; //根据索引获取路径参数
  querys(): Record<string, string[]>; //获取所有查询参数
  postForm(s: string): string; //获取表单数据
  postForms(): Record<string, string[]>; //获取所有表单数据
  path(): string; //获取请求路径
  header(s: string): string; //获取请求头
  get(s: string): string; //获取请求头
  headers(): Record<string, string[]>; //获取所有请求头
  method(): string; //获取请求方法
  cookie(s: string): string; //获取 cookie
  cookies(): Record<string, string>; //获取 cookies
  continue(): void; //继续匹配其他路由
  setSession(k: string, v: string): string; //设置会话值
  getSession(k: string): string; //获取会话值
  getSessionId(): string; //获取会话ID
  destroySession(): string; //销毁会话
  logined(): boolean; //是否面板登录状态
}

interface Response {
  send(body: any): Response; //发送响应体
  sendStatus(status: number): Response; //发送状态码
  json(...ps: any[]): Response; //发送JSON响应
  header(str: string, value: string): Response; //设置响应头
  set(str: string, value: string): void; //设置响应头
  render(view: string, params: Record<string, any>): Response; //渲染视图
  redirect(...is: any[]): void; //重定向到URL
  status(i: number, ...s: string[]): Response; //设置状态码和文本
  setCookie(name: string, value: string, ...i: any[]): Response; //设置 Cookie
  stop(): void; //代码片段停止
}

fetch

net/http 封装而成,如有更多需求可以联系作者。

function fetch(options: {
  url: string; //请求地址
  method: string; //请求方法
  headers: { [key: string]: string }; //请求头
  json: boolean; // 返回json对象,等价于 responseType: "json"
  timeout: number;//超时参数,单位毫秒
  form:  { [key: string]: any };//formData表单数据,优先于下面的body
  body: any; // 请求体,支持字符串、二进制,对象自动转json字符串和添加相应请求头
  allow_redirects: boolean; // 是否允许重定向,默认允许
  proxy: {
    url: string, //代理地址,支持http、https、socks5
    user: string, //
    password: string, //
  }
}): Promise<response:{
  status: number; // 状态码,同statusCode
  headers: { [key: string]: string };
  body: any;
}>

Adapter

interface Message{
  message_id: string; // 消息ID
  user_id: string;    // 用户ID
  chat_id: string;    // 聊天ID
  content: string;    // 聊天内容
  user_name: string;  // 用户名
  chat_name: string;  // 群组名
}

class Adapter(botplt: string, botid: string) {
  isAdapter(botid: string): boolean; //判断id是否为机器人
  push(message: Message): [messageId: string[], error: string]; //推送消息,无视禁言设置
  getReplyMessage(): Promise<message: Message>; //获取一条回复消息,实际发送成功后,如果有id,请设置 message.message_id
  setReplyHandler(func: (message: Message): string): void; //设置回复事件处理方法,方法中返回消息ID,不推荐使用。
  receive(message: Message): Sender; //接收一个消息,并返回一个Sender对象
  setRecallMessage(func: (i: string | string[]) => boolean): void;//设置撤回消息函数。
  setGroupKick(func: (user_id: string, chat_id: string, reject_add_request: boolean) => void): boolean; //设置群聊成员移除函数,reject_add_request指5是否继续接受请求
  setGroupBan(func: (user_id: string, chat_id: string, duration: number) => void): boolean;//设置群聊成员禁言函数
  setGroupUnban(func: (user_id: string, chat_id: string) => void): boolean;//设置群聊成员解除禁言函数
  setIsAdmin(func: (user_id: string) => boolean): void; //设置用户是否是成员函数,默认自动实现
  destroy(): void;//销毁机器人
}
function getAdapter(platform: string, bot_id string): [Adapter: string[], error: string]; //获取一个机器人

function getAdapterBotsID(bot_id string): string[]; //获取一个平台的所有机器人

function getAdapterBotPlts(platform: string): string[]; //所有机器人平台

Bucket

例:通过 Bucket("app") 初始化一个 app 存储痛

interface Bucket(name: string) {
  get(key: string, defaultValue: any): any; // 取值
  set(key: string, value: any): Error | null; // 设值
  watch(key: string, event: (old: any, new_: any, key: string) => void); // 设置监听器,key 值为 * 时将监听整个桶的存储事件
  foreach(func: (key: string, value: any) => void): void; // 遍历值
  delete(key: string): Error | null; // 删值
  empty(): Error | undefined; // 清空桶
  keys(): string[]; // 获取所有键名
  len(): number | undefined; // 获取数据数目
  buckets(): string[]; // 获取所有存在的桶名
  _name(): string; // 获取当前桶名
}

Cron

可以通过let task = Cron()返回的对象来添加定时任务 const {id, error} = task.add("* * * * *", ()=>{})

interface Cron {
  add(crontab: string, ()=>void): {id: number, error: string}//添加定时任务 crontab同时支持秒级和分钟级
  remove(id: number): void//移除定时任务
}

插件表单

可以使用注释 @form {title: "标题", key: "test.title"} 添加表单元素。当如也可以直接在插件代码中添加,如下。

// 单个表单元素
Form({
  title: "姓名",
  key: "test.name",
});
// 多个表单元素
Form([
  {
    title: "姓名",
    key: "test.name",
  },
  {
    title: "性别",
    key: "test.sex",
  },
]);
// 使用schema-form
Form([
  {
    title: "创建时间",
    key: "test.createName",
    dataIndex: "test.createName",
    valueType: "date",
  },
  {
    title: "创建时间",
    key: "test.createName",
    dataIndex: "test.createName",
    valueType: "date",
  },
  {
    title: "分组",
    valueType: "group",
    columns: [
      {
        title: "状态",
        dataIndex: "test.groupState",
        valueType: "select",
        width: "xs",
        valueEnum: {
          all: { text: "全部", status: "Default" },
          open: {
            text: "未解决",
            status: "Error",
          },
          closed: {
            text: "已解决",
            status: "Success",
            disabled: true,
          },
          processing: {
            text: "解决中",
            status: "Processing",
          },
        },
      },
      {
        title: "标题",
        width: "md",
        dataIndex: "test.groupTitle",
        formItemProps: {
          rules: [
            {
              required: true,
              message: "此项为必填项",
            },
          ],
        },
      },
    ],
  },
]);

其他

sleep(millsec: number): void; //等待
md5(string): string; //加密
running(): boolean; //服务是否运行
uuid(): string; //生成uuid