编写插件

WARNING

插件目前仅仅是作者基于够用就行的思想开发的,仍处于早期阶段,开放能力和 API 都并不完善,且可能发生变动。如果您正准备为 paotuan.io 编写插件,请务必联系我们,一同探讨,以及将您的插件添加到本网站中。

你可以在本节中了解插件的类型定义和支持的 api。你也可以通过插件编写示例来学习插件的编写。

插件的基本结构

插件的文件夹为与程序文件同级的 plugins 文件夹,其中每个子文件夹就是一个插件。

要开始编写插件,你可以在 plugins 文件夹中新建一个文件夹,并起名为插件的名字。

为了防止大家的插件名字冲突,我们推荐使用域名反写的方式为插件取名,例如 com.example.myplugin

每个插件文件夹本质上都是一个 nodejs 项目,至少需要一个 index.js 文件。当然你也可以使用 TypeScript,只是最后需要编译为 JavaScript。

index.js 文件通过 module.exports 暴露一个用于插件注册的函数,大致结构如下:

// 通过 module.exports 暴露一个函数,该函数的签名是:
// (context: IPluginRegisterContext) => IPluginConfig
// 类型的具体定义见 interface/config/plugin.ts 文件
module.exports = (context) => {
  // 插件的 id 应和插件文件夹的名字相同
  return { id: 'com.example.myplugin', ... }
}

IPluginRegisterContext

在插件注册时(程序启动时),主程序会向插件注入一些信息和可供调用的全局 api。

类型签名:

export interface IPluginRegisterContext {
  // 主程序版本号,例如 v1.0.0
  versionName: string
  // 主程序版本 code,例如 12
  versionCode: number
  // 一个简易的投骰方法,支持解析 骰子指令->骰子表达式 一节中的所有语法 
  roll: (exp: string) => DiceRoll
  // mustache#render,但取消了默认的转义
  render: (template: string, view: any, partials?: any) => string
  // 获取玩家关联的人物卡信息
  getCard: (env: Env) => Card | null
  // 获取当前频道关联了人物卡的 user id 列表
  getLinkedCardUserList: (env: Env) => string[]
  // 关联/取消关联人物卡
  linkCard: (env: Env, cardName?: string) => void
  // 根据条件筛选所有已导入的人物卡
  queryCard: (query: ICardQuery) => ICard[]
  // 发送消息
  sendMessage: (env: Env, msg: string, options?: SendMessageOptions) => Promise<IMessage | null>
  // 获取用户偏好设置
  getPreference: (env: Env) => Record<string, string>
  // 模拟用户发起指令
  dispatchUserCommand: (context: ParseUserCommandResult) => Promise<void>
  // lodash 辅助函数
  _: lodash
}

投骰 api

可以使用 roll 方法而非最原始的 Math.random 进行投骰。

该方法接收一个骰子表达式作为参数,支持骰子表达式一节中的所有语法,例如 2d10 d100+d10 2d10kl1 等等。

该方法返回一个 DiceRoll 对象open in new window,包含了本次投骰的信息。有如下常用字段:

const r2d10 = roll('2d10')
r2d10.total // => 13                 // 投骰结果总计
r2d10.rolls // => [8, 5]             // 每次投骰结果
r2d10.output // => 2d10: [8, 5] = 13 // 投骰结果的可读描述

人物卡 api

使用 getCard 方法获取当前消息发送者的人物卡信息。若此人未关联人物卡,返回 null,否则返回 Card 对象。如希望获取其他人的人物卡,将入参 env.userId 替换为对应用户的 userId 即可。

Card 对象上可以使用 getEntry('XX') 方法获取人物卡的条目,内部会处理常见的同义词和 困难/极难 前缀。若条目存在,则返回 CardEntry 对象,可通过 cardEntry.value 获取值。若条目不存在则返回 null

可以使用 setEntry('XX', value) 方法修改人物卡的数值或新增条目。


使用 linkCard 方法为当前消息发送者关联人物卡。若忽略 cardName 参数,代表取消关联此人的人物卡。如希望为其他人操作人物卡关联,将入参 env.userId 替换为对应用户的 userId 即可。

使用 queryCard 方法查询当前已导入的所有人物卡。可查询的条件参数如下:

export interface ICardQuery {
  name?: string // 筛选人物卡名称
  type?: ('general' | 'coc' | 'dnd')[] // 筛选人物卡类型
  isTemplate?: boolean // 是否是人物卡模板
}

消息 api

使用 sendMessage 方法可以发送消息到子频道和私信。

注意:大多数情况下你不需要主动调用这个方法来发消息,自定义回复的处理函数中返回值本身就会作为消息发送。只有遇到纯自定义回复不能满足的场景时,才需要考虑使用这两个方法发消息。

发消息时可指定选项:

type SendMessageOptions = {
  msgType?: 'text' | 'image'
  skipParse?: boolean
}

msgType 非必传,默认为 text 代表文本消息。也可以传 image 发图片,此时 msg 参数就代表图片的 url。

另外如果消息体本身传入的是标签语法,例如 <img src="https://example.com/test.png" />,就不用再指定 msgType 了。

skipParse 默认为 false。默认情况下发送的消息内容会经过一次解析,以支持嵌入骰子指令和人物卡数据(和自定义回复的逻辑相同),但这也有可能对你的正常逻辑造成影响。设为 true 可以跳过这个流程。

注意发送消息会受到 QQ 频道的消息发送限制,例如主动消息不符合时间段、消息审核不通过、图片获取失败等因素都可能造成发送消息失败。

IPluginConfig

返回插件本身的配置。

类型签名:

export interface IPluginConfig {
  // 插件 id,应与插件的文件夹名称相同,例如 com.example.myplugin
  id: string
  // 插件名字,可选,用于展示
  name?: string
  // 插件描述,可选,用于展示
  description?: string
  // 插件版本号,可选
  version?: number
  // 定义用户偏好设置项,可允许用户在插件管理中进行设置
  preference?: {
    // 设置项唯一 key
    key: string
    // 设置项名称
    label?: string
    // 设置项默认值
    defaultValue?: string
  }[]
  // 提供自定义回复能力,可选
  customReply?: ICustomReplyConfig[]
  // 提供指令别名能力,可选
  aliasRoll?: IAliasRollConfig[]
  // 提供自定义文案能力,可选
  customText?: ICustomTextConfig[]
  // 提供 hook 能力,可选
  hook?: IHookFunctionConfig
}

ICustomReplyConfig

提供自定义回复能力。

类型签名:

export interface ICustomReplyConfig {
  // 自定义回复 id。在同一个插件内部需要有唯一性
  id: string
  // 自定义回复名称,用于展示
  name: string
  // 自定义回复功能描述,用于展示
  description?: string
  // 是否默认启用,默认 true
  defaultEnabled?: boolean
  // 触发词
  command: string
  // 触发方式
  trigger: 'exact' | 'startWith' | 'include' | 'regex'
  // 处理自定义回复的方法
  handler: (env: Env, matchGroup: Record<string, string>) => string | Promise<string>
}

自定义回复能力的核心在于 handler 方法,允许我们用任意 JavaScript 代码处理用户输入的内容,并给出回复。

这个方法接收参数 env 代表当前消息的上下文信息,包含用户、频道信息等,通常用于调用上方的全局 api。matchGroup 代表正则表达式的命名捕获组(如有)。

方法返回一个字符串,作为回复到子频道里的消息。除了普通文本消息外,也支持 <img> 语法发送图片消息,例如:

'<img src="https://wiki.connect.qq.com/wp-content/uploads/2013/10/01_qq_logo-1-300x144.png" />'

'<img src="test.png" />'

在发送本地图片时,图片的路径相对于插件的根目录。

这个方法是支持异步的,即可以在处理消息时进行一些耗时操作或调用外部接口等。但处理时间不能超过 5s,否则会受到 QQ 频道的被动消息限制导致发送失败。

IAliasRollConfig

提供指令别名能力。

类型签名:

export type IAliasRollConfig = {
  // 指令别名 id。在同一个插件内部需要有唯一性
  id: string
  // 指令别名名称,用于展示
  name: string
  // 指令别名功能描述,用于展示
  description?: string
  // 是否默认启用,默认 true
  defaultEnabled?: boolean
  // 应用范围
  scope: 'command' | 'expression'
  // 触发方式
  trigger: 'naive' | 'regex' | 'startWith'
  // 触发指令
  command: string
  // 解析后指令
  replacer: string | ((matchResult: RegExpMatchArray) => string)
}

指令别名能力根据 scope 分为两种不同的应用范围。这两种范围之间的区别可以参考指令别名配置部分。

scopecommand 时,trigger 可选 regexstartWith

scopeexpression 时,trigger 可选 naiveregex。其中 naive 代表和界面上的配置相同。

ICustomTextConfig

提供自定义文案能力。

类型签名:

export type ICustomTextHandler = (env: Record<string, any>) => string

export interface ICustomTextConfig {
  // 自定义文案 id。在同一个插件内部需要有唯一性
  id: string
  // 自定义文案名称,用于展示
  name: string
  // 自定义文案功能描述,用于展示
  description?: string
  // 是否默认启用,默认 true
  defaultEnabled?: boolean
  // 自定义文案映射表
  texts: Partial<Record<CustomTextKeys, ICustomTextHandler>>
}

自定义文案的核心是 texts 对象,它是一个 key - value 结构体。

其中的 key,即 CustomTextKeys,代表插件想要处理的某条自定义文案。它的枚举值可以在所有文案词条查看。

value 即是具体的处理方法。接受参数 env 代表可以使用的变量(和界面上的按钮相同),返回字符串作为处理结果。

IHookFunctionConfig

提供 Hook 方法,允许插件钩入指令解析与掷骰逻辑。

类型签名:

export interface IHookFunctionConfig {
  // 接收到指令时触发
  onReceiveCommand?: IHookFunction<OnReceiveCommandCallback>[]
  // 解析骰子指令前触发
  beforeParseDiceRoll?: IHookFunction<BeforeParseDiceRollCallback>[]
  // 人物卡数值变化时触发
  onCardEntryChange?: IHookFunction<OnCardEntryChangeCallback>[]
  // 接收到表情表态时触发
  onMessageReaction?: IHookFunction<OnMessageReactionCallback>[]
  // 掷骰/检定前触发
  beforeDiceRoll?: IHookFunction<BeforeDiceRollCallback>[]
  // 掷骰/检定后触发
  afterDiceRoll?: IHookFunction<AfterDiceRollCallback>[]
}

Hook 目前并不十分完善,后续会补充更多的示例。目前如有需要了解用法,可参考现有的各插件代码。

Last Updated: