Skip to content
This repository has been archived by the owner on Aug 15, 2023. It is now read-only.

Latest commit

 

History

History
553 lines (422 loc) · 23.4 KB

插件开发指引.md

File metadata and controls

553 lines (422 loc) · 23.4 KB

插件开发指引

说明

此文档只大致说明了插件开发的流程,具体的 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 中遵守这些约定,但是了解这些约定有助于你更好地使用本项目中的代码。

  1. 在所有可能的情况下,使用 undefined 替代 null
  2. 避免无意义的异步。
  3. 避免无意义地兑现一个 Promise 和无意义地 await
  4. 显式地比较变量的真值( true === ok ),隐式地比较方法返回的真值( isOK() )。
  5. 等价判断时常量在前( 1 === number )。
  6. 所有的导出统一在包的末尾进行。
  7. 变量和函数使用小驼峰式命名,类使用大驼峰式命名,数据结构中的属性名无约定。
  8. 模块变量以 m 开头,其后的部分,如果是变量使用大驼峰式命名,常量则使用大蛇式命名并使用 Object.freeze 冻结对象。
  9. 避免将匿名箭头函数赋值给变量。

这些规则并未作用于网页部分,这些代码由 Mark9804 编写和维护。

插件开发

开发者模块

运行命令 npm run install-dev 以安装开发者模块。

开发步骤

  1. ../config_defaults/command.yml 中添加入口。
  2. ../src/plugins/ 目录下实现插件。
  3. 如有需要,在 ../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>) !

插件参数

msg

除了 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 字段是一个可能包含这些类型的数组,但是为了统一 messageraw_message 字段,在 ../src/utils/load.js 中剔除了 TextElem 之外的所有类型并只保留一个 TextElem 。除此之外,将 raw_messagemessage 中仅存的 TextElem 进行了统一。除此之外, msg.raw_messagemsg.message[0].text 依次去除了 @ 机器人的 CQ 码、命令前缀 config.prefixes 和行首空格,但是你应当总是使用 msg.text 而非这两个属性。

msg.bot

除了 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()

功能权限

逻辑上,一个权限包含若干个功能,权限的开关影响到其中所有功能。如果要为插件中的功能添加权限管理,则需要。

  1. ../config_defaults/command.yml 中新增的插件下定义功能。
  2. ../config_defaults/command_master.yml 中的 master 插件中定义权限同名的功能。
  3. ../config_defaults/authority.yml 中定义权限的默认状态。
  4. ../src/utils/config.js 中的 global.authority.setting 中定义权限同名的属性,其值的数组中存放功能名称。
  5. ../src/utils/config.js 中的 readAuthority 中定义权限的默认值,在丢失所有配置文件时使用此值。
  6. 在插件中合适的代码逻辑处使用 ../src/utils/auth.js 中的 checkAuth 进行权限检查。

网页部分

Puppeteer

本项目使用 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;

Vue.js

你可以通过 ../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>

lodash

你可以通过 ../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 ,它支持 ChromeFirefox

  1. 可以在 config.yml 当中指定选项 viewDebug: 1 来进行实时调试。
  2. 在机器人运行时,可以使用命令 npm run tool-view -- -h 来查看如何调试 ../data/record/last_params/ 下的数据。

数据库

说明

../src/utils/database.js 中使用 lodash 封装了 lowdb

使用命令 npm run tool-db -- -h 来查看如何调试数据库。但因为数据库的缓存机制,所以无法使用此命令实时查看数据。

API

导入模块

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);

增删改查

缓存机制

所有的增删改查只操作内存缓存,均不涉及磁盘操作。

本节约定
  1. 当使用“对象”时,表示 JavaScript 中的概念;当使用“object”时,表示 JavaScript 中的类型。
  2. 当使用“字段”时,表示数据库对应的 object 中最外层级的路径。
  3. 当使用“路径”时,表示数据库中某字段下的路径。
  4. 调用中的变量参数的约定见下表。
变量 类型 作用 示例
name string 数据库的名字 "testDB"
key string 数据库中的字段 "data"
path string 数据库中某字段下的路径 "p1[0].p2"
predicate object 断言 { id: 100 }
键的存在性

判断数据库 name 中以下情形之一。返回 boolean 。

  1. 字段 key 是否存在。
  2. 字段 key 中是否存在路径 path
db.has(name, key);
db.has(name, key, path);
值的存在性

判断数据库 name 中以下情形之一。返回 boolean 。

  1. 字段 key 对应的 Array 中是否有包含 predicate 的 object ,或字段 key 的 object 中是否包含 predicate
  2. 字段 key 中路径 path 对应的 Array 中是否有包含 predicate 的 object ,或字段 key 中路径 path 的 object 中是否包含 predicate
db.includes(name, key, predicate);
db.includes(name, key, path, predicate);
获取数据

获取数据库 name 中以下对象之一。返回 object 或 undefined 。修改返回值不影响数据库。

  1. 字段 key 的 object 。
  2. 字段 key 中路径 path 对应的 object 。
  3. 字段 key 对应的 Array 中包含 predicate 的 object 。
  4. 字段 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 。

  1. 字段 key 的数据
  2. 字段 key 中路径 path 的数据
db.set(name, key, data);
db.set(name, key, path, data);
删除数据

删除数据库 name 中以下对象之一。返回 boolean 。

  1. 字段 key 对应的 Array 中包含 predicate 的 object ,或字段 key 对应的包含 predicate 的 object 。
  2. 字段 key 中路径 path 对应的 Array 中包含 predicate 的 object ,或字段 key 中路径 path 对应的包含 predicate 的 object 。
db.remove(name, key, predicate);
db.remove(name, key, path, predicate);
插入数据

数据库 name 中以下对象之一插入 data 。返回 boolean 。

  1. 字段 key 对应的 Array 。
  2. 字段 key 中路径 path 对应的 Array 。
db.push(name, key, data);
db.push(name, key, path, data);
更新数据

数据库 name 中以下对象之一中,将 data 合并到包含 predicate 的对象中。返回 boolean 。

  1. 字段 key 对应的 object 或 Array 。
  2. 字段 key 中路径 path 对应的 object 或 Array 。
db.update(name, key, predicate, data);
db.update(name, key, path, predicate, data);

更新逻辑如下。

  1. 使用 db.remove 删除旧数据。
  2. 将新数据和旧数据合并形成一个新的 object (数组不做合并以新数据为准)。
  3. 根据字段 key 或字段 key 中路径 path 值的类型,按照以下规则更新数据。
    1. 类型为 object ,使用 db.set 将其替换为新的 object 。
    2. 类型为 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)}`);
})();