Skip to content

开发插件入门

NOTE

部分示例代码来自mioki官方文档

命名规范

Mioku 中对插件命名的要求:

  • Git 仓库名:mioku-plugin-name
  • 本地插件文件夹名:name
  • Mioku 中启用的插件名:name

例如你要写一个天气插件:

  • Git 仓库:mioku-plugin-weather
  • 本地目录:plugins/weather/
  • 启用插件名:weather

插件目录结构

一个基础的插件,只需要这两个文件:

text
plugins/weather/
  index.ts
  package.json

其中:

  • index.ts:插件运行入口
  • package.json:插件的基本信息

当然,我们推荐把复杂的逻辑从index.ts中分离出来方便管理。

开始编写一个插件

下面以 weather 插件为例

bash
mkdir -p plugins/weather
cd plugins/weather
# 初始化仓库
git init

新建 package.json

写入下面这份配置

json
{
  "name": "mioku-plugin-weather",
  "version": "1.0.0",
  "description": "天气插件",
  "main": "index.ts",
  "mioku": {
    "services": [],
    "help": {
      "title": "天气",
      "description": "天气插件示例",
      "commands": [
        {
          "cmd": "/天气",
          "desc": "查询天气",
          "usage": "/天气 北京",
          "role": "member"
        }
      ]
    }
  }
}

package 字段如下

字段类型必填说明
namestring推荐使用 mioku-plugin-<name>
versionstring插件版本号
descriptionstring插件描述
mainstring插件入口文件,一般写 index.ts
mioku.servicesstring[]插件依赖的 Mioku 服务名称
mioku.helpobject插件帮助信息,Mioku 会自动读取

mioku 配置块常用字段如下

字段类型必填说明
servicesstring[]声明插件依赖的服务,例如 configscreenshotai
helpobject插件帮助信息,Mioku 会自动收集

编写 index.ts

现在再写一个入口文件,插件就可以运行了

ts
import { definePlugin } from "mioki";

export default definePlugin({
  name: "weather",
  version: "1.0.0",
  description: "天气插件",

  async setup(ctx) {
    ctx.handle("message", async (event) => {
      // 读取消息中的文本
      const text = ctx.text(event).trim();
      const match = text.match(/^\/天气\s+(.+)$/);
      if (!match) {
        return;
      }
      const city = match[1];
      // 回复一条消息
      await event.reply(`${city}:晴,26°C,适合出门。`);
    });
  },
});

definePlugin 字段说明

插件的运行入口通过 definePlugin({...}) 定义

ts
export default definePlugin({
  name: "demo",
  version: "1.0.0",
  priority: 100,
  description: "示例插件",
  dependencies: [],
  setup(ctx) {},
});

插件结构字段如下

属性类型必填说明
namestring插件唯一标识,应与插件目录名一致
versionstring插件版本号,推荐使用语义化版本
prioritynumber加载优先级,数值越小越先加载,默认 100
descriptionstring插件描述信息
dependenciesstring[]插件依赖,仅供参考,框架不处理
setupfunction插件初始化函数,接收上下文对象

上下文对象

ctx 就是插件运行时的上下文对象。
平时写插件,大部分工作都会围绕它展开

部分用法如下

ts
// 机器人实例(当前处理事件的 bot)
ctx.bot; // NapCat 实例

// 所有已连接的 bot 列表
ctx.bots; // ExtendedNapCat[]

// 当前 bot 的 QQ 号
ctx.self_id; // number

// 机器人信息
ctx.bot.uin; // QQ 号
ctx.bot.nickname; // 昵称

// 消息构造器
ctx.segment; // 消息段构造器

// 日志器
ctx.logger; // 插件专属日志器

// 事件去重器
ctx.deduplicator; // Deduplicator

// 框架配置
ctx.botConfig;

ctx.handle("message", async (event) => {
  // 检查发消息的人是不是主人
  const owner = ctx.isOwner(event);

  // 检查发消息的人是不是管理员
  const admin = ctx.isAdmin(event);

  ctx.logger.info(`owner=${owner} admin=${admin}`);
});

更完整的接口说明请看

常用 API

写基础插件时,最常用的是下面这些

API说明
ctx.handle()注册事件处理器
ctx.text(event)获取消息纯文本
ctx.match()快速做关键词匹配
ctx.segment构造消息段
ctx.pickBot(id)多实例场景下选择指定 bot
ctx.cron()注册定时任务
ctx.logger输出插件日志
ctx.isOwner(event)判断是否为主人
ctx.isAdmin(event)判断是否为管理员

事件监听器

使用 ctx.handle() 注册事件监听器

ts
// 监听所有消息
ctx.handle('message', async (event) => {
    ctx.logger.info(`收到消息:${event.raw_message}`)
})

// 仅监听群消息
ctx.handle('message.group', async (event) => {
    ctx.logger.info(`收到群 ${event.group_id} 的消息`)
})

// 仅监听私聊消息
ctx.handle('message.private', async (event) => {
    ctx.logger.info(`收到来自 ${event.user_id} 的私聊消息`)
})

// 监听通知事件
ctx.handle('notice', async (event) => {
    ctx.logger.info(`收到通知:${event.notice_type}`)
})

// 监听请求事件
ctx.handle('request.friend', async (event) => {
    ctx.logger.info(`收到好友请求:${event.user_id}`)
    await event.approve() // 自动同意
})

定时任务

使用 ctx.cron() 注册定时任务

ts
// 每小时执行一次
ctx.cron("0 * * * *", async (_ctx, task) => {
  // task.date 是这次调度的触发时间
  ctx.logger.info(`定时任务执行时间: ${task.date.toISOString()}`);
});

每天早上九点执行

ts
ctx.cron("0 9 * * *", async () => {
  ctx.logger.info("早安任务开始");
});

消息回复

最简单的回复就是直接使用 event.reply()

ts
ctx.handle("message", async (event) => {
  if (ctx.text(event).trim() === "/hello") {
    // 最基础的文字回复
    await event.reply("你好");
  }
});

引用回复

ts
ctx.handle("message", async (event) => {
  if (ctx.text(event).trim() === "/quote") {
    // 第二个参数为 true 时会引用回复
    await event.reply("这是一条引用回复", true);
  }
});

消息段构造

使用 ctx.segment 构造各种各样的消息段

ts
ctx.handle('message', async (event) => {
    // 纯文本
    ctx.segment.text('Hello')

    // @某人
    ctx.segment.at(123456789)
    ctx.segment.at('all') // @全体成员

    // QQ 表情
    ctx.segment.face(66) // 爱心表情

    // 图片
    ctx.segment.image('https://example.com/image.png')
    ctx.segment.image('file:///path/to/image.png')
    ctx.segment.image('base64://...')

    // 语音
    ctx.segment.record('https://example.com/audio.mp3')

    // 视频
    ctx.segment.video('https://example.com/video.mp4')

    // JSON 卡片
    ctx.segment.json('{"app":"com.tencent.xxx",...}')

    // 合并转发
    ctx.segment.forward('转发消息ID')

    // 回复
    ctx.segment.reply('消息ID')

    // 组合发送
    await event.reply([
        ctx.segment.at(event.user_id),
        ' 这是一条测试消息 ',
        ctx.segment.face(66),
        ctx.segment.image('https://example.com/image.png'),
    ])
})

消息匹配

精确匹配

ts
ctx.handle('message', (e) => {
  if (e.raw_message === 'hello') {
    e.reply('mioku')
  }
})

包含匹配

ts
ctx.handle('message', (e) => {
  if (e.raw_message.includes('早上好')) {
    e.reply('不好也可以')
  }
})

使用正则

ts
ctx.handle('message', (e) => {
  const match = e.raw_message.match(/^签到(\d+)?$/)
  if (match) {
    const times = match[1] ? parseInt(match[1]) : 1
    e.reply(`签到 ${times} 次成功`)
  }
})

使用 ctx.match() 函数

ts
ctx.handle('message', (e) => {
    ctx.match(e, {
        // 字符串匹配
        hello: 'world',
        ping: 'pong',

        // 动态回复
        时间: () => new Date().toLocaleString(),

        // 异步回复
        天气: async () => {
            const data = await fetchWeather()
            return `今日天气:${data.weather}`
        },

        // 返回消息段数组
        测试: () => [ctx.segment.text('测试成功 '), ctx.segment.face(66)],

        // 返回 null/undefined/false 则不回复
        静默: () => null,
    })
})

带参数的写法

ts
ctx.handle("message", async (event) => {
  await ctx.match(event, {
    "/echo *": (matches) => {
      // matches[1] 是通配部分
      return `你刚刚说的是:${matches[1]}`;
    }
  });
});

使用 mri 对复杂指令进行解析

ts
ctx.handle('message', (e) => {
    // 使用 mri 解析命令行参数
    const { cmd, params, options } = ctx.createCmd(e.raw_message, {
        prefix: '/',
    })

    if (cmd === 'ban') {
        const [userId, duration] = params
        const reason = options.reason || '违规'
        // 执行禁言
    }

    if (cmd === 'echo') {
        e.reply(params.join(' '))
    }
})

插件清理

setup() 可以返回一个清理函数,在插件卸载时自动执行。

ts
// 启动一个普通定时器
const timer = setInterval(() => {
  ctx.logger.info("定时器运行中");
}, 5000);

// setup 返回的函数会在插件卸载时自动执行
return () => {
  clearInterval(timer);
  ctx.logger.info("插件已卸载,定时器已清理");
};

启用插件

写好插件后,把插件名加入 Mioku 配置里的 plugins 列表。

编辑 config/mioku.json 或前往 WebUI 启用你的插件:

json
{
  "mioki": {
    "plugins": ["boot", "help", "chat", "hello"]
  }
}

测试插件

向机器人发送:

text
/hello

如果收到回复,说明插件已经可以正常运行。

插件示例

以下示例插件均来自mioki官方文档。

ts
import { definePlugin } from 'mioki'

export default definePlugin({
  name: 'repeater',
  version: '1.0.0',
  setup(ctx) {
    ctx.handle('message.group', async (event) => {
      if (event.raw_message === '复读') {
        const lastMessage = event.message
          .filter((m) => m.type === 'text')
          .map((m) => m.text)
          .join('')

        if (lastMessage) {
          await event.reply(lastMessage)
        }
      }
    })
  },
})
ts
import { definePlugin } from 'mioki'

export default definePlugin({
  name: 'welcome',
  version: '1.0.0',
  setup(ctx) {
    ctx.handle('notice.group.increase', async (event) => {
      await event.group.sendMsg([
        ctx.segment.at(event.user_id),
        ' 欢迎加入群聊!请阅读群公告~',
      ])
    })
  },
})
ts
import { definePlugin } from 'mioki'

export default definePlugin({
  name: 'auto-approve',
  version: '1.0.0',
  setup(ctx) {
    // 自动同意好友请求
    ctx.handle('request.friend', async (event) => {
      ctx.logger.info(`自动同意好友请求:${event.user_id}`)
      await event.approve()
    })

    // 自动同意入群申请(包含特定答案)
    ctx.handle('request.group.add', async (event) => {
      if (event.comment.includes('暗号')) {
        ctx.logger.info(`自动同意入群申请:${event.user_id}`)
        await event.approve()
      } else {
        await event.reject('请填写正确的暗号')
      }
    })
  },
})

Released under the MIT License with love.