523 lines
17 KiB
Markdown
523 lines
17 KiB
Markdown
# 傻妞
|
||
|
||
一个不太有用的机器人,不生产消息,只搬运消息。
|
||
|
||
## 特性
|
||
|
||
- 简单易用的消息搬运功能。
|
||
- 简单强大的自定义回复功能。
|
||
- 完整支持 ECMAScript 5.1 的插件系统,基于 [otto](https://github.com/robertkrimen/otto)。
|
||
- 支持通过内置的阉割版 `Express` / `fetch` ,接入互联网。
|
||
- 内置 `Cron` ,轻松实现定时任务。
|
||
- 持久化的 `Bucket` 存储模块。
|
||
- 支持同时接入多个平台多个机器人,自己开发。
|
||
|
||
## 快速上手
|
||
|
||
### 安装
|
||
|
||
在 [releases](https://github.com/cdle/sillyGirl/releases) 中找到合适自己系统版本的程序运行带 `-t` 可以开启终端机器人,直接与程序进行交互。
|
||
|
||
```shell
|
||
./sillyplus -t
|
||
2023/05/24 14:12:01.859 [I] 默认使用boltdb进行数据存储。
|
||
2023/05/24 14:12:01.950 [I] Http服务已运行(8080)。
|
||
```
|
||
|
||
### 开发第一个插件
|
||
|
||
```js
|
||
/**
|
||
* @title HelleWorld
|
||
* @rule raw ^你好$
|
||
*/
|
||
|
||
s.reply("Helle World!");
|
||
```
|
||
|
||
怼着程序输入 `你好` ,就可以看到机器人回复的 `Helle World!` 了
|
||
|
||
```sh
|
||
你好
|
||
2023/05/24 14:15:48.350 [I] 匹配到规则:^你好$
|
||
Helle World!
|
||
```
|
||
|
||
插件注释 `@rule raw ^你好$` 中的正则表达式被消息匹配时插件脚本就会被触发。
|
||
|
||
### 添加和销毁定时任务
|
||
|
||
不同于`HelleWorld`,插件注释 `@on_start true` 时是作为傻妞系统服务持续运行的。
|
||
|
||
```js
|
||
/**
|
||
* @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;
|
||
```
|
||
|
||
程序输出:
|
||
|
||
```sh
|
||
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`以发送消息。
|
||
|
||
```js
|
||
/**
|
||
* @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 秒都会输出该机器人收到的消息以及同时触发规则运行插件的日志。
|
||
|
||
```sh
|
||
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!
|
||
```
|
||
|
||
### 与用户交互
|
||
|
||
```js
|
||
/**
|
||
* @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 接口
|
||
|
||
```js
|
||
/**
|
||
* @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 请求
|
||
|
||
```js
|
||
/**
|
||
* @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));
|
||
```
|
||
|
||
### 持久化存储
|
||
|
||
```js
|
||
/**
|
||
* @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` 才有了傻妞从不认识小千到认识小千的过程。
|
||
|
||
### 管理员
|
||
|
||
```js
|
||
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 省略... | 给插件增加图标 |
|
||
|
||
### Sender
|
||
|
||
傻妞搬运的核心对象,在插件中为全局变量 s or sender。
|
||
|
||
```ts
|
||
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")()`。
|
||
|
||
```ts
|
||
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` 封装而成,如有更多需求可以联系作者。
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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 存储痛
|
||
|
||
```ts
|
||
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("* * * * *", ()=>{})`
|
||
|
||
```ts
|
||
interface Cron {
|
||
add(crontab: string, ()=>void): {id: number, error: string}//添加定时任务 crontab同时支持秒级和分钟级
|
||
remove(id: number): void//移除定时任务
|
||
}
|
||
```
|
||
|
||
### 插件表单
|
||
|
||
可以使用注释 `@form {title: "标题", key: "test.title"}` 添加表单元素。当如也可以直接在插件代码中添加,如下。
|
||
|
||
```js
|
||
// 单个表单元素
|
||
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: "此项为必填项",
|
||
},
|
||
],
|
||
},
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
```
|
||
|
||
### 其他
|
||
|
||
```ts
|
||
sleep(millsec: number): void; //等待
|
||
md5(string): string; //加密
|
||
running(): boolean; //服务是否运行
|
||
uuid(): string; //生成uuid
|
||
```
|