编写插件
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 对象,包含了本次投骰的信息。有如下常用字段:
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
分为两种不同的应用范围。这两种范围之间的区别可以参考指令别名配置部分。
当 scope
为 command
时,trigger
可选 regex
或 startWith
。
当 scope
为 expression
时,trigger
可选 naive
或 regex
。其中 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 目前并不十分完善,后续会补充更多的示例。目前如有需要了解用法,可参考现有的各插件代码。