一个不太有用的机器人,不生产消息,只搬运消息。
- 简单易用的消息搬运功能。
- 简单强大的自定义回复功能。
- 完整支持 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("你没出拳,算我赢了!");
}
/**
* @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!
。
/**
* @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
是管理员账号通过"&"拼接起来的,系统默认依此判断用户是否是管理员。
默认不监听不回复任何群组,监听口令 listen
和 unlisten
,回复口令 reply
和 noreply
,需要管理员在对应群组发送口令。
字段 | 举例 | 用法 |
---|---|---|
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 省略... | 给插件增加图标 |
傻妞搬运的核心对象,在插件中为全局变量 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()
返回一个对象,或者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; //代码片段停止
}
由 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;
}>
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("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; // 获取当前桶名
}
可以通过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