此文档只大致说明了插件开发的流程,具体的 API 细节请参考具体的代码实现,你也可以在某些目录下的 README.txt
中找到其所提供 API 的说明列表。
注意,本项目的问答功能可满足简单的需求,你可以把此功能当做轻量级插件使用。
Adachi-BOT
├── app.js # 主程序
├── bot.js # 机器人
├── config # 用户配置目录
├── config_defaults # 默认配置目录
│ ├── command.yml # 插件配置文件
│ ├── command_master.yml # 管理员插件配置文件
│ └── setting.yml # 机器人行为配置文件
├── data # 数据、三方库、字体和临时文件目录
├── scripts # 用户工具目录
├── src # 源码目录
│ ├── bot # 主程序组件
│ ├── jobs # 定时任务
│ ├── plugins # 插件
│ ├── utils # 核心库
│ └── views # 网页
├── tools # 开发者工具目录
└── server.js # 文件服务器
你不必在自己的 fork 中遵守这些约定,但是了解这些约定有助于你更好地使用本项目中的代码。
- 在所有可能的情况下,使用
undefined
替代null
。 - 避免无意义的异步。
- 避免无意义地兑现一个 Promise 和无意义地
await
。 - 显式地比较变量的真值(
true === ok
),隐式地比较方法返回的真值(isOK()
)。 - 等价判断时常量在前(
1 === number
)。 - 所有的导出统一在包的末尾进行。
- 变量和函数使用小驼峰式命名,类使用大驼峰式命名,数据结构中的属性名无约定。
- 模块变量以
m
开头,其后的部分,如果是变量使用大驼峰式命名,常量则使用大蛇式命名并使用Object.freeze
冻结对象。 - 避免将匿名箭头函数赋值给变量。
运行命令 npm run install-dev
以安装开发者模块。
- 在
../config_defaults/command.yml
中添加入口。 - 在
../src/plugins/
目录下实现插件。 - 如有需要,在
../src/views/
下实现网页。
下面的 Patch 演示了如何添加一个插件。
From 36d30199646ee1e26cc0de8ffedb30b2460ed56b Mon Sep 17 00:00:00 2001
From: Qin Fandong <shell_way@foxmail.com>
Date: Tue, 14 Dec 2021 14:45:49 +0800
Subject: [PATCH] Hello World!
---
config_defaults/command.yml | 17 +++++++++++++++++
src/plugins/hello_world/index.js | 19 +++++++++++++++++++
src/utils/config.js | 2 +-
3 files changed, 37 insertions(+), 1 deletion(-)
create mode 100644 src/plugins/hello_world/index.js
diff --git a/config_defaults/command.yml b/config_defaults/command.yml
index 0daf8a3..e9923bd 100644
--- a/config_defaults/command.yml
+++ b/config_defaults/command.yml
@@ -224,6 +224,23 @@ gacha:
entrance:
- ^取消定轨
+hello_world:
+ enable: true
+ weights: 10099
+ regex:
+ - ^hello\sworld$
+ functions:
+ hello_world:
+ type: command
+ show: true
+ weights: 9999
+ name: hello world
+ usage: null
+ revert: false
+ description: 向你致以诚挚的问候
+ entrance:
+ - ^hello
+
tools:
enable: true
weights: 99
diff --git a/src/plugins/hello_world/index.js b/src/plugins/hello_world/index.js
new file mode 100644
index 0000000..5d2f861
--- /dev/null
+++ b/src/plugins/hello_world/index.js
@@ -0,0 +1,19 @@
+import { checkAuth } from "#utils/auth";
+import { hasEntrance } from "#utils/config";
+
+function doHelloWorld(msg) {
+ const message = `Welcome to world, ${msg.name} (${msg.uid}) !`;
+ msg.bot.say(msg.sid, message, msg.type, msg.uid);
+}
+
+async function Plugin(msg) {
+ switch (true) {
+ case hasEntrance(msg.text, "hello_world", "hello_world"):
+ if (!checkAuth(msg, "hello_world")) {
+ doHelloWorld(msg);
+ }
+ break;
+ }
+}
+
+export { Plugin as run };
diff --git a/src/utils/config.js b/src/utils/config.js
index 1c1bad9..7d7f728 100644
--- a/src/utils/config.js
+++ b/src/utils/config.js
@@ -498,7 +498,7 @@ global.authority = {
artifact_auth: ["artifacts", "strengthen", "dungeons"],
character_overview_auth: ["info", "material", "weapon", "talent", "weekly"],
feedback_auth: ["feedback"],
- fun_auth: ["menu", "prophecy", "roll"],
+ fun_auth: ["menu", "prophecy", "roll", "hello_world"],
gacha_auth: ["gacha", "pool", "select", "select-nothing", "select-what"],
music_auth: ["music", "music_source"],
mys_news_auth: [global.innerAuthName.mysNews],
--
2.31.1
应用该 Patch 后,启动机器人,发送 QQ 聊天信息 hello world
则会得到回复 Welcome to world, <nickname> (<id>) !
。
除了 oicq 中消息的原有的属性外,你还可以使用以下属性。
属性 | 内容 |
---|---|
bot |
QQ Client |
atMe |
有人 @ 机器人为 true ,否则为 false |
groupOfStranger |
发送者为陌生人为其所在的群号,否则为 undefined |
uid |
消息发送者的 QQ 号 |
gid |
群消息为群号,好友消息为 undefined |
sid |
群消息为 msg.gid ,好友消息为 msg.uid |
type |
群消息为 group ,好友消息为 private |
name |
消息发送者的 QQ 昵称 |
text |
依次去除了 @ 机器人的 CQ 码、命令前缀 config.prefixes 和行首空格的聊天文本 |
对开发者透明的一处数据结构改变。
插件的参数 msg
传递的 oicq 数据结构改变。原本的其中的 message
字段是一个可能包含这些类型的数组,但是为了统一 message
和 raw_message
字段,在 ../src/utils/load.js
中剔除了 TextElem 之外的所有类型并只保留一个 TextElem 。除此之外,将 raw_message
和 message
中仅存的 TextElem 进行了统一。除此之外, msg.raw_message
和 msg.message[0].text
依次去除了 @
机器人的 CQ 码、命令前缀 config.prefixes
和行首空格,但是你应当总是使用 msg.text
而非这两个属性。
除了 oicq 中 Client 中的原有方法外,你还可以使用以下方法。
方法 | 作用 |
---|---|
boardcast |
发送一条广播 |
say |
发送一条消息 |
sayMaster |
给全体管理者发送一条消息 |
在 readConfig
中引入以下包含了配置文件中数据的全局变量,可供插件使用。使用这些全局变量前确保仔细阅读了 ../src/utils/config.js
中的注释,清楚地了解你要用的数据结构。另外你需要显式地通过全局对象 global
使用这些全局变量,以避免 npm run code-check
将全局变量视为未声明的变量。
变量 | 数据 |
---|---|
global.rootdir |
项目根目录 |
global.configdefdir |
项目根目录下的 config_defaults 目录 |
global.configdir |
项目根目录下的 config 目录 |
global.datadir |
项目根目录下的 data 目录 |
global.oicqdir |
项目根目录下的 data 目录 |
global.resqdir |
项目根目录下的 resources 目录 |
global.package |
../package.json |
global.all |
../config*/command.yml 和 ../config*/command_master.yml 的部分内容 |
global.command |
../config*/command.yml |
global.master |
../config*/command_master.yml |
global.artifacts |
../config*/artifacts.yml |
global.authority |
部分数据来自 ../config*/authority.yml |
global.config |
../config*/setting.yml |
global.cookies |
../config*/cookies.yml |
global.greeting |
../config*/greeting.yml |
global.menu |
../config*/menu.yml |
global.prophecy |
../config*/prophecy.yml |
global.names |
../config*/names.yml |
global.eggs |
../config*/pool_eggs.yml |
global.info.character |
../resources/Version2/info/docs/<角色名>.json |
一些其他的全局变量如下。
变量 | 数据 | 引入阶段 |
---|---|---|
global.bots |
所有可用的 msg.bot |
create() |
global.bots.logger |
global.bots[0].logger |
create() |
global.browser |
Puppeteer 浏览器实例 | init() |
逻辑上,一个权限包含若干个功能,权限的开关影响到其中所有功能。如果要为插件中的功能添加权限管理,则需要。
- 在
../config_defaults/command.yml
中新增的插件下定义功能。 - 在
../config_defaults/command_master.yml
中的master
插件中定义权限同名的功能。 - 在
../config_defaults/authority.yml
中定义权限的默认状态。 - 在
../src/utils/config.js
中的global.authority.setting
中定义权限同名的属性,其值的数组中存放功能名称。 - 在
../src/utils/config.js
中的readAuthority
中定义权限的默认值,在丢失所有配置文件时使用此值。 - 在插件中合适的代码逻辑处使用
../src/utils/auth.js
中的checkAuth
进行权限检查。
本项目使用 Puppeteer 通过对网页截图生成图片,如果插件有生成图片的需求,则需要在 ../src/views/
中编写网页,并使用 ../src/utils/render.js
中的 render
方法对该网页进行截图。
在 render
方法中使用 URL 参数传递数据给网页,你需要在网页中从 URL 参数中获取数据。
import { getParams } from "../common/utils.js";
const params = getParams(window.location.href);
const prop1 = params.prop1;
你可以通过 ../data/js/vue3.global.prod.js
中的变量 window.Vue
来使用 Vue.js 。
<script src="../../data/js/vue3.global.prod.js"></script>
<script>
const { createApp } = window.Vue;
const app = createApp({});
// do something
</script>
你可以通过 ../data/js/lodash.min.js
中的变量 window._
来使用 lodash 。
<script src="../../data/js/lodash.min.js"></script>
<script>
const lodash = window._;
// do something;
</script>
项目中提供了一些手段帮助你进行调试。调试时建议使用 Vue.js devtools ,它支持 Chrome 和 Firefox 。
- 可以在
config.yml
当中指定选项viewDebug: 1
来进行实时调试。 - 在机器人运行时,可以使用命令
npm run tool-view -- -h
来查看如何调试../data/record/last_params/
下的数据。
在 ../src/utils/database.js
中使用 lodash 封装了 lowdb 。
使用命令
npm run tool-db -- -h
来查看如何调试数据库。但因为数据库的缓存机制,所以无法使用此命令实时查看数据。
import db from "#utils/database";
初始化名称为 name
的数据库。返回 boolean 。数据库已存在则加载其数据,数据库不存在则创建空数据库。
db.init(name, struct);
struct
默认为{ user: [] }
,必须为 object 。
得到目前所有可用的数据库。返回 Array 。
db.names();
得到数据库 name
的磁盘文件路径。返回 string 。避免使用,并避免以任何方式操作此文件。
db.file(name);
数据库的数据由 db.init
一次性从磁盘读入内存缓存。缓存周期性和在程序退出时自动同步到磁盘,当前数据库缓存的同步周期为五分钟。
将数据库 name
的缓存同步到磁盘。尽量避免使用。返回 boolean 。
db.sync(name);
所有的增删改查只操作内存缓存,均不涉及磁盘操作。
- 当使用“对象”时,表示 JavaScript 中的概念;当使用“object”时,表示 JavaScript 中的类型。
- 当使用“字段”时,表示数据库对应的 object 中最外层级的路径。
- 当使用“路径”时,表示数据库中某字段下的路径。
- 调用中的变量参数的约定见下表。
变量 | 类型 | 作用 | 示例 |
---|---|---|---|
name |
string | 数据库的名字 | "testDB" |
key |
string | 数据库中的字段 | "data" |
path |
string | 数据库中某字段下的路径 | "p1[0].p2" |
predicate |
object | 断言 | { id: 100 } |
判断数据库 name
中以下情形之一。返回 boolean 。
- 字段
key
是否存在。 - 字段
key
中是否存在路径path
。
db.has(name, key);
db.has(name, key, path);
判断数据库 name
中以下情形之一。返回 boolean 。
- 字段
key
对应的 Array 中是否有包含predicate
的 object ,或字段key
的 object 中是否包含predicate
。 - 字段
key
中路径path
对应的 Array 中是否有包含predicate
的 object ,或字段key
中路径path
的 object 中是否包含predicate
。
db.includes(name, key, predicate);
db.includes(name, key, path, predicate);
获取数据库 name
中以下对象之一。返回 object 或 undefined
。修改返回值不影响数据库。
- 字段
key
的 object 。 - 字段
key
中路径path
对应的 object 。 - 字段
key
对应的 Array 中包含predicate
的 object 。 - 字段
key
中路径path
对应的 Array 中包含predicate
的 object 。
db.get(name, key);
db.get(name, key, path);
db.get(name, key, predicate);
db.get(name, key, path, predicate);
设置数据库 name
中以下数据之一为 data
。返回 boolean 。
- 字段
key
的数据 - 字段
key
中路径path
的数据
db.set(name, key, data);
db.set(name, key, path, data);
删除数据库 name
中以下对象之一。返回 boolean 。
- 字段
key
对应的 Array 中包含predicate
的 object ,或字段key
对应的包含predicate
的 object 。 - 字段
key
中路径path
对应的 Array 中包含predicate
的 object ,或字段key
中路径path
对应的包含predicate
的 object 。
db.remove(name, key, predicate);
db.remove(name, key, path, predicate);
数据库 name
中以下对象之一插入 data
。返回 boolean 。
- 字段
key
对应的 Array 。 - 字段
key
中路径path
对应的 Array 。
db.push(name, key, data);
db.push(name, key, path, data);
数据库 name
中以下对象之一中,将 data
合并到包含 predicate
的对象中。返回 boolean 。
- 字段
key
对应的 object 或 Array 。 - 字段
key
中路径path
对应的 object 或 Array 。
db.update(name, key, predicate, data);
db.update(name, key, path, predicate, data);
更新逻辑如下。
- 使用
db.remove
删除旧数据。- 将新数据和旧数据合并形成一个新的 object (数组不做合并以新数据为准)。
- 根据字段
key
或字段key
中路径path
值的类型,按照以下规则更新数据。
- 类型为 object ,使用
db.set
将其替换为新的 object 。- 类型为 Array ,使用
db.push
将新的 object 插入其中。
下面的代码演示了如何使用这些数据库 API ,注意其中未做错误处理。
// db.sync 需要使用其中的全局变量 global.rootdir
// 开发本项目时无需导入,因为 app.js 中已经导入了此文件
import "#utils/config";
// 导入数据库 API
import db from "#utils/database";
(function main() {
// 数据库名字
const name = "testDB";
// 初始化字段,通常用于声明数据库的结构
const struct = { user: [], data: {} };
// XXX 使用前必须先初始化数据库
db.init(name, struct);
console.log(`Set data for database ${name} ...`);
// 设置数据库中 user 字段的值
db.set(name, "user", [
{ id: 100, name: "A" },
{ id: 101, name: "A" },
]);
// 设置数据库中 data 字段的值
db.set(name, "data", { p1: [{ p2: "text" }], a: { b: { c: "text" } } });
// 单独设置某字段下路径的值
db.set(name, "user", "[1].name", "B");
// 获取数据库中某字段的全量数据
// XXX 尽量避免直接获取并操作某字段的全量数据
if (db.has(name, "user")) {
console.log("user:", db.get(name, "user") || []);
}
if (db.has(name, "data")) {
console.log("data:", db.get(name, "data") || {});
}
// 检查数据库中 data 字段下是否存在 p1[0].p2 路径
if (db.has(name, "data", "p1[0].p2")) {
// 获取数据库中 data 字段下路径 p1[0].p2 的值
const p2 = db.get(name, "data", "p1[0].p2");
console.log("data has path p1[0].p2", `with value "${p2}"`, "and type", typeof p2);
}
// 检查数据库中 user 字段的 Array 是否存在 id 值为 100 的 object
if (db.includes(name, "user", { id: 100 })) {
// 获取数据库中 user 字段的 Array 中 id 值为 100 的 object
console.log("object include { id: 100 } in user:", db.get(name, "user", { id: 100 }));
}
// 检查数据库中 data 字段下路径 p1 的 Array 中是否存在 p2 的值为 "text" 的 object
if (db.includes(name, "data", "p1", { p2: "text" })) {
// 获取数据库中 data 字段下路径 p1 的 Array 中 p2 的值为 "text" 的 object
console.log('object include { p2: "text" } in array of data.p1:', db.get(name, "data", "p1", { p2: "text" }));
}
// 检查数据库中 data 字段下路径 a.b 的 object 中是否包含 c 为 "text" 的属性
if (db.includes(name, "data", "a.b", { c: "text" })) {
// 获取数据库中 data 字段下路径 a.b 的 object 中 c 的值为 "text" 的 object
console.log('object data.a.b include { c: "text" }:', db.get(name, "data", "a.b", { c: "text" }));
}
console.log(`Update data for database ${name} ...`);
// 删除数据库 user 字段的 Array 中 id 为 101 的 object
db.remove(name, "user", { id: 101 });
// 删除数据库 data 字段下路径 p1 的 Array 中 p2 为 "text" 的 object
db.remove(name, "data", "p1", { p2: "text" });
// 更新数据库 user 字段的 Array 中 id 为 100 的 object
db.update(name, "user", { id: 100 }, { id: 1000, name: "AA" });
// 数据库 user 字段下插入一个 id 为 1001 的 object
db.push(name, "user", { id: 1001, name: "BB" });
// 数据库 data 字段下路径 p1 中插入一个 p22 为 "text2" 的 object
db.push(name, "data", "p1", { p22: "text2" });
// 更新数据库 data 字段下路径 p1 的 Array 中 p22 为 "text2" 的 object
db.update(name, "data", "p1", { p22: "text2" }, { p22: "text22" });
// 更新数据库 data 字段下路径 a.b 的 object ——如果其值的 object 包含 c 为 "text" 的属性
db.update(name, "data", "a.b", { c: "text" }, { c: "textabc" });
console.log("user:", db.get(name, "user") || []);
console.log("data:", db.get(name, "data") || {});
console.log(`Update data for database ${name} ...`);
// 删除数据库 data 字段下路径 p1[0] 的 object ——如果其值的 object 包含 p22 为 "text22" 的属性
db.remove(name, "data", "p1[0]", { p22: "text22" });
// 删除数据库 data 字段下路径 a.b 的 object ——如果其值的 object 包含 c 为 "textabc" 的属性
db.remove(name, "data", "a.b", { c: "textabc" });
console.log("data:", db.get(name, "data") || {});
// 获取所有的数据库名字
console.log("All databases:", db.names().join(", "));
// 根据名字同步数据库缓存到磁盘
// XXX 尽量避免同步缓存,已有一个周期性的定时任务同步缓存,程序退出时也会做同步
console.log(`Syncing database: ${name}`);
db.sync(name);
// XXX 避免以任何方式操作此文件
console.log(`Database file: ${db.file(name)}`);
})();