[order: number = 1]` Search games/anime on Bangumi\r\n- `/bgmc` Get today's Bangumi schedule\r\n\r\n```text\r\n/bgm 素晴らしき日々~不連続存在~\r\n> Original Name: 素晴らしき日々~不連続存在~公式ビジュアルアーカイヴ\r\nChinese Name: 素晴之日 不连续的存在 Official Visual Archive\r\nSummary: 人気アダルトゲームブランド「ケロQ」から、実に6年ぶりに発売された新作『素晴らしき日々 ~不連続存在~』。その魅力をギュッと閉じ込めたファン必携の一冊。描き下ろしイラスト&原作を担当したSCA-自(すかぢ)氏の新作書き下ろしテキスト満載でお届け。\r\nTags: 素晴らしき日々 设定集 电波 神作 素晴日 公式书 2010 百合 悬疑 画集・設定資料集 画集 FanBook 推理 fanbook VFB\r\nDetails: https://bgm.tv/subject/8318\r\n[image,https://lain.bgm.tv/pic/cover/l/3f/e2/8318_HbCYx.jpg]\r\n```\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-alias",
+ "description": "用户级别名设置",
+ "category": [
+ "official",
+ "plugin"
+ ],
+ "version": "1.0.2",
+ "author": {
+ "name": "Arimura Sena",
+ "email": "biyuehuya@gmail.com"
+ },
+ "time": {
+ "created": 1708491534668,
+ "modified": 1723122134585
+ },
+ "dist": {
+ "dependencies": 0,
+ "fileCount": 8,
+ "unpackedSize": 41112,
+ "tarball": "https://registry.npmjs.org/@kotori-bot/kotori-plugin-alias/-/kotori-plugin-alias-1.0.2.tgz"
+ },
+ "keywords": [],
+ "readme": "ERROR: No README data found!"
+ },
+ {
+ "name": "@kotori-bot/adapter-slack",
+ "description": "Slack 适配器",
+ "category": [
+ "official",
+ "adapter"
+ ],
+ "version": "1.0.0",
+ "author": {
+ "name": "Arimura Sena",
+ "email": "biyuehuya@gmail.com"
+ },
+ "time": {
+ "created": 1723122122109,
+ "modified": 1723122122846
+ },
+ "dist": {
+ "dependencies": 1,
+ "fileCount": 11,
+ "unpackedSize": 49415,
+ "tarball": "https://registry.npmjs.org/@kotori-bot/adapter-slack/-/adapter-slack-1.0.0.tgz"
+ },
+ "keywords": [
+ "slack"
+ ],
+ "readme": "# @kotori-bot/adapter-slack\n\nSupports for slack. Create own bot: [Getting started](https://slack.dev/bolt-js/getting-started).\n\n## Config\n\n```typescript\nexport const config = Tsu.Object({\n token: Tsu.String().describe(\"Bot's token\"),\n appToken: Tsu.String().describe('Application token (Use for socket connection)'),\n signingSecret: Tsu.String().describe('Signing secret')\n})\n```\n\n## Supports\n\n### Events\n\n- on_message (fully supported)\n\n### Api\n\n- sendPrivateMsg\n- sendGroupMsg\n- sendChannelMsg\n\n### Elements\n\n- text\n- image\n- voice\n- video\n\n## TODO\n\nSupport more standard api...\n\n## Reference\n\n- [Kotori Docs](https://kotori.js.org/)\n- [slack api](https://api.slack.com/docs)\n"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-adapter-sandbox",
+ "description": "模块测试环境,虚拟沙盒适配器",
+ "category": [
+ "official",
+ "adapter"
+ ],
+ "version": "1.1.0",
+ "author": {
+ "name": "Arimura Sena",
+ "email": "biyuehuya@gmail.com"
+ },
+ "time": {
+ "created": 1708491525667,
+ "modified": 1723122116832
+ },
+ "dist": {
+ "dependencies": 0,
+ "fileCount": 13,
+ "unpackedSize": 75294,
+ "tarball": "https://registry.npmjs.org/@kotori-bot/kotori-plugin-adapter-sandbox/-/kotori-plugin-adapter-sandbox-1.1.0.tgz"
+ },
+ "keywords": [],
+ "readme": "# @kotori-bot/kotori-plugin-adapter-onebot\r\n\r\nTo test bot for Kotori.\r\n\r\n## Config\r\n\r\nNo configuration is required.\r\n\r\n## Supports\r\n\r\n### Events\r\n\r\n- on_message (exclude `RequestScope.CHANNEL`)\r\n- on_message_delete (exclude `MessageScope.CHANNEL`)\r\n- on_group_increase\r\n- on_group_decrease\r\n- on_group_whole_ban\r\n- on_friend_decrease\r\n- on_friend_increase\r\n- on_group_ban\r\n- on_group_admin\r\n\r\n### Api\r\n\r\n- sendPrivateMsg\r\n- sendGroupMsg\r\n- deleteMsg\r\n- getUserInfo\r\n- getFriendList\r\n- getGroupInfo\r\n- getGroupList\r\n- getGroupMemberInfo\r\n- getGroupMemberList\r\n- setGroupName\r\n- leaveGroup\r\n- setGroupAdmin\r\n- setGroupCard\r\n- setGroupBan\r\n- setGroupWholeBan\r\n- setGroupKick\r\n\r\n### Elements\r\n\r\n- text\r\n- mention\r\n- mentionAll\r\n- image\r\n- voice\r\n- audio\r\n- video\r\n- reply\r\n- file\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-adapter-qq",
+ "description": "基于 Tencent 官方 API 的适配器",
+ "category": [
+ "official",
+ "adapter"
+ ],
+ "version": "1.2.0",
+ "author": {
+ "name": "Arimura Sena",
+ "email": "biyuehuya@gmail.com"
+ },
+ "time": {
+ "created": 1703848078985,
+ "modified": 1723122112172
+ },
+ "dist": {
+ "dependencies": 2,
+ "fileCount": 15,
+ "unpackedSize": 62708,
+ "tarball": "https://registry.npmjs.org/@kotori-bot/kotori-plugin-adapter-qq/-/kotori-plugin-adapter-qq-1.2.0.tgz"
+ },
+ "keywords": [
+ "qq",
+ "tencent"
+ ],
+ "readme": "# @kotori-bot/kotori-plugin-adapter-onebot\n\nBase on tencent api.\n\n## Config\n\n```typescript\nexport const config = Tsu.Object({\n appid: Tsu.String().describe('Appid, get from https://q.qq.com/qqbot/'),\n secret: Tsu.String().describe(\"Bot's secret \"),\n retry: Tsu.Number().positive().default(10).describe('try reconnect times when disconnected (seconds)')\n})\n```\n\n## Supports\n\n### Events\n\n- on_message\n\n### Api\n\n- sendGroupMsg\n- sendChannelMsg\n\n### Elements\n\n- text\n- image\n- voice\n- mention\n- video\n\n## Reference\n\n- [Kotori Docs](https://kotori.js.org/)\n- [QQ机器人文档](https://bot.q.qq.com/wiki/develop/api-v2/)\n"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-adapter-onebot",
+ "description": "基于 OneBot11 标准的适配器",
+ "category": [
+ "official",
+ "adapter"
+ ],
+ "version": "2.1.0",
+ "author": {
+ "name": "Arimura Sena",
+ "email": "biyuehuya@gmail.com"
+ },
+ "time": {
+ "created": 1703848078986,
+ "modified": 1723122106908
+ },
+ "dist": {
+ "dependencies": 2,
+ "fileCount": 13,
+ "unpackedSize": 75843,
+ "tarball": "https://registry.npmjs.org/@kotori-bot/kotori-plugin-adapter-onebot/-/kotori-plugin-adapter-onebot-2.1.0.tgz"
+ },
+ "keywords": [
+ "onebot",
+ "onebot11",
+ "qq",
+ "liteloader",
+ "LiteLoaderNTQQ",
+ "NapCat",
+ "go-cqhttp"
+ ],
+ "readme": "# @kotori-bot/kotori-plugin-adapter-onebot\n\n![OneBot 11](https://img.shields.io/badge/OneBot-11-black?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHAAAABwCAMAAADxPgR5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAxQTFRF////29vbr6+vAAAAk1hCcwAAAAR0Uk5T////AEAqqfQAAAKcSURBVHja7NrbctswDATQXfD//zlpO7FlmwAWIOnOtNaTM5JwDMa8E+PNFz7g3waJ24fviyDPgfhz8fHP39cBcBL9KoJbQUxjA2iYqHL3FAnvzhL4GtVNUcoSZe6eSHizBcK5LL7dBr2AUZlev1ARRHCljzRALIEog6H3U6bCIyqIZdAT0eBuJYaGiJaHSjmkYIZd+qSGWAQnIaz2OArVnX6vrItQvbhZJtVGB5qX9wKqCMkb9W7aexfCO/rwQRBzsDIsYx4AOz0nhAtWu7bqkEQBO0Pr+Ftjt5fFCUEbm0Sbgdu8WSgJ5NgH2iu46R/o1UcBXJsFusWF/QUaz3RwJMEgngfaGGdSxJkE/Yg4lOBryBiMwvAhZrVMUUvwqU7F05b5WLaUIN4M4hRocQQRnEedgsn7TZB3UCpRrIJwQfqvGwsg18EnI2uSVNC8t+0QmMXogvbPg/xk+Mnw/6kW/rraUlvqgmFreAA09xW5t0AFlHrQZ3CsgvZm0FbHNKyBmheBKIF2cCA8A600aHPmFtRB1XvMsJAiza7LpPog0UJwccKdzw8rdf8MyN2ePYF896LC5hTzdZqxb6VNXInaupARLDNBWgI8spq4T0Qb5H4vWfPmHo8OyB1ito+AysNNz0oglj1U955sjUN9d41LnrX2D/u7eRwxyOaOpfyevCWbTgDEoilsOnu7zsKhjRCsnD/QzhdkYLBLXjiK4f3UWmcx2M7PO21CKVTH84638NTplt6JIQH0ZwCNuiWAfvuLhdrcOYPVO9eW3A67l7hZtgaY9GZo9AFc6cryjoeFBIWeU+npnk/nLE0OxCHL1eQsc1IciehjpJv5mqCsjeopaH6r15/MrxNnVhu7tmcslay2gO2Z1QfcfX0JMACG41/u0RrI9QAAAABJRU5ErkJggg==)\n\nBase on [OneBot 11 Standard](https://github.com/botuniverse/onebot-11), you can use the these programs that support OneBot 11's implement to connect with qq:\n\n- For Linux: [NapCat](https://napneko.github.io/)\n- For Windows: [LiteLoaderQQNT](https://liteloaderqqnt.github.io/) with [LLOneBot](https://llonebot.github.io/)\n- No more available: [go-cqhttp](https://docs.go-cqhttp.org/)\n\n## Config\n\n```typescript\nexport const config = Tsu.Union(\n Tsu.Object({\n mode: Tsu.Literal('ws').describe('Connect mode: WebSocket'),\n port: Tsu.Number().port().describe('WebSocket server port'),\n address: Tsu.String()\n .regexp(/^ws(s)?:\\/\\/([\\w-]+\\.)+[\\w-]+(\\/[\\w-./?%&=]*)?$/)\n .default('ws://127.0.0.1')\n .describe('WebSocket address'),\n retry: Tsu.Number().int().min(1).default(10).describe('try reconnect times when disconnected')\n }),\n Tsu.Object({\n mode: Tsu.Literal('ws-reverse').describe('Connect mode: WebSocket Reverse')\n })\n)\n```\n\n## Supports\n\n### Events\n\n- on_message (exclude `MessageScope.CHANNEL`)\n- on_message_delete (exclude `MessageScope.CHANNEL`)\n- on_request (exclude `RequestScope.CHANNEL`)\n- on_group_increase\n- on_group_decrease\n- on_group_admin\n- on_group_ban\n- custom: onebot_poke\n\n### Api\n\n- sendPrivateMsg\n- sendGroupMsg\n- deleteMsg\n- getUserInfo\n- getFriendList\n- getGroupInfo\n- getGroupList\n- getGroupMemberInfo\n- getGroupMemberList\n- setGroupName\n- leaveGroup\n- setGroupAdmin\n- setGroupCard\n- setGroupAvatar\n- setGroupBan\n- setGroupWholeBan\n- setGroupNotice\n- setGroupKick\n\n### Elements\n\n- text\n- mention\n- mentionAll\n- image\n- voice\n- video\n- reply\n\n## Reference\n\n- [Kotori Docs](https://kotori.js.org/)\n- [go-cqhttp 帮助中心](https://docs.go-cqhttp.org/)\n- [OneBot](https://onebot.dev/)\n"
+ },
+ {
+ "name": "kotori-plugin-adapter-minecraft",
+ "description": "Minecraft 基岩版原生 WebSocket 适配器",
+ "category": [
+ "adapter"
+ ],
+ "version": "2.0.1",
+ "author": {
+ "name": "Arimura Sena",
+ "email": "biyuehuya@gmail.com"
+ },
+ "time": {
+ "created": 1714896768531,
+ "modified": 1723122101932
+ },
+ "dist": {
+ "dependencies": 2,
+ "fileCount": 11,
+ "unpackedSize": 49619,
+ "tarball": "https://registry.npmjs.org/kotori-plugin-adapter-minecraft/-/kotori-plugin-adapter-minecraft-2.0.1.tgz"
+ },
+ "keywords": [
+ "minecraft",
+ "websocket",
+ "minecraft bedrock",
+ "mojang"
+ ],
+ "readme": "# @kotori-bot/kotori-plugin-adapter-minecraft\r\n\r\nBase on `mcwss` package for Minecraft Bedrock Edition, support `private` and `group` scope.\r\n\r\n## Config\r\n\r\n```typescript\r\nexport const config = Tsu.Object({\r\n nickname: Tsu.String().default('Romi').describe('Bot\\'s name'),\r\n template: Tsu.Union(Tsu.Null(), Tsu.String()).default('<%nickname%> %msg%').describe('The template of bot sent message ')\r\n})\r\n```\r\n\r\n## Supports\r\n\r\n### Events\r\n\r\n- on_message (`MessageScope.PRIVATE` and `MessageScope.GROUP`)\r\n\r\n### Api\r\n\r\n- sendPrivateMsg\r\n\r\n### Elements\r\n\r\n- text\r\n- mention\r\n- mentionAll\r\n\r\n## TODO\r\n\r\n### Todo Events\r\n\r\n- on_group_increase\r\n- on_group_decrease\r\n- on_group_ban\r\n\r\n### Todo Api\r\n\r\n- getGroupMemberInfo\r\n- getGroupMemberList\r\n- setGroupAdmin\r\n- setGroupBan\r\n- setGroupKick\r\n\r\n### Other\r\n\r\nAdapter server [LeviLamina](https://github.com/LiteLDev/LeviLamina/) and Java Edition.\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n- [McWss](https://github.com/biyuehu/mcwss)\r\n"
+ },
+ {
+ "name": "@kotori-bot/adapter-mail",
+ "description": "电子邮箱适配器",
+ "category": [
+ "official",
+ "adapter"
+ ],
+ "version": "1.0.0",
+ "author": {
+ "name": "Arimura Sena",
+ "email": "biyuehuya@gmail.com"
+ },
+ "time": {
+ "created": 1723122096213,
+ "modified": 1723122096860
+ },
+ "dist": {
+ "dependencies": 6,
+ "fileCount": 16,
+ "unpackedSize": 61373,
+ "tarball": "https://registry.npmjs.org/@kotori-bot/adapter-mail/-/adapter-mail-1.0.0.tgz"
+ },
+ "keywords": [],
+ "readme": "# @kotori-bot/adapter-mail\n\nSupports for email. Such as `Google Mail`, `QQ Mail`, `163 Mail` and more...\n\n## Config\n\n```typescript\nexport const config = Tsu.Object({\n title: Tsu.String().domain().default('Love from kotori bot mailer').describe('Mail default title'),\n commandEnable: Tsu.Boolean()\n .default(true)\n .describe(\"Whether to enable command, other bot's master can send mail by the command, please set at top mail bot\"),\n forward: Tsu.Array(Tsu.String())\n .default([])\n .describe(\"bots' identity, will forward to the bot's master on receiving mail, please set at top mail bot\"),\n user: Tsu.String().describe('Email address'),\n interval: Tsu.Number().default(60).describe('Check mail interval (seconds)'),\n password: Tsu.String().describe('Email password'),\n imapHost: Tsu.String().describe('IMAP server host'),\n imapPort: Tsu.Number().describe('IMAP server port'),\n smtpHost: Tsu.String().describe('SMTP server host'),\n smtpPort: Tsu.Number().describe('SMTP server port')\n})\n```\n\n## Supports\n\n### Events\n\n- on_message (only `MessageScope.PRIVATE`)\n\n### Api\n\n- sendPrivateMsg\n\n### Elements\n\n- text\n- mention\n- image\n- voice\n- video\n- file\n- reply\n\n## Reference\n\n- [Kotori Docs](https://kotori.js.org/)\n"
+ },
+ {
+ "name": "@kotori-bot/adapter-discord",
+ "description": "Discord 适配器",
+ "category": [
+ "official",
+ "adapter"
+ ],
+ "version": "1.0.0",
+ "author": {
+ "name": "Arimura Sena",
+ "email": "biyuehuya@gmail.com"
+ },
+ "time": {
+ "created": 1723122075270,
+ "modified": 1723122075861
+ },
+ "dist": {
+ "dependencies": 1,
+ "fileCount": 11,
+ "unpackedSize": 48677,
+ "tarball": "https://registry.npmjs.org/@kotori-bot/adapter-discord/-/adapter-discord-1.0.0.tgz"
+ },
+ "keywords": [
+ "discord"
+ ],
+ "readme": "# @kotori-bot/adapter-discord\n\nSupports for discord. Create own bot: [Discord Developer Portal](https://discord.com/developers/applications).\n\n## Config\n\n```typescript\nexport const config = Tsu.Object({\n token: Tsu.String().describe(\"Bot's token\")\n})\n```\n\n## Supports\n\n### Events\n\n- on_message (only `MessageScope.CHANNEL`)\n- on_message_delete (only `MessageScope.CHANNEL`)\n\n### Api\n\n- sendChannelMsg\n\n### Elements\n\n- text\n\n## TODO\n\nSupport more standard api...\n\n## Reference\n\n- [Kotori Docs](https://kotori.js.org/)\n- [discord.js](https://discord.js.org/)\n"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-adapter-cmd",
+ "description": "基于控制台的适配器",
+ "category": [
+ "official",
+ "adapter"
+ ],
+ "version": "1.1.1",
+ "author": {
+ "name": "Arimura Sena",
+ "email": "biyuehuya@gmail.com"
+ },
+ "time": {
+ "created": 1703848078451,
+ "modified": 1723121849869
+ },
+ "dist": {
+ "dependencies": 0,
+ "fileCount": 11,
+ "unpackedSize": 48184,
+ "tarball": "https://registry.npmjs.org/@kotori-bot/kotori-plugin-adapter-cmd/-/kotori-plugin-adapter-cmd-1.1.1.tgz"
+ },
+ "keywords": [],
+ "readme": "# @kotori-bot/kotori-plugin-adapter-cmd\n\nBase on console i/o, a method for quickly testing modules, only support `MessageScope.PRIVATE`.\n\n## Config\n\n```typescript\nexport const config = Tsu.Object({\n nickname: Tsu.String().default('Kotarou').describe('User\\'s nickname'),\n 'self-nickname': Tsu.String().default('KotoriO').describe('Bot\\'s nickname'),\n 'self-id': Tsu.String().default('720').describe('Bot\\'s id'),\n})\n```\n\n## Supports\n\n### Events\n\n- on_message (only `MessageScope.PRIVATE`)\n\n### Api\n\n- sendPrivateMsg\n\n### Elements\n\n- text\n- mention\n- mentionAll\n\n## Reference\n\n- [Kotori Docs](https://kotori.js.org/)\n"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-access",
+ "description": "权限插件,用于设置多个机器人管理员",
+ "category": [
+ "official",
+ "plugin"
+ ],
+ "version": "1.0.2",
+ "author": {
+ "name": "Arimura Sena",
+ "email": "biyuehuya@gmail.com"
+ },
+ "time": {
+ "created": 1708489191006,
+ "modified": 1723121844094
+ },
+ "dist": {
+ "dependencies": 0,
+ "fileCount": 8,
+ "unpackedSize": 41009,
+ "tarball": "https://registry.npmjs.org/@kotori-bot/kotori-plugin-access/-/kotori-plugin-access-1.0.2.tgz"
+ },
+ "keywords": [],
+ "readme": "ERROR: No README data found!"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-status",
+ "description": "查看服务器运行状态",
+ "category": [
+ "official",
+ "plugin"
+ ],
+ "version": "1.0.0",
+ "author": {
+ "name": "Arimura Sena",
+ "email": "biyuehuya@gmail.com"
+ },
+ "time": {
+ "created": 1708491642556,
+ "modified": 1708491643354
+ },
+ "dist": {
+ "dependencies": 0,
+ "fileCount": 9,
+ "unpackedSize": 41389,
+ "tarball": "https://registry.npmjs.org/@kotori-bot/kotori-plugin-status/-/kotori-plugin-status-1.0.0.tgz"
+ },
+ "keywords": [],
+ "readme": ""
+ }
+ ]
+}
\ No newline at end of file
diff --git a/assets/deps.json b/assets/deps.json
new file mode 100644
index 00000000..d335be70
--- /dev/null
+++ b/assets/deps.json
@@ -0,0 +1,12 @@
+{
+ "kotori-bot": "^1.6.4",
+ "@kotori-bot/kotori-plugin-access": "^1.0.2",
+ "@kotori-bot/kotori-plugin-adapter-cmd": "^1.1.1",
+ "@kotori-bot/kotori-plugin-adapter-sandbox": "^1.1.0",
+ "@kotori-bot/kotori-plugin-alias": "^1.0.2",
+ "@kotori-bot/kotori-plugin-core": "^1.4.4",
+ "@kotori-bot/kotori-plugin-filter": "^1.1.0",
+ "@kotori-bot/kotori-plugin-helper": "^1.3.1",
+ "@kotori-bot/kotori-plugin-status": "^1.0.0",
+ "@kotori-bot/kotori-plugin-webui": "^1.4.1"
+}
\ No newline at end of file
diff --git a/assets/guide_base_command.md.B8T0uA6j.js b/assets/guide_base_command.md.B8T0uA6j.js
new file mode 100644
index 00000000..841bdee0
--- /dev/null
+++ b/assets/guide_base_command.md.B8T0uA6j.js
@@ -0,0 +1,204 @@
+import{_ as i,c as a,a0 as h,o as k}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"指令注册","description":"","frontmatter":{},"headers":[],"relativePath":"guide/base/command.md","filePath":"guide/base/command.md","lastUpdated":1723293723000}'),n={name:"guide/base/command.md"};function t(l,s,p,e,d,r){return k(),a("div",null,s[0]||(s[0]=[h(`指令注册 引入 在上一节中学习了事件系统的使用,现在通过 on_message
事件实现一个小功能:
typescript ctx . on ( ' on_message ' , ( session ) => {
+ if ( ! session . message . startsWith ( ' / ' )) return ;
+ const command = session . message . slice ( 1 );
+
+ if ( command === ' echo ' ) {
+ const content = command . slice ( 5 );
+ session . send ( content ? content : ' 输入内容为空 ' );
+ } else if ( command === ' time ' ) {
+ session . send ( \` 现在的时间是 \${ new Date (). getTime () } \` );
+ } else {
+ session . send ( ' 未知的指令 ' );
+ }
+});
当收到「/echo xxx」消息时将发送「xxx」;当收到「/time」消息时将发送当前时间戳;两者都不是时发送「未知的指令」。然而当结果越来越多后,if...else
语句也会越来越多,显然,这是十分糟糕的。尽管可以考虑将条件内容作为键、结果处理包装成回调函数作为值,以键值对形式装进一个对象或者 Map 中,然后遍历执行。但是当条件越来越复杂时,字符串的键远无法满足需求,同时也可能有相当一部分内容仅在私聊或者群聊下可用,其次,参数的处理也需要在结果处理内部中完成,这是十分复杂与繁琐的,因此便有入了本节内容。
基本使用 指令(Command) 是 Kotori 的核心功能,也是最常见的交互方式,指令实质是 Kotori 内部对 on_message
事件的再处理与封装,这点与后续将学习的中间件和正则匹配是一致的,因此也可以看作是一个事件处理的语法糖。通过 ctx.command()
可注册一条指令,参数为指令模板字符,返回 Command
实例对象,实例上有着若干方法用于装饰该指令,其返回值同样为当前指令的实例对象。
typescript ctx . command ( ' echo <...content> ' ). action (( data ) => data . args . join ( ' ' ));
+
+ctx . command ( ' time ' ). action (() => {
+ const time = new Date (). getTime ();
+ return time ;
+});
指令模板字符 typescript ctx . command ( ' bar ' );
+ctx . command ( ' car <arg1> <arg2> ' );
+ctx . command ( ' dar <arg1> [arg2] [arg3=value] ' );
+ctx . command ( ' ear [arg1:number=1] [...args:string] - 指令描述 ' );
上述演示了指令模板字符的基本格式。
尖括号 <>
表示必要参数,方括号 []
为可选参数 括号内部内容格式是 参数名:参数类型
,参数名应为小写字母与数字([a-z0-9])组成,参数类型可省略,默认 string
,支持的类型有: string
、number
、boolean
可选参数中可在参数类型后添加 =值
设置默认参数 参数中可在参数名前添加 ...
设置剩余参数,与 TypeScript 不同的是,剩余参数的类型不需要加上数组表示 在指令模板字符最后添加 - 指令描述
设置指令描述 指令内容为截止到第一个参数出现之前的字符串(不含空格) 参数名应尽量语义化;剩余参数应在所有参数最后面;应仅在可选参数中设置默认参数;必要参数应在可选参数之前;指令模板字符不应包含指令前缀 选项 通过 Command.option()
设置指令选项,接受两个参数:
该选项的缩写名 选项模板字符,可设置多个指令选项 typescript ctx
+ . command ( ' bar ' )
+ . option ( ' S ' , ' speaker - 这是选项的描述 ' )
+ . option ( ' G ' , ' global:boolean - 这是一个布尔类型的选项 ' )
+ . option ( ' T ' , ' time:number - 这是一个数字类型的选项 ' );
一般地,使用单个大写字母作为缩写名,解析器将把字符串中单个连接符 -
开头内容作为选项缩写名解析 使用多个小写字母作为全名(多个单词使用 单个连接符 -
解析),解析器将把字符串中两个连接符 --
开头的内容作为选项全名解析 在选项模板字符最后添加 - 指令描述
设置选项描述 类似地,选项模板字符有着与指令模板字符一样的类型注解方式,默认 string
,支持的类型有: string
、number
、boolean
选项模板字符不支持设置默认值 回调函数 通过 Command.action()
设置指令的回调函数,且每个指令仅可设置一个回调函数,回调函数中接收两个参数:
args
与 options
两个键组成的对象,类型分别为 (string | number | boolean)[]
与 Record<string, string | number | boolean>
,分别代表用户输入的参数值与选项值会话事件数据 options
中的键为对应选项的全名而非缩写名。
typescript ctx . command ( ' bar <...args> ' ). action (( data ) => {
+ ctx . logger . info ( data . args ); // 输出参数值数组
+ ctx . logger . info ( data . options ); // 输出选项值对象
+ session . send ( ' 这是一条消息 ' );
+});
回调函数中的第二个参数为当前会话事件数据 session
。session
对象包含了当前指令触发产生的所有上下文信息,比如消息 id、消息类型、触发指令的账号、Bot 实例等,在处理函数中可以方便地与 Bot 进行交互。还可以从 session
中获取诸如发送消息等实用工具函数。下文将详细讲解 session
对象相关内容,此处仅做演示。
typescript ctx . command ( ' at ' ). action (( _ , session ) => {
+ session . send ( \` 你好, \${ session . el . at ( session . userId ) } ,你的名字是 \${ session . sender . nickname } \` );
+});
作用域 通过 Command.scope()
设置指令作用域,值类型为 MessageScope
,如若不设置则默认所有场景均可使用。
typescript export enum MessageScope {
+ PRIVATE , // 私聊
+ GROUP // 群聊
+}
typescript ctx
+ . command ( ' test ' )
+ . scope ( MessageScope . PRIVATE )
+ . action (() => ' 这是一条仅私聊可用的消息 ' );
+
+ctx
+ . command ( ' hello ' )
+ . scope ( MessageScope . GROUP )
+ . action (( _ , session ) => {
+ session . send ( \` 这是一条仅群聊可用的消息 \` );
+ });
别名 通过 Command.alias()
设置指令别名,参数为 string | string[]
。
typescript ctx
+ . command ( ' original ' )
+ . alias ( ' o ' ) // 别名可以是单个字符串
+ . alias ([ ' ori ' , ' org ' ]) // 也可以是字符串数组
+ . action (() => ' 这是原版指令 ' );
权限 通过 Command.access()
设置指令权限,值类型为 CommandAccess
。
typescript export const enum CommandAccess {
+ MEMBER , // 所有成员可用,默认值,权限最低
+ MANGER , // 管理员(群管理员/群主或 Bot 管理员)及以上权限可用
+ ADMIN // 仅该 Bot 最高管理员可用
+}
CommandAccess.ADMIN
对应 kotori.yml
中的 AdapterConfig.master
选项
typescript ctx
+ . command ( ' op ' )
+ . access ( CommandAccess . ADMIN )
+ . action (() => ' 这是一条特殊指令 ' );
帮助信息 通过 Command.help()
设置指令帮助信息,相对于指令模板字符中的指令描述,其提供更为详尽全面的信息。
typescript ctx . command ( ' bar ' ). help ( ' 这里是指令的帮助信息 ' );
返回值处理 在上述众多演示中,可能你已注意到,与事件系统不同,指令的回调函数可以直接返回一个值作为消息发出,而不必使用 session.send()
方法。其本质上是自动将回调函数返回值作为参数传入 session.quick()
方法,具体处理逻辑请参考下文。
typescript ctx . command ( ' concat <str1> <str2> ' ). action (({ args : [ str1 , str2 ] }) => str1 + str2 );
+
+ctx . command ( ' render ' ). action (() => [ ' template.text ' , { content : ' 这是模板内容 ' }]);
+
+ctx . command ( ' fetch ' ). action ( async () => {
+ const res = await ctx . http . get ( ' https://api.example.com ' );
+ return [ ' template.status ' , { status : res . status }];
+});
对于返回数组的情况设计国际化相关内容,将在第三章中讲解
子指令 试想一下,有一个指令 list
有着多个操作,如查询、添加、删除列表等,大可以使用多个完全独立的指令如 list_query
、list_add
、list_remove
,但这并不优雅。此处通过注册一个指令并判断其第一个参数的值执行相应操作
typescript /* 错误示例不要抄 */
+ctx . command ( ' black query - manger.descr.black.query ' ). action (({ args }, session ) => {
+ switch ( args [ 0 ]) {
+ case ' query ' :
+ /* ... */
+ break ;
+ case ' add ' :
+ /* ... */
+ break ;
+ case ' remove ' :
+ /* ... */
+ break ;
+ default :
+ return \` 无效的参数 \${ args [ 0 ] } \` ;
+ }
+ /* ... */
+});
但是,其需要判断 args[0]
并处理无效时的情况,额外的代码嵌套依旧不够优雅。且多个操作下对于参数个数要求不一,如查询可以直接输入 list query
,但对于添加/删除往往需要在后方再传入一个参数以指定添加/删除目标 list add xxx
。因此,当同一指令有多个操作(即多个指令回调函数)且各个操作间相对独立时可使用子指令。基础用法:
typescript ctx . command ( ' cmd sub1 ' ). action (() => ' 操作1 ' );
+ctx . command ( ' cmd sub2 ' ). action (() => ' 操作2 ' );
+
+// 甚至可以支持嵌套子指令...
+ctx . command ( ' cmd sub3 sub1 ' ). action (() => ' 操作3的操作1 ' );
+ctx . command ( ' cmd sub3 sub2 ' ). action (() => ' 操作3的第二个操作 ' );
+
+// 多个不同子指令间可设置不同的权限、作用域等,互不影响
+ctx
+ . command ( ' cmd sub4 group ' )
+ . action (() => ' 这个子指令仅群聊可用 ' )
+ . scope ( MessageScope . GROUP );
+
+ctx
+ . command ( ' cmd sub4 manger ' )
+ . action (() => ' 这个子指令仅管理员可用 ' )
+ . access ( CommandAccess . MANGER );
+
+ ctx
+ . command ( ' cmd sub4 ADMIN_private ' )
+ . action (() => ' 这个子指令仅最高管理员且在私聊下可用 ' )
+ . access ( CommandAccess . ADMIN ),
+ . scope ( MessageScope . PRIVATE );
使用子指令实现 list
指令:
typescript ctx . command ( ' list query - 查询列表 ' ). action (() => {
+ /* ... */
+});
+
+ctx
+ . command ( ' list add <target> - 从列表中添加指定目标 ' )
+ . action (() => {
+ /* ... */
+ })
+ . access ( CommandAccess . MANGER );
+
+ctx
+ . command ( ' list remove <target> - 从列表中删除指定目标 ' )
+ . action (() => {
+ /* ... */
+ })
+ . access ( CommandAccess . MANGER );
会话事件数据 上一节的会话事件部分和本节中均提到了会话事件数据 session
,又或是后面的中间件与正则匹配,都会有着它的身影。而指令系统作为 Kotori 中使用最广泛的功能且当前你已掌握事件系统的概念,会话事件数据的内容得以放在此处进行详细讲解。
重要属性 typescript export interface EventDataApiBase {
+ type ? : MessageScope ;
+ api : Api ;
+ el : Elements ;
+ userId : EventDataTargetId ;
+ groupId ? : EventDataTargetId ;
+ operatorId ? : EventDataTargetId ;
+ i18n : I18n ;
+ send ( message : MessageRaw ): void ;
+ format ( template : string , data : Record < string , unknown > | CommandArgType []): string ;
+ quick ( message : MessageQuick ): void ;
+ prompt ( message ? : MessageRaw ): Promise < MessageRaw >;
+ confirm ( options ? : { message : MessageRaw ; sure : MessageRaw }): Promise < boolean >;
+ error < T extends Exclude < keyof CommandResult , CommandResultNoArgs >>(
+ type : T ,
+ data : CommandResult [ T ] extends object ? CommandResult [ T ] : never
+ ): CommandError ;
+ error < T extends CommandResultNoArgs >( type : T ): CommandError ;
+ extra ? : unknown ;
+}
session
对象本质上就是一个事件数据对象(即会话事件),上述是会话事件的共有属性,不同会话事件中有着不同的额外属性,如 EventDataGroupMsg
事件有 messageId
、sender
、message
、groupId
,而 EventDataPrivateMsg
事件没有 groupId
,EventDataPrivateRecall
事件其中的仅有 messageId
,这些额外属性均不在当前讨论范围内,具体内容参考接口文档。对于上述的共有属性在当前阶段也不必全部掌握。
api: Api
实例对象,提供多个与当前聊天平台的交互接口 el: Elements
实例对象,api.adapter.elements
属性的语法糖 i18n: 国际化相关方法 字符串处理 typescript export type CommandArgType = string | number | boolean ;
+type ObjectArgs = Record < string , CommandArgType >;
+type ArrayArgs = CommandArgType [];
session.format()
方法是一个简单的模板字符串替换工具(此处请区别于 JavaScript 中的 「模板字符串」)。接收两个参数:
源字符串 模板字符串参数,其类型有两种,分别为 ObjectArgs
、ArrayArgs
。 typescript ctx . command ( ' himeki ' ). action (( _ , { format }) => {
+ format ( ' 名字:%name% \\n 身高:%height%cm \\n 口头禅:%msg% ' , {
+ name : ' Ichinose Himeki ' ,
+ height : 153 ,
+ msg : ' 最喜欢你了,欧尼酱 '
+ });
+ // 等同于:
+ format ( ' 名字:{0} \\n 身高:{1}cm \\n 口头禅:{2} ' , [ ' Ichinose Himeki ' , 153 , ' 最喜欢你了,欧尼酱 ' ]);
+ // 最终输出:名字:Ichinose Himeki\\n年龄:153\\n口头禅:最喜欢你了,欧尼酱
+});
通过上述代码可知:
对象模板:通过 %key%
的形式进行替换,与对象键值一一对应,其更具有语义性,适合文本长且参数较多使用,但使用过多易造成代码冗余 数组模板:通过 {index}
的形式进行替换,与数组索引一一对应,缺少语义性但更简洁,适合短文本使用,不易造成代码冗余 模板字符串替换适合动态获取数据后呈现数据 适当的对模板字符串参数嵌套使用 session.format()
可实现较为复杂的动态数据展示,但不宜过多 消息发送 在上一节已提到 session.send()
方法是对 session.api
上发送消息方法的封装,而 session.quick()
方法则是对 session.send()
的封装。一般地,在有会话事件数据可使用且无特殊需求下,均推荐使用 session.quick()
,后续所有代码演示无特殊情况也默认使用该方法。
对于 string
将调用 i18n.locale()
方法实现国际化 对于 [string, ObjectArgs | ArrayArgs]
参数,将先遍历数组中第二个值下的所有属性并调用 i18n.locale()
进行替换,然后将其传入 session.format()
方法 对于 undefined
、''
、void
、null
、0
则不作处理(一般不允许传入这些东西,主要发生在指令处理的回调函数返回值上) 对于 Error
则另作处理(一般不允许传入这些东西,主要发生在指令处理的回调函数返回值上) 对于 Promise
则等待 Promise 完成后再做上述处理 关于 i18n.locale()
方法当前可粗略理解为:传入一个已预定好且唯一的字符串值,根据当前使用语言返回相应语言文本。当然,不理解并不妨碍你使用 session.quick()
方法
json // locales/zh_CN.json
+{
+ " test.msg.himeki.hitokoto " : " 最喜欢你了,欧尼酱 " ,
+ " test.msg.himeki " : " 名字:{0} \\n 身高:{1}cm \\n 口头禅:{2} "
+}
typescript // src/index.ts
+// 告诉 Kotori 自动加载国际化文件
+export const lang = [ __dirname , ' ../locales ' ];
+
+export function ( ctx : Context ) {
+ ctx . command ( ' himeki ' ). action (( _ , session ) => {
+ // 使用 session.send():
+ const hitokoto = session . i18n . locale ( ' test.msg.himeki.hitokoto ' );
+ const msg = session . format ( session . i18n . locale ( ' test.msg.himeki ' ), [ ' Ichinose Himeki ' , 153 , hitokoto ]);
+ session . send ( msg );
+ // 使用 session.quick():
+ session . quick ([ ' test.msg.himeki ' , [ ' Ichinose Himeki ' , 153 , ' test.msg.himeki.hitokoto ' ]]);
+ // 直接返回:
+ return [ ' test.msg.himeki ' , [ ' Ichinose Himeki ' , 153 , ' test.msg.himeki.hitokoto ' ]];
+ });
+}
会话交互 目前 Kotori 原生提供了两个会话交互方法:session.prompt()
与 session.confirm()
,它们和浏览器中的 prompt()
与 confirm()
类似,分别对应为输入框和提示框。
typescript ctx . command ( ' question ' ). action ( async ( _ , session ) => {
+ await session . quick ( ' 这里有一个问题想问你... ' );
+ const likeme = await session . confirm ({
+ message : ' 你喜欢我吗? ' ,
+ sure : ' 喜欢 '
+ });
+ if ( ! likeme ) return ' 伤透了我的心 ' ;
+ const ago = Number ( await session . prompt ( ' 喜欢我多久了? ' ));
+ if ( Number . isNaN ( ago ) || ago < 0 ) return ' 这可不是一个有效的Number啊! ' ;
+ await session . quick ( ago >= 0 && ago <= 1 ? ' 什么嘛...原来才刚刚开始喜欢啊 ' : \` 居然喜欢了 \${ ago } 这么久啊! \` );
+ return ' 谢谢你的喜欢哦~ ' ;
+});
WARNING
一次性有多个会话交互(消息、输入、确认...)时请注意不用遗漏 await
关键词,否则可能会有一些意料之外的效果。
两者参数均只有一个且可选 session.prompt()
参数为 string
,对应提示消息,返回 Promise<string>
session.confirm()
参数为 { message: string, sure: string }
,分别对应提示消息和确认消息(只有用户发送消息与确认消息完全一致时返回 true
反之 false
),返回 Promise<boolean>
NOTE
目前会话交互功能甚少,内容也不全面,如对 i18n 支持不够完善、需手动进行数据校验、Promise 超时等问题,如有能力欢迎你前来帮助 Kotori 完善。
错误处理 随着功能的不断增多,不稳定性也随之增多,面对用户传入的各种奇怪数据,虽有着 Kotori 本身的指令参数和数据校验用于防护,但这并不能百分百避免所有错误发生,因此学会自行错误处理至关重要。以下是 Kotori 内置的指令错误类型可供参考:
typescript type CommandArgTypeSign = ' string ' | ' number ' | ' boolean ' ;
+
+interface CommandParseResult {
+ option_error : { expected : CommandArgTypeSign ; reality : CommandArgTypeSign ; target : string }; // 选项类型错误
+ arg_error : { expected : CommandArgTypeSign ; reality : CommandArgTypeSign ; index : number }; // 参数类型错误
+ arg_many : { expected : number ; reality : number }; // 参数过多
+ arg_few : CommandParseResult [ ' arg_many ' ]; // 参数过少
+ syntax : { index : number ; char : string }; // 语法错误(引号、反斜杠问题)
+ unknown : { input : string }; // 未知的指令
+}
+
+export interface CommandResult extends CommandParseResult {
+ error : { error : unknown }; // 未知错误
+ data_error : { target : string | number };
+ res_error : { error : TsuError };
+ num_error : null ;
+ no_access_manger : null ; // 无管理员权限
+ no_access_admin : null ; // 无最高管理员权限
+ disable : null ;
+ exists : { target : string };
+ no_exists : CommandResult [ ' exists ' ];
+}
Kotori 中指令指令错误分为两大类:
指令解析时错误:即上述的 CommandParseResult
,这些在指令系统不需要你操心,因为它们已全部交由上游的 Kotori 内置中间件进行处理,在解析指令时就会被发现 指令运行时错误:即上述的 Omit<CommandResult, keyof CommandParseResult>
,它们有的发生在指令执行前(如 no_access_manger
、no_access_admin
),又或者 error
这种错误之外的错误(执行回调函数时捕获的错误),这两者也不需要你操心 需要操心的是剩下可能发生在指令执行期间的错误,这些错误无法由 Kotori 处理,全需要你在编写代码时手动处理:
data_error
参数错误(不同于参数类型错误)res_error
资源错误(主要是指网络请求第三方 Api 时返回数据类型有误)num_error
序号错误(主要是指需要用户传入数字进行选择的情况)exists
目标已存在(如添加目标到名单里但目标已存在于名单)no_exists
目标不存在(如删除目标从名单里但目标不存在于名单)使用 session.error()
方法即可在运行时阶段抛出错误,
typescript export function main ( ctx : Context ) {
+ ctx . command ( ' hitokoto ' ). action ( async ( _ , session ) => {
+ const res = await ctx . http . get ( ' https://hotaru.icu/api/hitokoto/v2/ ' );
+ /* 这里有一些检查数据的操作 */
+ if ( condition ) return session . error ( ' res ' , { error : new Error () }); // 这里会有些小问题,代码仅做演示,请勿照抄
+ return [ ' 今日一言: {0}{1} ' , [ res . data , res . data . from ? \` —— \${ res . data . from } \` : '' ]];
+ });
+}
上述代码展示了其非常经典的一个例子,机器人的功能往往部分来自于网络接口请求,确保其第三方内容的稳定性更是必要的,因此对获取的数据进行检查,然后再进行访问属性操作,如若获取的数据与预期不一致则使用 session.error()
抛出错误
ctx.http
是一个网络请求工具,基于 Axios 封装,具体内容参考接口文档;此处的「检查数据的操作」实际上指 Schema,这将在第三章中讲解
`,86)]))}const A=i(n,[["render",t]]);export{y as __pageData,A as default};
diff --git a/assets/guide_base_command.md.B8T0uA6j.lean.js b/assets/guide_base_command.md.B8T0uA6j.lean.js
new file mode 100644
index 00000000..841bdee0
--- /dev/null
+++ b/assets/guide_base_command.md.B8T0uA6j.lean.js
@@ -0,0 +1,204 @@
+import{_ as i,c as a,a0 as h,o as k}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"指令注册","description":"","frontmatter":{},"headers":[],"relativePath":"guide/base/command.md","filePath":"guide/base/command.md","lastUpdated":1723293723000}'),n={name:"guide/base/command.md"};function t(l,s,p,e,d,r){return k(),a("div",null,s[0]||(s[0]=[h(`指令注册 引入 在上一节中学习了事件系统的使用,现在通过 on_message
事件实现一个小功能:
typescript ctx . on ( ' on_message ' , ( session ) => {
+ if ( ! session . message . startsWith ( ' / ' )) return ;
+ const command = session . message . slice ( 1 );
+
+ if ( command === ' echo ' ) {
+ const content = command . slice ( 5 );
+ session . send ( content ? content : ' 输入内容为空 ' );
+ } else if ( command === ' time ' ) {
+ session . send ( \` 现在的时间是 \${ new Date (). getTime () } \` );
+ } else {
+ session . send ( ' 未知的指令 ' );
+ }
+});
当收到「/echo xxx」消息时将发送「xxx」;当收到「/time」消息时将发送当前时间戳;两者都不是时发送「未知的指令」。然而当结果越来越多后,if...else
语句也会越来越多,显然,这是十分糟糕的。尽管可以考虑将条件内容作为键、结果处理包装成回调函数作为值,以键值对形式装进一个对象或者 Map 中,然后遍历执行。但是当条件越来越复杂时,字符串的键远无法满足需求,同时也可能有相当一部分内容仅在私聊或者群聊下可用,其次,参数的处理也需要在结果处理内部中完成,这是十分复杂与繁琐的,因此便有入了本节内容。
基本使用 指令(Command) 是 Kotori 的核心功能,也是最常见的交互方式,指令实质是 Kotori 内部对 on_message
事件的再处理与封装,这点与后续将学习的中间件和正则匹配是一致的,因此也可以看作是一个事件处理的语法糖。通过 ctx.command()
可注册一条指令,参数为指令模板字符,返回 Command
实例对象,实例上有着若干方法用于装饰该指令,其返回值同样为当前指令的实例对象。
typescript ctx . command ( ' echo <...content> ' ). action (( data ) => data . args . join ( ' ' ));
+
+ctx . command ( ' time ' ). action (() => {
+ const time = new Date (). getTime ();
+ return time ;
+});
指令模板字符 typescript ctx . command ( ' bar ' );
+ctx . command ( ' car <arg1> <arg2> ' );
+ctx . command ( ' dar <arg1> [arg2] [arg3=value] ' );
+ctx . command ( ' ear [arg1:number=1] [...args:string] - 指令描述 ' );
上述演示了指令模板字符的基本格式。
尖括号 <>
表示必要参数,方括号 []
为可选参数 括号内部内容格式是 参数名:参数类型
,参数名应为小写字母与数字([a-z0-9])组成,参数类型可省略,默认 string
,支持的类型有: string
、number
、boolean
可选参数中可在参数类型后添加 =值
设置默认参数 参数中可在参数名前添加 ...
设置剩余参数,与 TypeScript 不同的是,剩余参数的类型不需要加上数组表示 在指令模板字符最后添加 - 指令描述
设置指令描述 指令内容为截止到第一个参数出现之前的字符串(不含空格) 参数名应尽量语义化;剩余参数应在所有参数最后面;应仅在可选参数中设置默认参数;必要参数应在可选参数之前;指令模板字符不应包含指令前缀 选项 通过 Command.option()
设置指令选项,接受两个参数:
该选项的缩写名 选项模板字符,可设置多个指令选项 typescript ctx
+ . command ( ' bar ' )
+ . option ( ' S ' , ' speaker - 这是选项的描述 ' )
+ . option ( ' G ' , ' global:boolean - 这是一个布尔类型的选项 ' )
+ . option ( ' T ' , ' time:number - 这是一个数字类型的选项 ' );
一般地,使用单个大写字母作为缩写名,解析器将把字符串中单个连接符 -
开头内容作为选项缩写名解析 使用多个小写字母作为全名(多个单词使用 单个连接符 -
解析),解析器将把字符串中两个连接符 --
开头的内容作为选项全名解析 在选项模板字符最后添加 - 指令描述
设置选项描述 类似地,选项模板字符有着与指令模板字符一样的类型注解方式,默认 string
,支持的类型有: string
、number
、boolean
选项模板字符不支持设置默认值 回调函数 通过 Command.action()
设置指令的回调函数,且每个指令仅可设置一个回调函数,回调函数中接收两个参数:
args
与 options
两个键组成的对象,类型分别为 (string | number | boolean)[]
与 Record<string, string | number | boolean>
,分别代表用户输入的参数值与选项值会话事件数据 options
中的键为对应选项的全名而非缩写名。
typescript ctx . command ( ' bar <...args> ' ). action (( data ) => {
+ ctx . logger . info ( data . args ); // 输出参数值数组
+ ctx . logger . info ( data . options ); // 输出选项值对象
+ session . send ( ' 这是一条消息 ' );
+});
回调函数中的第二个参数为当前会话事件数据 session
。session
对象包含了当前指令触发产生的所有上下文信息,比如消息 id、消息类型、触发指令的账号、Bot 实例等,在处理函数中可以方便地与 Bot 进行交互。还可以从 session
中获取诸如发送消息等实用工具函数。下文将详细讲解 session
对象相关内容,此处仅做演示。
typescript ctx . command ( ' at ' ). action (( _ , session ) => {
+ session . send ( \` 你好, \${ session . el . at ( session . userId ) } ,你的名字是 \${ session . sender . nickname } \` );
+});
作用域 通过 Command.scope()
设置指令作用域,值类型为 MessageScope
,如若不设置则默认所有场景均可使用。
typescript export enum MessageScope {
+ PRIVATE , // 私聊
+ GROUP // 群聊
+}
typescript ctx
+ . command ( ' test ' )
+ . scope ( MessageScope . PRIVATE )
+ . action (() => ' 这是一条仅私聊可用的消息 ' );
+
+ctx
+ . command ( ' hello ' )
+ . scope ( MessageScope . GROUP )
+ . action (( _ , session ) => {
+ session . send ( \` 这是一条仅群聊可用的消息 \` );
+ });
别名 通过 Command.alias()
设置指令别名,参数为 string | string[]
。
typescript ctx
+ . command ( ' original ' )
+ . alias ( ' o ' ) // 别名可以是单个字符串
+ . alias ([ ' ori ' , ' org ' ]) // 也可以是字符串数组
+ . action (() => ' 这是原版指令 ' );
权限 通过 Command.access()
设置指令权限,值类型为 CommandAccess
。
typescript export const enum CommandAccess {
+ MEMBER , // 所有成员可用,默认值,权限最低
+ MANGER , // 管理员(群管理员/群主或 Bot 管理员)及以上权限可用
+ ADMIN // 仅该 Bot 最高管理员可用
+}
CommandAccess.ADMIN
对应 kotori.yml
中的 AdapterConfig.master
选项
typescript ctx
+ . command ( ' op ' )
+ . access ( CommandAccess . ADMIN )
+ . action (() => ' 这是一条特殊指令 ' );
帮助信息 通过 Command.help()
设置指令帮助信息,相对于指令模板字符中的指令描述,其提供更为详尽全面的信息。
typescript ctx . command ( ' bar ' ). help ( ' 这里是指令的帮助信息 ' );
返回值处理 在上述众多演示中,可能你已注意到,与事件系统不同,指令的回调函数可以直接返回一个值作为消息发出,而不必使用 session.send()
方法。其本质上是自动将回调函数返回值作为参数传入 session.quick()
方法,具体处理逻辑请参考下文。
typescript ctx . command ( ' concat <str1> <str2> ' ). action (({ args : [ str1 , str2 ] }) => str1 + str2 );
+
+ctx . command ( ' render ' ). action (() => [ ' template.text ' , { content : ' 这是模板内容 ' }]);
+
+ctx . command ( ' fetch ' ). action ( async () => {
+ const res = await ctx . http . get ( ' https://api.example.com ' );
+ return [ ' template.status ' , { status : res . status }];
+});
对于返回数组的情况设计国际化相关内容,将在第三章中讲解
子指令 试想一下,有一个指令 list
有着多个操作,如查询、添加、删除列表等,大可以使用多个完全独立的指令如 list_query
、list_add
、list_remove
,但这并不优雅。此处通过注册一个指令并判断其第一个参数的值执行相应操作
typescript /* 错误示例不要抄 */
+ctx . command ( ' black query - manger.descr.black.query ' ). action (({ args }, session ) => {
+ switch ( args [ 0 ]) {
+ case ' query ' :
+ /* ... */
+ break ;
+ case ' add ' :
+ /* ... */
+ break ;
+ case ' remove ' :
+ /* ... */
+ break ;
+ default :
+ return \` 无效的参数 \${ args [ 0 ] } \` ;
+ }
+ /* ... */
+});
但是,其需要判断 args[0]
并处理无效时的情况,额外的代码嵌套依旧不够优雅。且多个操作下对于参数个数要求不一,如查询可以直接输入 list query
,但对于添加/删除往往需要在后方再传入一个参数以指定添加/删除目标 list add xxx
。因此,当同一指令有多个操作(即多个指令回调函数)且各个操作间相对独立时可使用子指令。基础用法:
typescript ctx . command ( ' cmd sub1 ' ). action (() => ' 操作1 ' );
+ctx . command ( ' cmd sub2 ' ). action (() => ' 操作2 ' );
+
+// 甚至可以支持嵌套子指令...
+ctx . command ( ' cmd sub3 sub1 ' ). action (() => ' 操作3的操作1 ' );
+ctx . command ( ' cmd sub3 sub2 ' ). action (() => ' 操作3的第二个操作 ' );
+
+// 多个不同子指令间可设置不同的权限、作用域等,互不影响
+ctx
+ . command ( ' cmd sub4 group ' )
+ . action (() => ' 这个子指令仅群聊可用 ' )
+ . scope ( MessageScope . GROUP );
+
+ctx
+ . command ( ' cmd sub4 manger ' )
+ . action (() => ' 这个子指令仅管理员可用 ' )
+ . access ( CommandAccess . MANGER );
+
+ ctx
+ . command ( ' cmd sub4 ADMIN_private ' )
+ . action (() => ' 这个子指令仅最高管理员且在私聊下可用 ' )
+ . access ( CommandAccess . ADMIN ),
+ . scope ( MessageScope . PRIVATE );
使用子指令实现 list
指令:
typescript ctx . command ( ' list query - 查询列表 ' ). action (() => {
+ /* ... */
+});
+
+ctx
+ . command ( ' list add <target> - 从列表中添加指定目标 ' )
+ . action (() => {
+ /* ... */
+ })
+ . access ( CommandAccess . MANGER );
+
+ctx
+ . command ( ' list remove <target> - 从列表中删除指定目标 ' )
+ . action (() => {
+ /* ... */
+ })
+ . access ( CommandAccess . MANGER );
会话事件数据 上一节的会话事件部分和本节中均提到了会话事件数据 session
,又或是后面的中间件与正则匹配,都会有着它的身影。而指令系统作为 Kotori 中使用最广泛的功能且当前你已掌握事件系统的概念,会话事件数据的内容得以放在此处进行详细讲解。
重要属性 typescript export interface EventDataApiBase {
+ type ? : MessageScope ;
+ api : Api ;
+ el : Elements ;
+ userId : EventDataTargetId ;
+ groupId ? : EventDataTargetId ;
+ operatorId ? : EventDataTargetId ;
+ i18n : I18n ;
+ send ( message : MessageRaw ): void ;
+ format ( template : string , data : Record < string , unknown > | CommandArgType []): string ;
+ quick ( message : MessageQuick ): void ;
+ prompt ( message ? : MessageRaw ): Promise < MessageRaw >;
+ confirm ( options ? : { message : MessageRaw ; sure : MessageRaw }): Promise < boolean >;
+ error < T extends Exclude < keyof CommandResult , CommandResultNoArgs >>(
+ type : T ,
+ data : CommandResult [ T ] extends object ? CommandResult [ T ] : never
+ ): CommandError ;
+ error < T extends CommandResultNoArgs >( type : T ): CommandError ;
+ extra ? : unknown ;
+}
session
对象本质上就是一个事件数据对象(即会话事件),上述是会话事件的共有属性,不同会话事件中有着不同的额外属性,如 EventDataGroupMsg
事件有 messageId
、sender
、message
、groupId
,而 EventDataPrivateMsg
事件没有 groupId
,EventDataPrivateRecall
事件其中的仅有 messageId
,这些额外属性均不在当前讨论范围内,具体内容参考接口文档。对于上述的共有属性在当前阶段也不必全部掌握。
api: Api
实例对象,提供多个与当前聊天平台的交互接口 el: Elements
实例对象,api.adapter.elements
属性的语法糖 i18n: 国际化相关方法 字符串处理 typescript export type CommandArgType = string | number | boolean ;
+type ObjectArgs = Record < string , CommandArgType >;
+type ArrayArgs = CommandArgType [];
session.format()
方法是一个简单的模板字符串替换工具(此处请区别于 JavaScript 中的 「模板字符串」)。接收两个参数:
源字符串 模板字符串参数,其类型有两种,分别为 ObjectArgs
、ArrayArgs
。 typescript ctx . command ( ' himeki ' ). action (( _ , { format }) => {
+ format ( ' 名字:%name% \\n 身高:%height%cm \\n 口头禅:%msg% ' , {
+ name : ' Ichinose Himeki ' ,
+ height : 153 ,
+ msg : ' 最喜欢你了,欧尼酱 '
+ });
+ // 等同于:
+ format ( ' 名字:{0} \\n 身高:{1}cm \\n 口头禅:{2} ' , [ ' Ichinose Himeki ' , 153 , ' 最喜欢你了,欧尼酱 ' ]);
+ // 最终输出:名字:Ichinose Himeki\\n年龄:153\\n口头禅:最喜欢你了,欧尼酱
+});
通过上述代码可知:
对象模板:通过 %key%
的形式进行替换,与对象键值一一对应,其更具有语义性,适合文本长且参数较多使用,但使用过多易造成代码冗余 数组模板:通过 {index}
的形式进行替换,与数组索引一一对应,缺少语义性但更简洁,适合短文本使用,不易造成代码冗余 模板字符串替换适合动态获取数据后呈现数据 适当的对模板字符串参数嵌套使用 session.format()
可实现较为复杂的动态数据展示,但不宜过多 消息发送 在上一节已提到 session.send()
方法是对 session.api
上发送消息方法的封装,而 session.quick()
方法则是对 session.send()
的封装。一般地,在有会话事件数据可使用且无特殊需求下,均推荐使用 session.quick()
,后续所有代码演示无特殊情况也默认使用该方法。
对于 string
将调用 i18n.locale()
方法实现国际化 对于 [string, ObjectArgs | ArrayArgs]
参数,将先遍历数组中第二个值下的所有属性并调用 i18n.locale()
进行替换,然后将其传入 session.format()
方法 对于 undefined
、''
、void
、null
、0
则不作处理(一般不允许传入这些东西,主要发生在指令处理的回调函数返回值上) 对于 Error
则另作处理(一般不允许传入这些东西,主要发生在指令处理的回调函数返回值上) 对于 Promise
则等待 Promise 完成后再做上述处理 关于 i18n.locale()
方法当前可粗略理解为:传入一个已预定好且唯一的字符串值,根据当前使用语言返回相应语言文本。当然,不理解并不妨碍你使用 session.quick()
方法
json // locales/zh_CN.json
+{
+ " test.msg.himeki.hitokoto " : " 最喜欢你了,欧尼酱 " ,
+ " test.msg.himeki " : " 名字:{0} \\n 身高:{1}cm \\n 口头禅:{2} "
+}
typescript // src/index.ts
+// 告诉 Kotori 自动加载国际化文件
+export const lang = [ __dirname , ' ../locales ' ];
+
+export function ( ctx : Context ) {
+ ctx . command ( ' himeki ' ). action (( _ , session ) => {
+ // 使用 session.send():
+ const hitokoto = session . i18n . locale ( ' test.msg.himeki.hitokoto ' );
+ const msg = session . format ( session . i18n . locale ( ' test.msg.himeki ' ), [ ' Ichinose Himeki ' , 153 , hitokoto ]);
+ session . send ( msg );
+ // 使用 session.quick():
+ session . quick ([ ' test.msg.himeki ' , [ ' Ichinose Himeki ' , 153 , ' test.msg.himeki.hitokoto ' ]]);
+ // 直接返回:
+ return [ ' test.msg.himeki ' , [ ' Ichinose Himeki ' , 153 , ' test.msg.himeki.hitokoto ' ]];
+ });
+}
会话交互 目前 Kotori 原生提供了两个会话交互方法:session.prompt()
与 session.confirm()
,它们和浏览器中的 prompt()
与 confirm()
类似,分别对应为输入框和提示框。
typescript ctx . command ( ' question ' ). action ( async ( _ , session ) => {
+ await session . quick ( ' 这里有一个问题想问你... ' );
+ const likeme = await session . confirm ({
+ message : ' 你喜欢我吗? ' ,
+ sure : ' 喜欢 '
+ });
+ if ( ! likeme ) return ' 伤透了我的心 ' ;
+ const ago = Number ( await session . prompt ( ' 喜欢我多久了? ' ));
+ if ( Number . isNaN ( ago ) || ago < 0 ) return ' 这可不是一个有效的Number啊! ' ;
+ await session . quick ( ago >= 0 && ago <= 1 ? ' 什么嘛...原来才刚刚开始喜欢啊 ' : \` 居然喜欢了 \${ ago } 这么久啊! \` );
+ return ' 谢谢你的喜欢哦~ ' ;
+});
WARNING
一次性有多个会话交互(消息、输入、确认...)时请注意不用遗漏 await
关键词,否则可能会有一些意料之外的效果。
两者参数均只有一个且可选 session.prompt()
参数为 string
,对应提示消息,返回 Promise<string>
session.confirm()
参数为 { message: string, sure: string }
,分别对应提示消息和确认消息(只有用户发送消息与确认消息完全一致时返回 true
反之 false
),返回 Promise<boolean>
NOTE
目前会话交互功能甚少,内容也不全面,如对 i18n 支持不够完善、需手动进行数据校验、Promise 超时等问题,如有能力欢迎你前来帮助 Kotori 完善。
错误处理 随着功能的不断增多,不稳定性也随之增多,面对用户传入的各种奇怪数据,虽有着 Kotori 本身的指令参数和数据校验用于防护,但这并不能百分百避免所有错误发生,因此学会自行错误处理至关重要。以下是 Kotori 内置的指令错误类型可供参考:
typescript type CommandArgTypeSign = ' string ' | ' number ' | ' boolean ' ;
+
+interface CommandParseResult {
+ option_error : { expected : CommandArgTypeSign ; reality : CommandArgTypeSign ; target : string }; // 选项类型错误
+ arg_error : { expected : CommandArgTypeSign ; reality : CommandArgTypeSign ; index : number }; // 参数类型错误
+ arg_many : { expected : number ; reality : number }; // 参数过多
+ arg_few : CommandParseResult [ ' arg_many ' ]; // 参数过少
+ syntax : { index : number ; char : string }; // 语法错误(引号、反斜杠问题)
+ unknown : { input : string }; // 未知的指令
+}
+
+export interface CommandResult extends CommandParseResult {
+ error : { error : unknown }; // 未知错误
+ data_error : { target : string | number };
+ res_error : { error : TsuError };
+ num_error : null ;
+ no_access_manger : null ; // 无管理员权限
+ no_access_admin : null ; // 无最高管理员权限
+ disable : null ;
+ exists : { target : string };
+ no_exists : CommandResult [ ' exists ' ];
+}
Kotori 中指令指令错误分为两大类:
指令解析时错误:即上述的 CommandParseResult
,这些在指令系统不需要你操心,因为它们已全部交由上游的 Kotori 内置中间件进行处理,在解析指令时就会被发现 指令运行时错误:即上述的 Omit<CommandResult, keyof CommandParseResult>
,它们有的发生在指令执行前(如 no_access_manger
、no_access_admin
),又或者 error
这种错误之外的错误(执行回调函数时捕获的错误),这两者也不需要你操心 需要操心的是剩下可能发生在指令执行期间的错误,这些错误无法由 Kotori 处理,全需要你在编写代码时手动处理:
data_error
参数错误(不同于参数类型错误)res_error
资源错误(主要是指网络请求第三方 Api 时返回数据类型有误)num_error
序号错误(主要是指需要用户传入数字进行选择的情况)exists
目标已存在(如添加目标到名单里但目标已存在于名单)no_exists
目标不存在(如删除目标从名单里但目标不存在于名单)使用 session.error()
方法即可在运行时阶段抛出错误,
typescript export function main ( ctx : Context ) {
+ ctx . command ( ' hitokoto ' ). action ( async ( _ , session ) => {
+ const res = await ctx . http . get ( ' https://hotaru.icu/api/hitokoto/v2/ ' );
+ /* 这里有一些检查数据的操作 */
+ if ( condition ) return session . error ( ' res ' , { error : new Error () }); // 这里会有些小问题,代码仅做演示,请勿照抄
+ return [ ' 今日一言: {0}{1} ' , [ res . data , res . data . from ? \` —— \${ res . data . from } \` : '' ]];
+ });
+}
上述代码展示了其非常经典的一个例子,机器人的功能往往部分来自于网络接口请求,确保其第三方内容的稳定性更是必要的,因此对获取的数据进行检查,然后再进行访问属性操作,如若获取的数据与预期不一致则使用 session.error()
抛出错误
ctx.http
是一个网络请求工具,基于 Axios 封装,具体内容参考接口文档;此处的「检查数据的操作」实际上指 Schema,这将在第三章中讲解
`,86)]))}const A=i(n,[["render",t]]);export{y as __pageData,A as default};
diff --git a/assets/guide_base_events.md.BNxI8SOa.js b/assets/guide_base_events.md.BNxI8SOa.js
new file mode 100644
index 00000000..88d0211d
--- /dev/null
+++ b/assets/guide_base_events.md.BNxI8SOa.js
@@ -0,0 +1,63 @@
+import{_ as i,c as a,a0 as h,o as n}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"事件系统","description":"","frontmatter":{},"headers":[],"relativePath":"guide/base/events.md","filePath":"guide/base/events.md","lastUpdated":1712229374000}'),k={name:"guide/base/events.md"};function t(p,s,l,e,d,r){return n(),a("div",null,s[0]||(s[0]=[h(`事件系统 事件系统(Events) 的上游是事件订阅者模式(Events Emiter) ,该设计模式与事件系统共同构成了 Kotori 的基础,Kotori 内部通过订阅事件保持各部分间的联系和协作任务。同时也有来自各个聊天平台的事件,通过订阅这些事件能实现丰富多样的功能。
订阅事件 事件系统的使用方法与常规的事件订阅者一致,通过 ctx.on()
订阅一个事件,第一个参数为事件名,第二个参数为回调函数,事件被触发时事件数据将作为实际参数传给回调函数。
typescript import { MessageScope } from ' kotori-bot ' ;
+
+// ...
+
+ctx . on ( ' on_message ' , ( session ) => {
+ if ( session . message !== ' 你是谁 ' ) return ;
+ if ( session . type === MessageScope . GROUP ) {
+ session . api . sendGroupMsg ( ' 是 Kotori! ' , session . groupId );
+ } else {
+ session . api . sendPrivateMsg ( ' 是 Kotori! ' , session . userId );
+ }
+});
从上述代码中可以看出,当收到消息时,如果不是「你是谁」则立即退出,执行完毕。如果是则判断 session.type
的值,调用相应的发送消息接口发送「是 Kotori!」。根据语义化命名可知:session.type
为消息类型,值是一个 MessageScope
枚举值,分为 「GROUP」(群聊)和「PRIVATE」(私聊);session.api
是 Api
的实例对象,提供了多种与聊天平台交互的接口,此处用到的 sendG丨groupMsg
与 sendPrivateMsg
分别是发送群聊消息与发送私聊消息,第一个参数为消息内容,第二个参数分别为群聊 id 与用户 id。
id 一般为对应聊天平台提供的 id/uid,叫法不一,值类型为 string 或 number。如当你收到由适配器 @kotori-bot/kotori-plugin-adapter-onebot 发出的消息时,groupId
为 QQ 群号,userId
为 QQ 号。
上面的代码每次都需要判断消息类型再执行相应方法,显得有点繁琐,因此 kotori 提供了一个语法糖:
typescript ctx . on ( ' on_message ' , ( session ) => {
+ if ( session . message !== ' 你是谁 ' ) return ;
+ session . send ( ' 是 Kotori! ' );
+ }
+} );
使用 session.send()
只需要传入消息内容即可,消息类型判断和传入相应 id 的工作已在该方法内部完成。session
上还有不少与之类似的语法糖,将在后面章节中逐一提到,也因如此,session.send()
在实际开发中使用率并不高,因为它对你后面将了解的内容而言依旧很繁琐。
取消订阅事件 正如订阅事件是「on」,取消订阅事件则是「off」。ctx.off()
的使用方法与 ctx.on()
一致。
typescript const handle = ( session : Session [ ' on_message ' ]) => {
+ ctx . off ( ' on_message ' , handle );
+ // ...
+};
+
+ctx . on ( ' on_message ' , handle );
上述代码中,触发事件后会立即取消订阅事件,意味着它只会被触发一次。ctx.on()
在执行后会返回取消订阅自己的方法,因此可以这样简化:
typescript const off = ctx . on ( ' on_message ' , ( session ) => {
+ off ();
+ // ...
+});
使用 ctx.once()
再进一步简化:
typescript ctx . once ( ' on_message ' , ( session ) => {
+ // ...
+});
工作流程与上面一致,通过 ctx.once()
订阅事件,在触发后会立即取消订阅。
使用 ctx.offAll()
取消订阅指定事件名下所有事件:
typescript ctx . once ( ' on_message ' , ( session ) => {
+ // ...
+});
+
+ctx . once ( ' on_message ' , ( session ) => {
+ // ...
+});
+
+ctx . on ( ' on_message ' , ( session ) => {
+ if ( session . message === ' 消失吧! ' ) return ;
+ ctx . offAll ( ' on_message ' );
+ }
+} );
在第三个回调函数中,当收到消息「消失吧!」时将取消订阅所有 on_message
事件。
事件类型 Kotori 中事件类型大致分为三类:
系统事件(System Event) :与生命周期和适配器有关的事件,回调函数中的参数名一般为 data
。会话事件(Session Event) :与聊天平台有关的事件,回调函数中的参数名一般为 session
。自定义事件(Custom Event) :由模块定义的事件,一般用于模块内部或多个模块间通信,参数量不固定。系统事件 常见的系统事件有:
ready
:当加载完所有模块时触发dispose
:当 Kotori 关闭时触发status
:当 Bot 的在线状态改变时触发通过 status
实现 Bot 上线后自动发送消息给最高管理员:
typescript ctx . on ( ' status ' , ( data ) => {
+ if ( data . status !== ' online ' ) return ;
+ const { api , config } = data . adapter ;
+ api . sendPrivateMsg ( ' 上线了! ' , config . master );
+});
由于 status
是由适配器发出的系统事件,它并没有类似于会话事件中的 session.send()
,因此只能使用最原始的办法发送消息。status
的事件数据中仅有两个值,一个是 data.status
表示当前在线状态(「online」或「offline」),data.adapter
为目标 Bot,Bot 上有 adapter.api
与 adapter.config
,前者等价于会话事件中的 session.api
,后者为 Bot 配置,来自于 kotori.yml
。
会话事件 常见的会话事件有:
on_message
:当收到消息时触发on_recall
:当有消息撤回时触发on_group_increase
:当群人数增加时触发通过 on_group_increase
实现群欢迎:
typescript ctx . on ( ' on_group_increase ' , ( session ) => {
+ session . send ( \` 因为遇见了 \${ session . el . at ( session . userId ) } ,我的世界才充满颜色! \` );
+});
其中 session.el
与 session.api
类似,是 Elements
的实例对象,它提供了用于转换消息元素的接口,如 session.el.at()
传入用户 id 转换成艾特消息,session.el.image()
传入图片 URL 转换成图片消息。当然,并不是所有聊天平台都支持所有的消息元素,应以具体聊天平台为准。
自定义事件与发出事件 得益于 TypeScript 有着 声明合并(Declaration Merging) 的特性,在模块中可通过其实现自定义事件的局部声明。
typescript declare module ' kotori-bot ' {
+ interface EventsMapping {
+ custom_event1 ( data : string ): void ;
+ }
+}
+
+ctx . on ( ' custom_event1 ' , ( data ) => {
+ ctx . logger . debug ( data );
+});
Kotori 中所有事件均定义在 EventsMapping
接口上。custom_event1
事件触发后将打印事件数据。
ctx.logger
是一个日志打印工具,ctx.logger.debug()
意味着打印内容仅在 dev
模式下运行 Kotori 可见,具体内容请参考接口文档
然而,订阅事件后,事件却从来没有发出,因此需要发出事件:
typescript // ...
+
+ctx . emit ( ' custom_event1 ' , ' 这是事件数据 ' );
+ctx . emit ( ' custom_event1 ' , ' 这里也是事件数据 ' );
ctx.emit()
第一个参数为事件名,然后为剩余参数,剩余参数与该事件参数一一对应。虽然 Kotori 中系统事件与会话事件的参数均只有一个,但是可以在自定义事件中实现任意多个参数:
typescript declare module ' kotori-bot ' {
+ interface EventsMapping {
+ custom_event2 ( arg1 : string , arg2 : number , arg3 : boolean ): void ;
+ custom_event3 (... args : any []): void ;
+ }
+}
+
+ctx . emit ( ' custom_event2 ' , ' string ' , 42 , true );
+ctx . emit ( ' custom_event3 ' , ' string1 ' , ' string2 ' , 233 , 2333 , { value : 42 });
TIP
一般地,自定义事件应只用于单个模块内部,用于多个模块间相互通信传输数据时,每个涉及模块应先加载定义自定义事件的模块,以免出现类型定义的问题。
`,46)]))}const o=i(k,[["render",t]]);export{y as __pageData,o as default};
diff --git a/assets/guide_base_events.md.BNxI8SOa.lean.js b/assets/guide_base_events.md.BNxI8SOa.lean.js
new file mode 100644
index 00000000..88d0211d
--- /dev/null
+++ b/assets/guide_base_events.md.BNxI8SOa.lean.js
@@ -0,0 +1,63 @@
+import{_ as i,c as a,a0 as h,o as n}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"事件系统","description":"","frontmatter":{},"headers":[],"relativePath":"guide/base/events.md","filePath":"guide/base/events.md","lastUpdated":1712229374000}'),k={name:"guide/base/events.md"};function t(p,s,l,e,d,r){return n(),a("div",null,s[0]||(s[0]=[h(`事件系统 事件系统(Events) 的上游是事件订阅者模式(Events Emiter) ,该设计模式与事件系统共同构成了 Kotori 的基础,Kotori 内部通过订阅事件保持各部分间的联系和协作任务。同时也有来自各个聊天平台的事件,通过订阅这些事件能实现丰富多样的功能。
订阅事件 事件系统的使用方法与常规的事件订阅者一致,通过 ctx.on()
订阅一个事件,第一个参数为事件名,第二个参数为回调函数,事件被触发时事件数据将作为实际参数传给回调函数。
typescript import { MessageScope } from ' kotori-bot ' ;
+
+// ...
+
+ctx . on ( ' on_message ' , ( session ) => {
+ if ( session . message !== ' 你是谁 ' ) return ;
+ if ( session . type === MessageScope . GROUP ) {
+ session . api . sendGroupMsg ( ' 是 Kotori! ' , session . groupId );
+ } else {
+ session . api . sendPrivateMsg ( ' 是 Kotori! ' , session . userId );
+ }
+});
从上述代码中可以看出,当收到消息时,如果不是「你是谁」则立即退出,执行完毕。如果是则判断 session.type
的值,调用相应的发送消息接口发送「是 Kotori!」。根据语义化命名可知:session.type
为消息类型,值是一个 MessageScope
枚举值,分为 「GROUP」(群聊)和「PRIVATE」(私聊);session.api
是 Api
的实例对象,提供了多种与聊天平台交互的接口,此处用到的 sendG丨groupMsg
与 sendPrivateMsg
分别是发送群聊消息与发送私聊消息,第一个参数为消息内容,第二个参数分别为群聊 id 与用户 id。
id 一般为对应聊天平台提供的 id/uid,叫法不一,值类型为 string 或 number。如当你收到由适配器 @kotori-bot/kotori-plugin-adapter-onebot 发出的消息时,groupId
为 QQ 群号,userId
为 QQ 号。
上面的代码每次都需要判断消息类型再执行相应方法,显得有点繁琐,因此 kotori 提供了一个语法糖:
typescript ctx . on ( ' on_message ' , ( session ) => {
+ if ( session . message !== ' 你是谁 ' ) return ;
+ session . send ( ' 是 Kotori! ' );
+ }
+} );
使用 session.send()
只需要传入消息内容即可,消息类型判断和传入相应 id 的工作已在该方法内部完成。session
上还有不少与之类似的语法糖,将在后面章节中逐一提到,也因如此,session.send()
在实际开发中使用率并不高,因为它对你后面将了解的内容而言依旧很繁琐。
取消订阅事件 正如订阅事件是「on」,取消订阅事件则是「off」。ctx.off()
的使用方法与 ctx.on()
一致。
typescript const handle = ( session : Session [ ' on_message ' ]) => {
+ ctx . off ( ' on_message ' , handle );
+ // ...
+};
+
+ctx . on ( ' on_message ' , handle );
上述代码中,触发事件后会立即取消订阅事件,意味着它只会被触发一次。ctx.on()
在执行后会返回取消订阅自己的方法,因此可以这样简化:
typescript const off = ctx . on ( ' on_message ' , ( session ) => {
+ off ();
+ // ...
+});
使用 ctx.once()
再进一步简化:
typescript ctx . once ( ' on_message ' , ( session ) => {
+ // ...
+});
工作流程与上面一致,通过 ctx.once()
订阅事件,在触发后会立即取消订阅。
使用 ctx.offAll()
取消订阅指定事件名下所有事件:
typescript ctx . once ( ' on_message ' , ( session ) => {
+ // ...
+});
+
+ctx . once ( ' on_message ' , ( session ) => {
+ // ...
+});
+
+ctx . on ( ' on_message ' , ( session ) => {
+ if ( session . message === ' 消失吧! ' ) return ;
+ ctx . offAll ( ' on_message ' );
+ }
+} );
在第三个回调函数中,当收到消息「消失吧!」时将取消订阅所有 on_message
事件。
事件类型 Kotori 中事件类型大致分为三类:
系统事件(System Event) :与生命周期和适配器有关的事件,回调函数中的参数名一般为 data
。会话事件(Session Event) :与聊天平台有关的事件,回调函数中的参数名一般为 session
。自定义事件(Custom Event) :由模块定义的事件,一般用于模块内部或多个模块间通信,参数量不固定。系统事件 常见的系统事件有:
ready
:当加载完所有模块时触发dispose
:当 Kotori 关闭时触发status
:当 Bot 的在线状态改变时触发通过 status
实现 Bot 上线后自动发送消息给最高管理员:
typescript ctx . on ( ' status ' , ( data ) => {
+ if ( data . status !== ' online ' ) return ;
+ const { api , config } = data . adapter ;
+ api . sendPrivateMsg ( ' 上线了! ' , config . master );
+});
由于 status
是由适配器发出的系统事件,它并没有类似于会话事件中的 session.send()
,因此只能使用最原始的办法发送消息。status
的事件数据中仅有两个值,一个是 data.status
表示当前在线状态(「online」或「offline」),data.adapter
为目标 Bot,Bot 上有 adapter.api
与 adapter.config
,前者等价于会话事件中的 session.api
,后者为 Bot 配置,来自于 kotori.yml
。
会话事件 常见的会话事件有:
on_message
:当收到消息时触发on_recall
:当有消息撤回时触发on_group_increase
:当群人数增加时触发通过 on_group_increase
实现群欢迎:
typescript ctx . on ( ' on_group_increase ' , ( session ) => {
+ session . send ( \` 因为遇见了 \${ session . el . at ( session . userId ) } ,我的世界才充满颜色! \` );
+});
其中 session.el
与 session.api
类似,是 Elements
的实例对象,它提供了用于转换消息元素的接口,如 session.el.at()
传入用户 id 转换成艾特消息,session.el.image()
传入图片 URL 转换成图片消息。当然,并不是所有聊天平台都支持所有的消息元素,应以具体聊天平台为准。
自定义事件与发出事件 得益于 TypeScript 有着 声明合并(Declaration Merging) 的特性,在模块中可通过其实现自定义事件的局部声明。
typescript declare module ' kotori-bot ' {
+ interface EventsMapping {
+ custom_event1 ( data : string ): void ;
+ }
+}
+
+ctx . on ( ' custom_event1 ' , ( data ) => {
+ ctx . logger . debug ( data );
+});
Kotori 中所有事件均定义在 EventsMapping
接口上。custom_event1
事件触发后将打印事件数据。
ctx.logger
是一个日志打印工具,ctx.logger.debug()
意味着打印内容仅在 dev
模式下运行 Kotori 可见,具体内容请参考接口文档
然而,订阅事件后,事件却从来没有发出,因此需要发出事件:
typescript // ...
+
+ctx . emit ( ' custom_event1 ' , ' 这是事件数据 ' );
+ctx . emit ( ' custom_event1 ' , ' 这里也是事件数据 ' );
ctx.emit()
第一个参数为事件名,然后为剩余参数,剩余参数与该事件参数一一对应。虽然 Kotori 中系统事件与会话事件的参数均只有一个,但是可以在自定义事件中实现任意多个参数:
typescript declare module ' kotori-bot ' {
+ interface EventsMapping {
+ custom_event2 ( arg1 : string , arg2 : number , arg3 : boolean ): void ;
+ custom_event3 (... args : any []): void ;
+ }
+}
+
+ctx . emit ( ' custom_event2 ' , ' string ' , 42 , true );
+ctx . emit ( ' custom_event3 ' , ' string1 ' , ' string2 ' , 233 , 2333 , { value : 42 });
TIP
一般地,自定义事件应只用于单个模块内部,用于多个模块间相互通信传输数据时,每个涉及模块应先加载定义自定义事件的模块,以免出现类型定义的问题。
`,46)]))}const o=i(k,[["render",t]]);export{y as __pageData,o as default};
diff --git a/assets/guide_base_middleware.md.BSX_ratG.js b/assets/guide_base_middleware.md.BSX_ratG.js
new file mode 100644
index 00000000..74ac7ef3
--- /dev/null
+++ b/assets/guide_base_middleware.md.BSX_ratG.js
@@ -0,0 +1,59 @@
+import{_ as i,c as a,a0 as n,o as h}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"中间件","description":"","frontmatter":{},"headers":[],"relativePath":"guide/base/middleware.md","filePath":"guide/base/middleware.md","lastUpdated":1723293723000}'),k={name:"guide/base/middleware.md"};function l(p,s,t,e,d,r){return h(),a("div",null,s[0]||(s[0]=[n(`中间件 中间件(Middleware) 是 Kotori 中另一种监听消息事件的语法糖,与指令系统类似,它也是对 on_message
事件的再处理与封装。中间件的主要用途是提前判断或者过滤掉不必要的消息事件,这样后续的指令和正则表达式等位于下游的设施也不会被这些消息事件触发,从而提高效率。
中间件的工作原理与 Express 等后端框架中的中间件概念基本一致。每次收到消息时,Kotori 会依次执行所有已注册的中间件,只有当所有中间件都通过时,该消息事件才会真正被处理。
注册中间件 通过 ctx.midware()
注册一个中间件,该方法接受两个参数:
中间件回调函数 可选的中间件优先级,默认为 50 优先级数字越小(但不能为负数)则优先级越高,如果两个中间件的优先级相同,则按照注册顺序执行,先注册的中间件会先执行。
WARNING
如无特殊需求建议请勿更改优先级,否则可能会导致一些意料之外的问题。
typescript ctx . midware (( next , session ) => {
+ // 中间件逻辑...
+ next (); // 通过此中间件
+}, 80 ); // 优先级为 80
中间件回调函数接收两个参数:
next
函数,调用它将执行下一个中间件session
对象,包含当前消息事件的上下文信息在中间件内部,你可以根据消息内容或发送者等信息决定是否调用 next()
函数。如果调用了 next()
则通过此中间件,否则此消息事件将被过滤掉,不再执行后续的中间件和其他处理逻辑。
移除中间件 ctx.midware()
方法的返回值是一个可以用于移除该中间件的函数。
typescript const dispose = ctx . midware (( next ) => {
+ // ...
+ next ();
+});
+
+// 移除中间件
+dispose ();
使用示例 基本使用 typescript ctx . midware (( next , session ) => {
+ console . log ( ' 收到一条消息 ' );
+ next ();
+});
+
+ctx . midware (( next , session ) => {
+ console . log ( ' 这是另一个中间件 ' );
+ session . quick ( ' 这条消息将被发送 ' );
+ next ();
+});
上述代码注册了两个中间件,每当收到一条消息时,它们都会被执行。第一个中间件只打印日志,第二个则先打印日志,然后发送一条消息。由于两个中间件都调用了 next()
函数,因此该消息事件会继续被处理。
过滤消息 typescript ctx . midware (( next , session ) => {
+ if ( session . message !== ' hello ' ) return ;
+ next ();
+});
+
+ctx . command ( ' hello ' ). action (() => ' Hello World! ' );
这个示例中的中间件会过滤掉消息内容不是「hello」的消息事件。只有当消息是「hello」时,中间件才会调用 next()
让该消息事件继续被处理。通过使用中间件,我们可以在消息流经 Kotori 的各个环节进行拦截和处理,实现更加灵活和可控的消息处理逻辑。
限制命令使用频率 有时我们需要限制某些命令的使用频率,以防止被滥用。这时可以使用中间件来实现这一功能。
typescript // 用于存储命令使用记录
+const cmdUsageRecord = new Map ();
+
+ctx . midware (( next , session ) => {
+ // 检查是否为命令消息
+ if ( ! session . message . startsWith ( ' / ' )) {
+ next (); // 非命令消息,直接通过
+ return ;
+ }
+
+ const cmd = session . message . slice ( 1 ); // 获取命令名
+ const userId = session . userId ; // 获取发送者ID
+
+ // 如果此命令无使用记录,则新建一个记录
+ if ( ! cmdUsageRecord . has ( cmd )) {
+ cmdUsageRecord . set ( cmd , new Map ());
+ }
+ const userRecord = cmdUsageRecord . get ( cmd );
+
+ // 获取该用户对此命令的最后使用时间
+ const lastUsedAt = userRecord . get ( userId ) || 0 ;
+
+ // 计算距离最后一次使用的时间间隔(单位:秒)
+ constInterval = ( Date . now () - lastUsedAt ) / 1000 ;
+
+ // 如果间隔小于10秒,则拒绝执行该命令
+ if ( Interval < 10 ) {
+ session . quick ( ' 命令使用过于频繁,请稍后再试 ' );
+ return ;
+ }
+
+ // 更新该用户对此命令的最后使用时间
+ userRecord . set ( userId , Date . now ());
+
+ next (); // 通过中间件
+}, 10 ); // 设置较高优先级
上述代码定义了一个中间件,用于限制命令的使用频率。具体逻辑如下:
首先检查收到的消息是否以 /
开头,如果不是则直接调用 next()
通过该中间件。 获取命令名和发送者 ID。 检查是否存在该命令的使用记录,如果没有则新建一个记录。 获取该用户对此命令的最后使用时间,如果不存在则认为是第一次使用,最后使用时间设为 0。 计算距离上次使用的时间间隔(单位为秒)。 如果时间间隔小于 10 秒,则拒绝执行该命令,发送 '命令使用过于频繁,请稍后再试'
。 如果时间间隔大于等于 10 秒,则更新该用户对此命令的最后使用时间,并调用 next()
通过该中间件。 该中间件的优先级设为 10,这是为了让它能够比大多数命令优先执行。我们使用 Map
来存储命令使用记录,外层 Map
的键为命令名,值为另一个 Map
,内层 Map
的键为用户 ID,值为该用户最后一次使用该命令的时间戳。
通过这种方式,我们可以精确地控制每个用户对每个命令的使用频率,并且只对命令消息生效,不会影响到其他普通消息的处理。需要注意的是,这个示例使用了内存来存储命令使用记录,因此在重启 Bot 后记录会被清空。在实际应用中,你可以将记录持久化存储到数据库中。
`,30)]))}const c=i(k,[["render",l]]);export{y as __pageData,c as default};
diff --git a/assets/guide_base_middleware.md.BSX_ratG.lean.js b/assets/guide_base_middleware.md.BSX_ratG.lean.js
new file mode 100644
index 00000000..74ac7ef3
--- /dev/null
+++ b/assets/guide_base_middleware.md.BSX_ratG.lean.js
@@ -0,0 +1,59 @@
+import{_ as i,c as a,a0 as n,o as h}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"中间件","description":"","frontmatter":{},"headers":[],"relativePath":"guide/base/middleware.md","filePath":"guide/base/middleware.md","lastUpdated":1723293723000}'),k={name:"guide/base/middleware.md"};function l(p,s,t,e,d,r){return h(),a("div",null,s[0]||(s[0]=[n(`中间件 中间件(Middleware) 是 Kotori 中另一种监听消息事件的语法糖,与指令系统类似,它也是对 on_message
事件的再处理与封装。中间件的主要用途是提前判断或者过滤掉不必要的消息事件,这样后续的指令和正则表达式等位于下游的设施也不会被这些消息事件触发,从而提高效率。
中间件的工作原理与 Express 等后端框架中的中间件概念基本一致。每次收到消息时,Kotori 会依次执行所有已注册的中间件,只有当所有中间件都通过时,该消息事件才会真正被处理。
注册中间件 通过 ctx.midware()
注册一个中间件,该方法接受两个参数:
中间件回调函数 可选的中间件优先级,默认为 50 优先级数字越小(但不能为负数)则优先级越高,如果两个中间件的优先级相同,则按照注册顺序执行,先注册的中间件会先执行。
WARNING
如无特殊需求建议请勿更改优先级,否则可能会导致一些意料之外的问题。
typescript ctx . midware (( next , session ) => {
+ // 中间件逻辑...
+ next (); // 通过此中间件
+}, 80 ); // 优先级为 80
中间件回调函数接收两个参数:
next
函数,调用它将执行下一个中间件session
对象,包含当前消息事件的上下文信息在中间件内部,你可以根据消息内容或发送者等信息决定是否调用 next()
函数。如果调用了 next()
则通过此中间件,否则此消息事件将被过滤掉,不再执行后续的中间件和其他处理逻辑。
移除中间件 ctx.midware()
方法的返回值是一个可以用于移除该中间件的函数。
typescript const dispose = ctx . midware (( next ) => {
+ // ...
+ next ();
+});
+
+// 移除中间件
+dispose ();
使用示例 基本使用 typescript ctx . midware (( next , session ) => {
+ console . log ( ' 收到一条消息 ' );
+ next ();
+});
+
+ctx . midware (( next , session ) => {
+ console . log ( ' 这是另一个中间件 ' );
+ session . quick ( ' 这条消息将被发送 ' );
+ next ();
+});
上述代码注册了两个中间件,每当收到一条消息时,它们都会被执行。第一个中间件只打印日志,第二个则先打印日志,然后发送一条消息。由于两个中间件都调用了 next()
函数,因此该消息事件会继续被处理。
过滤消息 typescript ctx . midware (( next , session ) => {
+ if ( session . message !== ' hello ' ) return ;
+ next ();
+});
+
+ctx . command ( ' hello ' ). action (() => ' Hello World! ' );
这个示例中的中间件会过滤掉消息内容不是「hello」的消息事件。只有当消息是「hello」时,中间件才会调用 next()
让该消息事件继续被处理。通过使用中间件,我们可以在消息流经 Kotori 的各个环节进行拦截和处理,实现更加灵活和可控的消息处理逻辑。
限制命令使用频率 有时我们需要限制某些命令的使用频率,以防止被滥用。这时可以使用中间件来实现这一功能。
typescript // 用于存储命令使用记录
+const cmdUsageRecord = new Map ();
+
+ctx . midware (( next , session ) => {
+ // 检查是否为命令消息
+ if ( ! session . message . startsWith ( ' / ' )) {
+ next (); // 非命令消息,直接通过
+ return ;
+ }
+
+ const cmd = session . message . slice ( 1 ); // 获取命令名
+ const userId = session . userId ; // 获取发送者ID
+
+ // 如果此命令无使用记录,则新建一个记录
+ if ( ! cmdUsageRecord . has ( cmd )) {
+ cmdUsageRecord . set ( cmd , new Map ());
+ }
+ const userRecord = cmdUsageRecord . get ( cmd );
+
+ // 获取该用户对此命令的最后使用时间
+ const lastUsedAt = userRecord . get ( userId ) || 0 ;
+
+ // 计算距离最后一次使用的时间间隔(单位:秒)
+ constInterval = ( Date . now () - lastUsedAt ) / 1000 ;
+
+ // 如果间隔小于10秒,则拒绝执行该命令
+ if ( Interval < 10 ) {
+ session . quick ( ' 命令使用过于频繁,请稍后再试 ' );
+ return ;
+ }
+
+ // 更新该用户对此命令的最后使用时间
+ userRecord . set ( userId , Date . now ());
+
+ next (); // 通过中间件
+}, 10 ); // 设置较高优先级
上述代码定义了一个中间件,用于限制命令的使用频率。具体逻辑如下:
首先检查收到的消息是否以 /
开头,如果不是则直接调用 next()
通过该中间件。 获取命令名和发送者 ID。 检查是否存在该命令的使用记录,如果没有则新建一个记录。 获取该用户对此命令的最后使用时间,如果不存在则认为是第一次使用,最后使用时间设为 0。 计算距离上次使用的时间间隔(单位为秒)。 如果时间间隔小于 10 秒,则拒绝执行该命令,发送 '命令使用过于频繁,请稍后再试'
。 如果时间间隔大于等于 10 秒,则更新该用户对此命令的最后使用时间,并调用 next()
通过该中间件。 该中间件的优先级设为 10,这是为了让它能够比大多数命令优先执行。我们使用 Map
来存储命令使用记录,外层 Map
的键为命令名,值为另一个 Map
,内层 Map
的键为用户 ID,值为该用户最后一次使用该命令的时间戳。
通过这种方式,我们可以精确地控制每个用户对每个命令的使用频率,并且只对命令消息生效,不会影响到其他普通消息的处理。需要注意的是,这个示例使用了内存来存储命令使用记录,因此在重启 Bot 后记录会被清空。在实际应用中,你可以将记录持久化存储到数据库中。
`,30)]))}const c=i(k,[["render",l]]);export{y as __pageData,c as default};
diff --git a/assets/guide_base_regexp.md.Bh1AF6U4.js b/assets/guide_base_regexp.md.Bh1AF6U4.js
new file mode 100644
index 00000000..d22ddb17
--- /dev/null
+++ b/assets/guide_base_regexp.md.Bh1AF6U4.js
@@ -0,0 +1,48 @@
+import{_ as i,c as a,a0 as h,o as k}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"正则匹配","description":"","frontmatter":{},"headers":[],"relativePath":"guide/base/regexp.md","filePath":"guide/base/regexp.md","lastUpdated":1723293723000}'),n={name:"guide/base/regexp.md"};function p(t,s,l,e,d,r){return k(),a("div",null,s[0]||(s[0]=[h(`正则匹配 正则匹配(RegExp) 同样是 Kotori 中一种监听消息事件的语法糖。它的主要用途是通过正则表达式匹配消息内容,然后执行相应的处理逻辑。值得一提的是,正则匹配位于消息事件的最后一环(在中间件和指令之后执行),这意味着只有通过了所有中间件和指令的消息,才会进入正则匹配的环节。
正则匹配依赖于正则表达式的强大功能,可以实现多种匹配模式,例如完全匹配、模糊匹配等,为消息处理提供了更大的灵活性。
注册正则匹配 通过 ctx.regexp()
注册一个正则匹配,该方法接受两个参数:
match
: 用于匹配消息内容的正则表达式callback
: 当正则匹配成功时执行的回调函数typescript ctx . regexp ( / ^ \\/ start $ / , ( match , session ) => {
+ session . send ( ' 游戏开始! ' );
+});
上述代码注册了一个正则匹配,当收到消息内容为 /start
时,它会执行回调函数,并向发送者发送 '游戏开始!'
消息。
callback
函数接收两个参数:
match
: 正则匹配结果,是一个数组,第一项为完整匹配结果,后续项为各个捕获组的内容session
: 当前消息事件的上下文信息在回调函数中,你可以根据匹配结果执行相应的逻辑,回调函数的返回值将作为消息发送的内容。
移除正则匹配 ctx.regexp()
方法的返回值是一个可以用于移除该正则匹配的函数。
typescript const off = ctx . regexp ( / pattern / , () => {
+ /* ... */
+});
+
+// 移除正则匹配
+off ();
正则匹配示例 简单匹配 typescript ctx . regexp ( / ^ \\/ echo ( . + ) $ / , ( match , session ) => {
+ const content = match [ 1 ]; // 捕获组内容
+ session . send ( content ); // 回声匹配消息
+});
上述代码注册了一个正则匹配,用于实现 「/echo」 命令。当收到类似 「/echo 你好」 的消息时,正则会匹配到 你好
并将其作为第一个捕获组,然后在回调函数中将捕获组的内容作为消息发送出去。
复杂匹配 typescript ctx . regexp ( / ^ 算 (( 数学 | 语文 | 英语 ) \\b ) \\s * ( \\d + ) 分 $ / , ( match , session ) => {
+ const [, , subject , score ] = match ;
+ let msg ;
+ switch ( subject ) {
+ case ' 数学 ' :
+ msg = \` 数学 \${ score } 分 \` ;
+ break ;
+ case ' 语文 ' :
+ msg = \` 语文 \${ score } 分 \` ;
+ break ;
+ case ' 英语 ' :
+ msg = \` 英语 \${ score } 分 \` ;
+ break ;
+ default :
+ msg = ' 不支持的科目 ' ;
+ }
+ session . send ( msg );
+});
这个例子演示了一个稍微复杂的正则匹配。正则 /^算((数学|语文|英语)\\b)\\s*(\\d+)分$/
用于匹配像 「算数学98分」、「算 语文80分」 这样的消息。正则中使用了一个命名捕获组 (?<subject>...)
(不过这个语法还未被 Node.js 完全支持,因此这里使用了普通的捕获组)。
在回调函数中,我们通过数组解构拿到匹配的学科和分数,然后根据不同的学科返回对应的消息内容。
模糊匹配 typescript ctx . regexp ( / 在 \\s * 吗 ? / , ( match , session ) => {
+ session . send ( ' 我在这里 ' );
+});
这个例子展示了如何使用正则进行模糊匹配。正则 /在\\s*吗?/
可以匹配 「在吗」、「在 吗」 以及 「在」 这三种情况。使用 ?
可以使前面的字符或字符组成为可选。
多个匹配 typescript ctx . regexp ( / ^ ( 加 | 减 | 乘 | 除 ) \\s * ( \\d + ) \\s * ( 加 | 减 | 乘 | 除 ) ? \\s * ( \\d + ) ? $ / , ( match , session ) => {
+ const [, op1 , n1 , op2 , n2 ] = match ;
+ let result ;
+ switch ( op1 ) {
+ case ' 加 ' :
+ result = n2 ? parseInt ( n1 ) + parseInt ( n2 ) : parseInt ( n1 );
+ break ;
+ case ' 减 ' :
+ result = n2 ? parseInt ( n1 ) - parseInt ( n2 ) : - parseInt ( n1 );
+ break ;
+ case ' 乘 ' :
+ result = n2 ? parseInt ( n1 ) * parseInt ( n2 ) : parseInt ( n1 );
+ break ;
+ case ' 除 ' :
+ result = n2 ? parseInt ( n1 ) / parseInt ( n2 ) : 1 / parseInt ( n1 );
+ break ;
+ }
+ session . send ( \` 结果是: \${ result } \` );
+});
这个例子展示了如何在一个正则匹配中处理多个匹配情况。正则 /^(加|减|乘|除)\\s*(\\d+)\\s*(加|减|乘|除)?\\s*(\\d+)?$/
可以匹配像 「加10「、「减20「、「乘30」、「除40「、「加10除2「 这样的算术表达式。
在回调函数中,我们根据匹配的运算符和操作数进行相应的计算,并将结果作为消息发送出去。需要注意的是,这里我们使用了可选的捕获组,因此在处理单个操作数的情况时需要进行判断。
通过正则匹配的强大功能,我们可以灵活地处理各种复杂的消息,实现个性化的交互体验。而将正则匹配与其他功能(如指令系统、数据持久化等)相结合,就能构建出更加强大的应用程序。
`,30)]))}const A=i(n,[["render",p]]);export{y as __pageData,A as default};
diff --git a/assets/guide_base_regexp.md.Bh1AF6U4.lean.js b/assets/guide_base_regexp.md.Bh1AF6U4.lean.js
new file mode 100644
index 00000000..d22ddb17
--- /dev/null
+++ b/assets/guide_base_regexp.md.Bh1AF6U4.lean.js
@@ -0,0 +1,48 @@
+import{_ as i,c as a,a0 as h,o as k}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"正则匹配","description":"","frontmatter":{},"headers":[],"relativePath":"guide/base/regexp.md","filePath":"guide/base/regexp.md","lastUpdated":1723293723000}'),n={name:"guide/base/regexp.md"};function p(t,s,l,e,d,r){return k(),a("div",null,s[0]||(s[0]=[h(`正则匹配 正则匹配(RegExp) 同样是 Kotori 中一种监听消息事件的语法糖。它的主要用途是通过正则表达式匹配消息内容,然后执行相应的处理逻辑。值得一提的是,正则匹配位于消息事件的最后一环(在中间件和指令之后执行),这意味着只有通过了所有中间件和指令的消息,才会进入正则匹配的环节。
正则匹配依赖于正则表达式的强大功能,可以实现多种匹配模式,例如完全匹配、模糊匹配等,为消息处理提供了更大的灵活性。
注册正则匹配 通过 ctx.regexp()
注册一个正则匹配,该方法接受两个参数:
match
: 用于匹配消息内容的正则表达式callback
: 当正则匹配成功时执行的回调函数typescript ctx . regexp ( / ^ \\/ start $ / , ( match , session ) => {
+ session . send ( ' 游戏开始! ' );
+});
上述代码注册了一个正则匹配,当收到消息内容为 /start
时,它会执行回调函数,并向发送者发送 '游戏开始!'
消息。
callback
函数接收两个参数:
match
: 正则匹配结果,是一个数组,第一项为完整匹配结果,后续项为各个捕获组的内容session
: 当前消息事件的上下文信息在回调函数中,你可以根据匹配结果执行相应的逻辑,回调函数的返回值将作为消息发送的内容。
移除正则匹配 ctx.regexp()
方法的返回值是一个可以用于移除该正则匹配的函数。
typescript const off = ctx . regexp ( / pattern / , () => {
+ /* ... */
+});
+
+// 移除正则匹配
+off ();
正则匹配示例 简单匹配 typescript ctx . regexp ( / ^ \\/ echo ( . + ) $ / , ( match , session ) => {
+ const content = match [ 1 ]; // 捕获组内容
+ session . send ( content ); // 回声匹配消息
+});
上述代码注册了一个正则匹配,用于实现 「/echo」 命令。当收到类似 「/echo 你好」 的消息时,正则会匹配到 你好
并将其作为第一个捕获组,然后在回调函数中将捕获组的内容作为消息发送出去。
复杂匹配 typescript ctx . regexp ( / ^ 算 (( 数学 | 语文 | 英语 ) \\b ) \\s * ( \\d + ) 分 $ / , ( match , session ) => {
+ const [, , subject , score ] = match ;
+ let msg ;
+ switch ( subject ) {
+ case ' 数学 ' :
+ msg = \` 数学 \${ score } 分 \` ;
+ break ;
+ case ' 语文 ' :
+ msg = \` 语文 \${ score } 分 \` ;
+ break ;
+ case ' 英语 ' :
+ msg = \` 英语 \${ score } 分 \` ;
+ break ;
+ default :
+ msg = ' 不支持的科目 ' ;
+ }
+ session . send ( msg );
+});
这个例子演示了一个稍微复杂的正则匹配。正则 /^算((数学|语文|英语)\\b)\\s*(\\d+)分$/
用于匹配像 「算数学98分」、「算 语文80分」 这样的消息。正则中使用了一个命名捕获组 (?<subject>...)
(不过这个语法还未被 Node.js 完全支持,因此这里使用了普通的捕获组)。
在回调函数中,我们通过数组解构拿到匹配的学科和分数,然后根据不同的学科返回对应的消息内容。
模糊匹配 typescript ctx . regexp ( / 在 \\s * 吗 ? / , ( match , session ) => {
+ session . send ( ' 我在这里 ' );
+});
这个例子展示了如何使用正则进行模糊匹配。正则 /在\\s*吗?/
可以匹配 「在吗」、「在 吗」 以及 「在」 这三种情况。使用 ?
可以使前面的字符或字符组成为可选。
多个匹配 typescript ctx . regexp ( / ^ ( 加 | 减 | 乘 | 除 ) \\s * ( \\d + ) \\s * ( 加 | 减 | 乘 | 除 ) ? \\s * ( \\d + ) ? $ / , ( match , session ) => {
+ const [, op1 , n1 , op2 , n2 ] = match ;
+ let result ;
+ switch ( op1 ) {
+ case ' 加 ' :
+ result = n2 ? parseInt ( n1 ) + parseInt ( n2 ) : parseInt ( n1 );
+ break ;
+ case ' 减 ' :
+ result = n2 ? parseInt ( n1 ) - parseInt ( n2 ) : - parseInt ( n1 );
+ break ;
+ case ' 乘 ' :
+ result = n2 ? parseInt ( n1 ) * parseInt ( n2 ) : parseInt ( n1 );
+ break ;
+ case ' 除 ' :
+ result = n2 ? parseInt ( n1 ) / parseInt ( n2 ) : 1 / parseInt ( n1 );
+ break ;
+ }
+ session . send ( \` 结果是: \${ result } \` );
+});
这个例子展示了如何在一个正则匹配中处理多个匹配情况。正则 /^(加|减|乘|除)\\s*(\\d+)\\s*(加|减|乘|除)?\\s*(\\d+)?$/
可以匹配像 「加10「、「减20「、「乘30」、「除40「、「加10除2「 这样的算术表达式。
在回调函数中,我们根据匹配的运算符和操作数进行相应的计算,并将结果作为消息发送出去。需要注意的是,这里我们使用了可选的捕获组,因此在处理单个操作数的情况时需要进行判断。
通过正则匹配的强大功能,我们可以灵活地处理各种复杂的消息,实现个性化的交互体验。而将正则匹配与其他功能(如指令系统、数据持久化等)相结合,就能构建出更加强大的应用程序。
`,30)]))}const A=i(n,[["render",p]]);export{y as __pageData,A as default};
diff --git a/assets/guide_components_adapter.md.CuUX_HEj.js b/assets/guide_components_adapter.md.CuUX_HEj.js
new file mode 100644
index 00000000..726427f2
--- /dev/null
+++ b/assets/guide_components_adapter.md.CuUX_HEj.js
@@ -0,0 +1 @@
+import{_ as t,c as r,j as a,a as o,o as n}from"./chunks/framework.p2VkXzrt.js";const _=JSON.parse('{"title":"实现适配器类","description":"","frontmatter":{},"headers":[],"relativePath":"guide/components/adapter.md","filePath":"guide/components/adapter.md","lastUpdated":1712229374000}'),s={name:"guide/components/adapter.md"};function d(p,e,c,i,l,m){return n(),r("div",null,e[0]||(e[0]=[a("h1",{id:"实现适配器类",tabindex:"-1"},[o("实现适配器类 "),a("a",{class:"header-anchor",href:"#实现适配器类","aria-label":'Permalink to "实现适配器类"'},"")],-1)]))}const u=t(s,[["render",d]]);export{_ as __pageData,u as default};
diff --git a/assets/guide_components_adapter.md.CuUX_HEj.lean.js b/assets/guide_components_adapter.md.CuUX_HEj.lean.js
new file mode 100644
index 00000000..726427f2
--- /dev/null
+++ b/assets/guide_components_adapter.md.CuUX_HEj.lean.js
@@ -0,0 +1 @@
+import{_ as t,c as r,j as a,a as o,o as n}from"./chunks/framework.p2VkXzrt.js";const _=JSON.parse('{"title":"实现适配器类","description":"","frontmatter":{},"headers":[],"relativePath":"guide/components/adapter.md","filePath":"guide/components/adapter.md","lastUpdated":1712229374000}'),s={name:"guide/components/adapter.md"};function d(p,e,c,i,l,m){return n(),r("div",null,e[0]||(e[0]=[a("h1",{id:"实现适配器类",tabindex:"-1"},[o("实现适配器类 "),a("a",{class:"header-anchor",href:"#实现适配器类","aria-label":'Permalink to "实现适配器类"'},"")],-1)]))}const u=t(s,[["render",d]]);export{_ as __pageData,u as default};
diff --git a/assets/guide_components_api.md.BnaAxIB8.js b/assets/guide_components_api.md.BnaAxIB8.js
new file mode 100644
index 00000000..57547f6c
--- /dev/null
+++ b/assets/guide_components_api.md.BnaAxIB8.js
@@ -0,0 +1 @@
+import{_ as t,c as o,j as a,a as n,o as r}from"./chunks/framework.p2VkXzrt.js";const _=JSON.parse('{"title":"实现接口类","description":"","frontmatter":{},"headers":[],"relativePath":"guide/components/api.md","filePath":"guide/components/api.md","lastUpdated":1712229374000}'),s={name:"guide/components/api.md"};function i(d,e,p,c,l,m){return r(),o("div",null,e[0]||(e[0]=[a("h1",{id:"实现接口类",tabindex:"-1"},[n("实现接口类 "),a("a",{class:"header-anchor",href:"#实现接口类","aria-label":'Permalink to "实现接口类"'},"")],-1)]))}const u=t(s,[["render",i]]);export{_ as __pageData,u as default};
diff --git a/assets/guide_components_api.md.BnaAxIB8.lean.js b/assets/guide_components_api.md.BnaAxIB8.lean.js
new file mode 100644
index 00000000..57547f6c
--- /dev/null
+++ b/assets/guide_components_api.md.BnaAxIB8.lean.js
@@ -0,0 +1 @@
+import{_ as t,c as o,j as a,a as n,o as r}from"./chunks/framework.p2VkXzrt.js";const _=JSON.parse('{"title":"实现接口类","description":"","frontmatter":{},"headers":[],"relativePath":"guide/components/api.md","filePath":"guide/components/api.md","lastUpdated":1712229374000}'),s={name:"guide/components/api.md"};function i(d,e,p,c,l,m){return r(),o("div",null,e[0]||(e[0]=[a("h1",{id:"实现接口类",tabindex:"-1"},[n("实现接口类 "),a("a",{class:"header-anchor",href:"#实现接口类","aria-label":'Permalink to "实现接口类"'},"")],-1)]))}const u=t(s,[["render",i]]);export{_ as __pageData,u as default};
diff --git a/assets/guide_components_custom.md.DujlTsCC.js b/assets/guide_components_custom.md.DujlTsCC.js
new file mode 100644
index 00000000..e18ed054
--- /dev/null
+++ b/assets/guide_components_custom.md.DujlTsCC.js
@@ -0,0 +1 @@
+import{_ as a,c as o,j as t,a as s,o as n}from"./chunks/framework.p2VkXzrt.js";const f=JSON.parse('{"title":"自定义服务","description":"","frontmatter":{},"headers":[],"relativePath":"guide/components/custom.md","filePath":"guide/components/custom.md","lastUpdated":1712229374000}'),r={name:"guide/components/custom.md"};function c(d,e,i,m,p,l){return n(),o("div",null,e[0]||(e[0]=[t("h1",{id:"自定义服务",tabindex:"-1"},[s("自定义服务 "),t("a",{class:"header-anchor",href:"#自定义服务","aria-label":'Permalink to "自定义服务"'},"")],-1)]))}const _=a(r,[["render",c]]);export{f as __pageData,_ as default};
diff --git a/assets/guide_components_custom.md.DujlTsCC.lean.js b/assets/guide_components_custom.md.DujlTsCC.lean.js
new file mode 100644
index 00000000..e18ed054
--- /dev/null
+++ b/assets/guide_components_custom.md.DujlTsCC.lean.js
@@ -0,0 +1 @@
+import{_ as a,c as o,j as t,a as s,o as n}from"./chunks/framework.p2VkXzrt.js";const f=JSON.parse('{"title":"自定义服务","description":"","frontmatter":{},"headers":[],"relativePath":"guide/components/custom.md","filePath":"guide/components/custom.md","lastUpdated":1712229374000}'),r={name:"guide/components/custom.md"};function c(d,e,i,m,p,l){return n(),o("div",null,e[0]||(e[0]=[t("h1",{id:"自定义服务",tabindex:"-1"},[s("自定义服务 "),t("a",{class:"header-anchor",href:"#自定义服务","aria-label":'Permalink to "自定义服务"'},"")],-1)]))}const _=a(r,[["render",c]]);export{f as __pageData,_ as default};
diff --git a/assets/guide_components_elements.md.DDywsrdv.js b/assets/guide_components_elements.md.DDywsrdv.js
new file mode 100644
index 00000000..e9955b17
--- /dev/null
+++ b/assets/guide_components_elements.md.DDywsrdv.js
@@ -0,0 +1 @@
+import{_ as a,c as n,j as t,a as s,o}from"./chunks/framework.p2VkXzrt.js";const _=JSON.parse('{"title":"实现元素类","description":"","frontmatter":{},"headers":[],"relativePath":"guide/components/elements.md","filePath":"guide/components/elements.md","lastUpdated":1712229374000}'),r={name:"guide/components/elements.md"};function d(c,e,l,i,m,p){return o(),n("div",null,e[0]||(e[0]=[t("h1",{id:"实现元素类",tabindex:"-1"},[s("实现元素类 "),t("a",{class:"header-anchor",href:"#实现元素类","aria-label":'Permalink to "实现元素类"'},"")],-1)]))}const u=a(r,[["render",d]]);export{_ as __pageData,u as default};
diff --git a/assets/guide_components_elements.md.DDywsrdv.lean.js b/assets/guide_components_elements.md.DDywsrdv.lean.js
new file mode 100644
index 00000000..e9955b17
--- /dev/null
+++ b/assets/guide_components_elements.md.DDywsrdv.lean.js
@@ -0,0 +1 @@
+import{_ as a,c as n,j as t,a as s,o}from"./chunks/framework.p2VkXzrt.js";const _=JSON.parse('{"title":"实现元素类","description":"","frontmatter":{},"headers":[],"relativePath":"guide/components/elements.md","filePath":"guide/components/elements.md","lastUpdated":1712229374000}'),r={name:"guide/components/elements.md"};function d(c,e,l,i,m,p){return o(),n("div",null,e[0]||(e[0]=[t("h1",{id:"实现元素类",tabindex:"-1"},[s("实现元素类 "),t("a",{class:"header-anchor",href:"#实现元素类","aria-label":'Permalink to "实现元素类"'},"")],-1)]))}const u=a(r,[["render",d]]);export{_ as __pageData,u as default};
diff --git a/assets/guide_extend_tools.md.BELaqOF4.js b/assets/guide_extend_tools.md.BELaqOF4.js
new file mode 100644
index 00000000..24a625b9
--- /dev/null
+++ b/assets/guide_extend_tools.md.BELaqOF4.js
@@ -0,0 +1 @@
+import{_ as a,c as o,j as t,a as s,o as r}from"./chunks/framework.p2VkXzrt.js";const _=JSON.parse('{"title":"工具函数","description":"","frontmatter":{},"headers":[],"relativePath":"guide/extend/tools.md","filePath":"guide/extend/tools.md","lastUpdated":1707642172000}'),d={name:"guide/extend/tools.md"};function n(l,e,i,c,p,m){return r(),o("div",null,e[0]||(e[0]=[t("h1",{id:"工具函数",tabindex:"-1"},[s("工具函数 "),t("a",{class:"header-anchor",href:"#工具函数","aria-label":'Permalink to "工具函数"'},"")],-1)]))}const u=a(d,[["render",n]]);export{_ as __pageData,u as default};
diff --git a/assets/guide_extend_tools.md.BELaqOF4.lean.js b/assets/guide_extend_tools.md.BELaqOF4.lean.js
new file mode 100644
index 00000000..24a625b9
--- /dev/null
+++ b/assets/guide_extend_tools.md.BELaqOF4.lean.js
@@ -0,0 +1 @@
+import{_ as a,c as o,j as t,a as s,o as r}from"./chunks/framework.p2VkXzrt.js";const _=JSON.parse('{"title":"工具函数","description":"","frontmatter":{},"headers":[],"relativePath":"guide/extend/tools.md","filePath":"guide/extend/tools.md","lastUpdated":1707642172000}'),d={name:"guide/extend/tools.md"};function n(l,e,i,c,p,m){return r(),o("div",null,e[0]||(e[0]=[t("h1",{id:"工具函数",tabindex:"-1"},[s("工具函数 "),t("a",{class:"header-anchor",href:"#工具函数","aria-label":'Permalink to "工具函数"'},"")],-1)]))}const u=a(d,[["render",n]]);export{_ as __pageData,u as default};
diff --git a/assets/guide_index.md.CbUYeeBD.js b/assets/guide_index.md.CbUYeeBD.js
new file mode 100644
index 00000000..bfbf49e8
--- /dev/null
+++ b/assets/guide_index.md.CbUYeeBD.js
@@ -0,0 +1 @@
+import{_ as t,c as e,a0 as r,o as i}from"./chunks/framework.p2VkXzrt.js";const h=JSON.parse('{"title":"前言","description":"","frontmatter":{},"headers":[],"relativePath":"guide/index.md","filePath":"guide/index.md","lastUpdated":1723293723000}'),o={name:"guide/index.md"};function l(c,a,p,s,n,d){return i(),e("div",null,a[0]||(a[0]=[r('前言 IMPORTANT
阅读本章前请确保你已阅读完毕 入门教程 。
WARNING
虽然目前开发文档已涵盖大部分基础内容,但在 v1.6 版本中刚加入的不少新特性并未在文档中更新。
前置要求 拥有一定的 JavaScript 与 Node.js 知识基础。 Kotori 运行于 Node.js 环境,因此开发 Kotori 模块前掌握 JavaScript 与 Node.js 基础内容是必然的。此处推荐几个文档:
基于 TypeScript 与现代化 ECMAScript 开发。
TypeScript 是 JavaScript 的超集,TypeScript 在继承了 JavaScript 全部特性的同时,为弱类型动态语言的 JavaScript 提供了一个独立且强大的类型系统。同时,使用 TypeScript 基本意味着使用 ESModule 与现代化的 JavaScript 语法与规范,这是 Kotori 三大特点之一。理论上在 Kotori 程序的生产环境中可正常运行由 JavaScript 直接编写的模块,但 Kotori 本身便使用 TypeScript 开发,因此更推荐你使用 TypeScript 用于你的模块开发,尽管这并不是必须的。
读后 接口文档 用于全面了解与查阅 Kotori 提供的所有公开 API。深入了解 Kotori 的开发历程、版本记录、运行流程、设计构思、设计参考等。 ',13)]))}const f=t(o,[["render",l]]);export{h as __pageData,f as default};
diff --git a/assets/guide_index.md.CbUYeeBD.lean.js b/assets/guide_index.md.CbUYeeBD.lean.js
new file mode 100644
index 00000000..bfbf49e8
--- /dev/null
+++ b/assets/guide_index.md.CbUYeeBD.lean.js
@@ -0,0 +1 @@
+import{_ as t,c as e,a0 as r,o as i}from"./chunks/framework.p2VkXzrt.js";const h=JSON.parse('{"title":"前言","description":"","frontmatter":{},"headers":[],"relativePath":"guide/index.md","filePath":"guide/index.md","lastUpdated":1723293723000}'),o={name:"guide/index.md"};function l(c,a,p,s,n,d){return i(),e("div",null,a[0]||(a[0]=[r('前言 IMPORTANT
阅读本章前请确保你已阅读完毕 入门教程 。
WARNING
虽然目前开发文档已涵盖大部分基础内容,但在 v1.6 版本中刚加入的不少新特性并未在文档中更新。
前置要求 拥有一定的 JavaScript 与 Node.js 知识基础。 Kotori 运行于 Node.js 环境,因此开发 Kotori 模块前掌握 JavaScript 与 Node.js 基础内容是必然的。此处推荐几个文档:
基于 TypeScript 与现代化 ECMAScript 开发。
TypeScript 是 JavaScript 的超集,TypeScript 在继承了 JavaScript 全部特性的同时,为弱类型动态语言的 JavaScript 提供了一个独立且强大的类型系统。同时,使用 TypeScript 基本意味着使用 ESModule 与现代化的 JavaScript 语法与规范,这是 Kotori 三大特点之一。理论上在 Kotori 程序的生产环境中可正常运行由 JavaScript 直接编写的模块,但 Kotori 本身便使用 TypeScript 开发,因此更推荐你使用 TypeScript 用于你的模块开发,尽管这并不是必须的。
读后 接口文档 用于全面了解与查阅 Kotori 提供的所有公开 API。深入了解 Kotori 的开发历程、版本记录、运行流程、设计构思、设计参考等。 ',13)]))}const f=t(o,[["render",l]]);export{h as __pageData,f as default};
diff --git a/assets/guide_modules_context.md.HAyvRJ52.js b/assets/guide_modules_context.md.HAyvRJ52.js
new file mode 100644
index 00000000..03a5ce4d
--- /dev/null
+++ b/assets/guide_modules_context.md.HAyvRJ52.js
@@ -0,0 +1,200 @@
+import{_ as i,c as a,a0 as h,o as n}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"上下文","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/context.md","filePath":"guide/modules/context.md","lastUpdated":1723293723000}'),k={name:"guide/modules/context.md"};function t(p,s,l,e,d,r){return n(),a("div",null,s[0]||(s[0]=[h(`上下文 上下文(Context) 是整个 Kotori 的核心机制,不仅是 Kotori 模块围绕着上下文实例实现一系列功能,即便是在 Kotori 内部也依赖于上下文实现各组件之间的通信与解耦合,同时也为 Kotori 的扩展提供了可能。犹如一个树根,Kotori 本身在内的各种内容均为其枝干,并通过不同的组合丰富枝干上枝叶的内容,上下文机制充分体现了**依赖注入(Dependency Injection)和 面向切面编程(Aspect Oriented Programming)**的思想。
注册与获取 上下文实例中包含诸多属性和方法,但绝大部分功能并非来源于上下文本身,而是来源于 Kotori 内部的其它组件。通过 ctx.provide()
可将指定对象注册到当前上下文实例中,并通过 ctx.get()
获取。
typescript declare class Server {}
+
+const ctx = new Context ();
+ctx . provide ( ' config ' , {
+ port : 3000 ,
+ host : ' localhost '
+});
+ctx . provide ( ' server ' , new Server ());
+
+const config = ctx . get ( ' config ' ); // { port: 3000 }
+const server = ctx . get ( ' server ' ); // Server {}
无论是对象字面量还是实例对象,都可以作为上下文实例的提供者。请注意,此处所有对象均是直接引用并未进行深拷贝。
注入与混合 使用 ctx.inject()
注入指定的已注册到当前上下文实例中的对象,注入后即可在上下文中通过注册名称直接获取到注入的实例,而无需再通过 ctx.get()
获取。
typescript ctx . provide ( ' config ' , {
+ port : 3000 ,
+ host : ' localhost '
+});
+ctx . config . port ; // TypeError: Cannot read properties of undefined (reading 'port')
+ctx . inject ( ' config ' );
+ctx . config . port ; // 3000
除了注入外,当只期望目标对象的部分属性或方法被装饰到上下文实例中时,可使用 ctx.mixin()
。
typescript ctx . provide ( ' demo ' , {
+ name : ' hello, kotori! ' ,
+ display () {
+ return this . name ;
+ }
+});
+
+ctx . display (); // Uncaught TypeError: ctx.display is not a function
+ctx . mixin ( ' demo ' , [ ' display ' ]);
+ctx . display (); // hello, kotori!
相比注入,混合更加颗粒化同时减去不必要的属性访问。无论是注入还是混合,都并非直接对对象进行复制或建立新引用,其通过代理控制对象的每个属性或方法的操作,以便解决在混合后,原对象中 this 指向等问题。
对于上面的演示代码,还可以进一步做一些对开发者友好的工作,凭借 TypeScript 中声明合并的特性,为开发者提供良好的代码补全提示。
typescript const ctx = new Context ();
+
+const config = {
+ /* ... */
+};
+const demo = {
+ /* ... */
+};
+
+declare interface Context {
+ config : typeof config ;
+ display : ( typeof demo )[ ' display ' ];
+}
+
+ctx . provide ( ' demo ' , demo );
+ctx . inject ( ' config ' );
+
+ctx . provide ( ' demo ' , config );
+ctx . mixin ( ' demo ' , [ ' display ' ]);
继承 此外,通过代理得以实现父子级上下文的概念。如,Kotori 直接给与每个模块的执行主体的上下文实例均为独一无二,它是 Kotori 内部中根上下文实例的子级上下文实例,此外也有部分上下文实例是孙级上下文实例或更深。当访问上下文中的属性或方法时,若当前上下文实例中不存在,则会沿着继承链向上查找,直到根上下文为止,这点与 JavaScript 中原型链的查找方式类似,但原理不同。使用 ctx.extends()
继承当前上下文。
typescript const ctx = new Context ();
+const ctxChild1 = ctx . extends ();
+const ctxChild2 = ctx . extends ();
+
+ctx . provide ( ' data1 ' , { value : 1 });
+ctx . inject ( ' data1 ' );
+ctx . data1 . value ; // 1
+ctxChild1 . data1 . value ; // 1
+
+ctxChild1 . provide ( ' data2 ' , { value : 2 });
+ctxChild1 . inject ( ' data2 ' );
+ctx . data2 ; // undefined
+ctxChild1 . data2 . value ; // 2
+
+ctxChild2 . provide ( ' data3 ' , { value : 3 });
+ctxChild2 . inject ( ' data3 ' );
+ctx . data3 ; // undefined
+ctxChild1 . data3 ; // undefined
+ctxChild2 . data3 . value ; // 3
可见,上下文继承后具有相对隔离性,对于子级上下文来说,只能访问自己父级上下文中注册的对象(即便是在自己被继承后注册的),而不能访问非自己父级上下文和其它子级上下文中注册的对象。而父级上下文也只能往上获取,无法往下获取自己子级上下文单独注册的对象。
typescript const ctx = new Context ();
+const ctxChild1 = ctx . extends ();
+const ctxChild2 = ctx . extends ({ meta : ' some meta data ' , ' child2 ' });
+
+ctx . meta ; // undefined
+ctxChild1 . meta ; // undefined
+ctxChild2 . meta ; //'some meta data'
+
+ctx . identity ; // undefined
+ctxChild1 . identity ; // 'sub'
+ctxChild2 . identity ; // 'child2'
在继承时,可传入两个可选参数用于标记新的子级上下文实例,第一个参数类型为对象,作用效果类似于将对象注册后并将对象上所有属性执行 ctx.mixin()
,但原理并不同,可用作传入一些子级上下文必要的元数据信息。第二个参数类型为字符串,为该子级上下文实例设置唯一标识符。对于根上下文实例而言,其标识符为 undefined
,对于未设置标识符的子级上下文实例,其标识符为 'sub'
。
typescript const ctx = new Context ();
+const ctxChild1 = ctx . extends ();
+const ctxChild2 = ctx . extends ();
+const ctxChild3 = ctxChild1 . extends ();
+
+ctx . root === ctx ; // true
+ctxChild1 . root === ctxChild1 || ctxChild1 . root === ctxChild2 ; // false
+ctxChild1 . root === ctx && ctxChild2 . root === ctx ; // true
+ctxChild3 . root === ctxChild1 ; // false
+ctxChild3 . root === ctx ; // true
通过 ctx.root
属性可获取当前上下文的根上下文实例,无论是子级上下文 ctxChild1
、ctxChild2
,还是继承了 ctxChild1
的孙级上下文 ctxChild3
,其根上下文实例均指向 ctx
,而根上下午实例的 ctx.root
指向自身。
事件系统 以上内容均由最初的 Context
类定义,通过类原生的继承方式和 ctx.inject()
、ctx.mixin()
等方法对 Context
进行装饰或扩展。而在 Context
类内部,它本身就已为自己注册并注入了两个实例对象,其一便是事件系统,这也是在第二章中介绍事件系统时说道「事件订阅者模式与事件系统共同构成了 Kotori 的基础」的原由,只不过 Context
类本身并未使用事件系统功能,且仅直接定义了 ready
与 dispose
事件,两者被作为整个程序生命周期的重要一环,其余事件由另一实例对象(见下文)或 Kotori 核心类定义。
插件系统 其二便是插件系统,它定义了 ready_module
与 dispose_module
事件。在上一节说过「在真正学习到上下文之前,可暂且默认插件等同于模块」,而现在你将会对「插件」有更深的认知。
通过 ctx.load()
加载插件并触发 ready_module
事件,且 ready_module
事件在插件加载完毕后触发。
typescript /* types */
+type ModuleInstanceClass = new ( ctx : Context , config : ModuleConfig ) => void ;
+type ModuleInstanceFunction = ( ctx : Context , config : ModuleConfig ) => void ;
+
+interface ModuleExport {
+ name ? : string ;
+ main ? : ModuleInstanceFunction ;
+ Main ? : ModuleInstanceClass ;
+ default ? : ModuleInstanceFunction | ModuleInstanceClass ;
+ inject ? : string [];
+ config ? : ModuleConfig ;
+}
+
+interface EventDataModule {
+ instance : ModuleExport | string | ModuleInstanceFunction | ModuleInstanceClass ;
+}
+
+/* index.ts */
+function plugin1 ( ctx : Context ) {
+ ctx . logger . debug ( ' plugin1 loaded ' );
+}
+
+export function main ( ctx : Context ) {
+ // output: module(main plugin) loaded
+ ctx . on ( ' read_module ' , ( data : EventDataModule ) => {
+ if ( data . instance === main ) ctx . logger . debug ( ' module(main plugin) loaded ' );
+ else if ( data . instance === plugin1 ) ctx . logger . debug ( ' plugin1(sub plugin) loaded ' );
+ });
+ ctx . load ( plugin1 ); // output: plugin1(sub plugin) loaded
+}
ctx.load()
支持四种参数形式,最常用的是直接传入执行主体函数(ModuleInstanceFunction
),此外也可以通过传入执行主体类(ModuleInstanceClass
),亦或你想为子插件传入配置或注册依赖等也可使用导出对象形式(ModuleExport
):
typescript interface SubConfig {
+ port : number ;
+}
+
+export function main ( ctx : Context ) {
+ ctx . load (
+ class {
+ public constructor ( private subCtx : Context ) {}
+ }
+ );
+ ctx . load ({
+ config : { port : 3000 },
+ main : ( subCtx : Context , cfg : SubConfig ) => {}
+ });
+}
导出对象形式与模块入口文件的导出是一致的。在 Kotori 内部,由加载器自动加载所有的模块入口文件进行预处理,然后转接给此处的 ctx.load()
进行调用执行主体。不同的是,此处可以定义 name
属性用于标记插件的名称,这将作用于该插件的上下文实例的 ctx.identity
中,而模块中的 ctx.identity
由加载器通过 package.json
中的包名自动获取。即便是子插件,它的上下文实例与配置数据也是完全独立,区别在于模块(由加载器加载)的上下文实例继承自 Kotori 内部中的根上下文实例,而子插件的上下文实例继承于当前模块的上下文实例,以此类推。入口文件中导出的 config
是一个配置检测者,加载器会调用它来验证 kotori.toml
中相应的实际配置数据是否符合要求,符合则将替换 config
为实际数据再传入 ctx.load()
作后续处理,在模块中执行 ctx.load()
,其配置数据拥有确定性(指由开发者保证,与 Kotori 无关),因此要求此处直接传入配置数据。
toml [ plugin . my-project ]
+value = ' here is a string '
typescript export const config = Tsu . Object ({
+ value : Tsu . String ()
+});
+
+export function main ( ctx : Context , cfg : Tsu . infer < typeof config >) {
+ ctx . logger . debug ( ctx . identity , cfg . value ); // my-project here is a string
+ const subCfg = {
+ value : 233
+ }
+ ctx . load ({
+ name : ' plugin1 ' ,
+ main : ( subCtx : Context , cfg : typeof 233 ) => {
+ subCtx . logger . debug ( subCtx . identity , cfg . value ); // plugin1 233
+ }
+ });
+}
子插件与当前模块的上下文实例完全独立,具有隔离性,由此可通过这一点做一些需要隔离的操作:
typescript export const inject = [];
+
+export function main ( ctx : Context ) {
+ ctx . load ({
+ name : ' plugin1 ' ,
+ inject : [ ' database ' ]
+ main : ( subCtx : Context ) => {
+ /* ctx.database... */
+ }
+ });
+ ctx . logger . debug ( ctx . database ) // undefined
+}
上述代码加载了一个依赖 database
服务的子插件,便可在其内部进行调用数据库操作,而在外层的模块中,并未依赖因此无法使用 ctx.database
属性。
当然你也可以指定多个函数主体,这将会验证上一节所讲的执行主体的识别顺序,因此这只会执行其中一个:
typescript export function main ( ctx : Context ) {
+ ctx . load ({
+ name : ' plugin1 ' ,
+ main : ( subCtx : Context ) => {
+ subCtx . logger . debug ( ' will not be loaded ' );
+ },
+ Main : class {
+ constructor ( subCtx : Context ) {
+ subCtx . logger . debug ( ' will not be loaded ' );
+ }
+ },
+ default : ( subCtx : Context ) => {
+ subCtx . logger . deug ( ' will be loaded ' );
+ }
+ });
+}
此外,也可以外层调用 CommonJS 规范的 require()
或 ESModule 规范的 import()
方法,两个方法将会返回动态导入文件的导出对象,区别在于前者是同步执行后者为异步执行,这将间接实现动态导入并加载外部 TypeScript/JavaScript 文件的插件。
typescript /** File structures
+ * src
+ * * index.ts
+ * * plugin.ts
+*/
+
+export async function main ( ctx : Context ) {
+ // Wrong way of writing
+ ctx . load ( require ( ' ./plugin.js ' ));
+ // or:
+ ctx . load ( await import ( ' ./plugin.ts ' ));
+
+ // Correct but not perfect writing
+ const file = \` ./plugin. \${ ctx . options . mode === ' dev ' ? ' .ts ' : ' .js ' } \` ;
+ ctx . load ( require ( file ));
+ // or:
+ ctx . load ( await import ( file ));
+}
[!WARN] 请慎重并正确使用该操作,绝对不可直接导入 .ts
或 .js
后缀的路径
因 Kotori 运行模式不同,直接导入带后缀的路径并不可取。在开发模式中,Kotori v1.5.0 及以上版本通过 tsx 运行,同时支持 TS/JS 文件,在 v1.5.0 以下版本通过 ts-node 运行,仅支持 TS 文件;在生产模式中,通过 Node.js 运行,仅支持 JS 文件。因此,为使你的模块更加坚固,考虑并适配不同情况是必要的。在上述代码中,通过上下文实例获取到当前运行模式以返回不同的文件扩展名动态导入,但这并不完全可靠和优雅。
typescript /** File structures
+ * src
+ * * index.ts
+ * * plugin
+ * * * index.ts
+*/
+
+import type { Context } from ' kotori-bot ' ;
+import { resolve } from ' node:path ' ;
+
+export function main ( ctx : Context ) {
+ ctx . load ( require ( resolve ( ' ./plugin ' )));
+ // Async version which better handled
+ import ( resolve ( ' ./plugin ' ))
+ . then (( plugin ) => ctx . load ( plugin ))
+ . catch (( err ) => ctx . logger . error ( ' Error in dynamic import plugin! ' , err ));
+
+}
在这一版中,通过改变文件目录结构并利用入口文件特性,以直接减少代码中多余的判断逻辑,并且通过 node:path
模块将输入路径处理成绝对路径。此外,在使用 import()
时进行异步处理与错误捕获,而非使用 await
关键字进行同步操作。对于两种方式,优缺点请自行甄别与选择使用,但值得一提的是,Kotori 加载器(@kotori-bbot/loader
)在实现自动加载目录下所有有效 npm 模块时,为杜绝异步操作的传染性,因而选择 require()
实现。
`,44)]))}const A=i(k,[["render",t]]);export{y as __pageData,A as default};
diff --git a/assets/guide_modules_context.md.HAyvRJ52.lean.js b/assets/guide_modules_context.md.HAyvRJ52.lean.js
new file mode 100644
index 00000000..03a5ce4d
--- /dev/null
+++ b/assets/guide_modules_context.md.HAyvRJ52.lean.js
@@ -0,0 +1,200 @@
+import{_ as i,c as a,a0 as h,o as n}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"上下文","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/context.md","filePath":"guide/modules/context.md","lastUpdated":1723293723000}'),k={name:"guide/modules/context.md"};function t(p,s,l,e,d,r){return n(),a("div",null,s[0]||(s[0]=[h(`上下文 上下文(Context) 是整个 Kotori 的核心机制,不仅是 Kotori 模块围绕着上下文实例实现一系列功能,即便是在 Kotori 内部也依赖于上下文实现各组件之间的通信与解耦合,同时也为 Kotori 的扩展提供了可能。犹如一个树根,Kotori 本身在内的各种内容均为其枝干,并通过不同的组合丰富枝干上枝叶的内容,上下文机制充分体现了**依赖注入(Dependency Injection)和 面向切面编程(Aspect Oriented Programming)**的思想。
注册与获取 上下文实例中包含诸多属性和方法,但绝大部分功能并非来源于上下文本身,而是来源于 Kotori 内部的其它组件。通过 ctx.provide()
可将指定对象注册到当前上下文实例中,并通过 ctx.get()
获取。
typescript declare class Server {}
+
+const ctx = new Context ();
+ctx . provide ( ' config ' , {
+ port : 3000 ,
+ host : ' localhost '
+});
+ctx . provide ( ' server ' , new Server ());
+
+const config = ctx . get ( ' config ' ); // { port: 3000 }
+const server = ctx . get ( ' server ' ); // Server {}
无论是对象字面量还是实例对象,都可以作为上下文实例的提供者。请注意,此处所有对象均是直接引用并未进行深拷贝。
注入与混合 使用 ctx.inject()
注入指定的已注册到当前上下文实例中的对象,注入后即可在上下文中通过注册名称直接获取到注入的实例,而无需再通过 ctx.get()
获取。
typescript ctx . provide ( ' config ' , {
+ port : 3000 ,
+ host : ' localhost '
+});
+ctx . config . port ; // TypeError: Cannot read properties of undefined (reading 'port')
+ctx . inject ( ' config ' );
+ctx . config . port ; // 3000
除了注入外,当只期望目标对象的部分属性或方法被装饰到上下文实例中时,可使用 ctx.mixin()
。
typescript ctx . provide ( ' demo ' , {
+ name : ' hello, kotori! ' ,
+ display () {
+ return this . name ;
+ }
+});
+
+ctx . display (); // Uncaught TypeError: ctx.display is not a function
+ctx . mixin ( ' demo ' , [ ' display ' ]);
+ctx . display (); // hello, kotori!
相比注入,混合更加颗粒化同时减去不必要的属性访问。无论是注入还是混合,都并非直接对对象进行复制或建立新引用,其通过代理控制对象的每个属性或方法的操作,以便解决在混合后,原对象中 this 指向等问题。
对于上面的演示代码,还可以进一步做一些对开发者友好的工作,凭借 TypeScript 中声明合并的特性,为开发者提供良好的代码补全提示。
typescript const ctx = new Context ();
+
+const config = {
+ /* ... */
+};
+const demo = {
+ /* ... */
+};
+
+declare interface Context {
+ config : typeof config ;
+ display : ( typeof demo )[ ' display ' ];
+}
+
+ctx . provide ( ' demo ' , demo );
+ctx . inject ( ' config ' );
+
+ctx . provide ( ' demo ' , config );
+ctx . mixin ( ' demo ' , [ ' display ' ]);
继承 此外,通过代理得以实现父子级上下文的概念。如,Kotori 直接给与每个模块的执行主体的上下文实例均为独一无二,它是 Kotori 内部中根上下文实例的子级上下文实例,此外也有部分上下文实例是孙级上下文实例或更深。当访问上下文中的属性或方法时,若当前上下文实例中不存在,则会沿着继承链向上查找,直到根上下文为止,这点与 JavaScript 中原型链的查找方式类似,但原理不同。使用 ctx.extends()
继承当前上下文。
typescript const ctx = new Context ();
+const ctxChild1 = ctx . extends ();
+const ctxChild2 = ctx . extends ();
+
+ctx . provide ( ' data1 ' , { value : 1 });
+ctx . inject ( ' data1 ' );
+ctx . data1 . value ; // 1
+ctxChild1 . data1 . value ; // 1
+
+ctxChild1 . provide ( ' data2 ' , { value : 2 });
+ctxChild1 . inject ( ' data2 ' );
+ctx . data2 ; // undefined
+ctxChild1 . data2 . value ; // 2
+
+ctxChild2 . provide ( ' data3 ' , { value : 3 });
+ctxChild2 . inject ( ' data3 ' );
+ctx . data3 ; // undefined
+ctxChild1 . data3 ; // undefined
+ctxChild2 . data3 . value ; // 3
可见,上下文继承后具有相对隔离性,对于子级上下文来说,只能访问自己父级上下文中注册的对象(即便是在自己被继承后注册的),而不能访问非自己父级上下文和其它子级上下文中注册的对象。而父级上下文也只能往上获取,无法往下获取自己子级上下文单独注册的对象。
typescript const ctx = new Context ();
+const ctxChild1 = ctx . extends ();
+const ctxChild2 = ctx . extends ({ meta : ' some meta data ' , ' child2 ' });
+
+ctx . meta ; // undefined
+ctxChild1 . meta ; // undefined
+ctxChild2 . meta ; //'some meta data'
+
+ctx . identity ; // undefined
+ctxChild1 . identity ; // 'sub'
+ctxChild2 . identity ; // 'child2'
在继承时,可传入两个可选参数用于标记新的子级上下文实例,第一个参数类型为对象,作用效果类似于将对象注册后并将对象上所有属性执行 ctx.mixin()
,但原理并不同,可用作传入一些子级上下文必要的元数据信息。第二个参数类型为字符串,为该子级上下文实例设置唯一标识符。对于根上下文实例而言,其标识符为 undefined
,对于未设置标识符的子级上下文实例,其标识符为 'sub'
。
typescript const ctx = new Context ();
+const ctxChild1 = ctx . extends ();
+const ctxChild2 = ctx . extends ();
+const ctxChild3 = ctxChild1 . extends ();
+
+ctx . root === ctx ; // true
+ctxChild1 . root === ctxChild1 || ctxChild1 . root === ctxChild2 ; // false
+ctxChild1 . root === ctx && ctxChild2 . root === ctx ; // true
+ctxChild3 . root === ctxChild1 ; // false
+ctxChild3 . root === ctx ; // true
通过 ctx.root
属性可获取当前上下文的根上下文实例,无论是子级上下文 ctxChild1
、ctxChild2
,还是继承了 ctxChild1
的孙级上下文 ctxChild3
,其根上下文实例均指向 ctx
,而根上下午实例的 ctx.root
指向自身。
事件系统 以上内容均由最初的 Context
类定义,通过类原生的继承方式和 ctx.inject()
、ctx.mixin()
等方法对 Context
进行装饰或扩展。而在 Context
类内部,它本身就已为自己注册并注入了两个实例对象,其一便是事件系统,这也是在第二章中介绍事件系统时说道「事件订阅者模式与事件系统共同构成了 Kotori 的基础」的原由,只不过 Context
类本身并未使用事件系统功能,且仅直接定义了 ready
与 dispose
事件,两者被作为整个程序生命周期的重要一环,其余事件由另一实例对象(见下文)或 Kotori 核心类定义。
插件系统 其二便是插件系统,它定义了 ready_module
与 dispose_module
事件。在上一节说过「在真正学习到上下文之前,可暂且默认插件等同于模块」,而现在你将会对「插件」有更深的认知。
通过 ctx.load()
加载插件并触发 ready_module
事件,且 ready_module
事件在插件加载完毕后触发。
typescript /* types */
+type ModuleInstanceClass = new ( ctx : Context , config : ModuleConfig ) => void ;
+type ModuleInstanceFunction = ( ctx : Context , config : ModuleConfig ) => void ;
+
+interface ModuleExport {
+ name ? : string ;
+ main ? : ModuleInstanceFunction ;
+ Main ? : ModuleInstanceClass ;
+ default ? : ModuleInstanceFunction | ModuleInstanceClass ;
+ inject ? : string [];
+ config ? : ModuleConfig ;
+}
+
+interface EventDataModule {
+ instance : ModuleExport | string | ModuleInstanceFunction | ModuleInstanceClass ;
+}
+
+/* index.ts */
+function plugin1 ( ctx : Context ) {
+ ctx . logger . debug ( ' plugin1 loaded ' );
+}
+
+export function main ( ctx : Context ) {
+ // output: module(main plugin) loaded
+ ctx . on ( ' read_module ' , ( data : EventDataModule ) => {
+ if ( data . instance === main ) ctx . logger . debug ( ' module(main plugin) loaded ' );
+ else if ( data . instance === plugin1 ) ctx . logger . debug ( ' plugin1(sub plugin) loaded ' );
+ });
+ ctx . load ( plugin1 ); // output: plugin1(sub plugin) loaded
+}
ctx.load()
支持四种参数形式,最常用的是直接传入执行主体函数(ModuleInstanceFunction
),此外也可以通过传入执行主体类(ModuleInstanceClass
),亦或你想为子插件传入配置或注册依赖等也可使用导出对象形式(ModuleExport
):
typescript interface SubConfig {
+ port : number ;
+}
+
+export function main ( ctx : Context ) {
+ ctx . load (
+ class {
+ public constructor ( private subCtx : Context ) {}
+ }
+ );
+ ctx . load ({
+ config : { port : 3000 },
+ main : ( subCtx : Context , cfg : SubConfig ) => {}
+ });
+}
导出对象形式与模块入口文件的导出是一致的。在 Kotori 内部,由加载器自动加载所有的模块入口文件进行预处理,然后转接给此处的 ctx.load()
进行调用执行主体。不同的是,此处可以定义 name
属性用于标记插件的名称,这将作用于该插件的上下文实例的 ctx.identity
中,而模块中的 ctx.identity
由加载器通过 package.json
中的包名自动获取。即便是子插件,它的上下文实例与配置数据也是完全独立,区别在于模块(由加载器加载)的上下文实例继承自 Kotori 内部中的根上下文实例,而子插件的上下文实例继承于当前模块的上下文实例,以此类推。入口文件中导出的 config
是一个配置检测者,加载器会调用它来验证 kotori.toml
中相应的实际配置数据是否符合要求,符合则将替换 config
为实际数据再传入 ctx.load()
作后续处理,在模块中执行 ctx.load()
,其配置数据拥有确定性(指由开发者保证,与 Kotori 无关),因此要求此处直接传入配置数据。
toml [ plugin . my-project ]
+value = ' here is a string '
typescript export const config = Tsu . Object ({
+ value : Tsu . String ()
+});
+
+export function main ( ctx : Context , cfg : Tsu . infer < typeof config >) {
+ ctx . logger . debug ( ctx . identity , cfg . value ); // my-project here is a string
+ const subCfg = {
+ value : 233
+ }
+ ctx . load ({
+ name : ' plugin1 ' ,
+ main : ( subCtx : Context , cfg : typeof 233 ) => {
+ subCtx . logger . debug ( subCtx . identity , cfg . value ); // plugin1 233
+ }
+ });
+}
子插件与当前模块的上下文实例完全独立,具有隔离性,由此可通过这一点做一些需要隔离的操作:
typescript export const inject = [];
+
+export function main ( ctx : Context ) {
+ ctx . load ({
+ name : ' plugin1 ' ,
+ inject : [ ' database ' ]
+ main : ( subCtx : Context ) => {
+ /* ctx.database... */
+ }
+ });
+ ctx . logger . debug ( ctx . database ) // undefined
+}
上述代码加载了一个依赖 database
服务的子插件,便可在其内部进行调用数据库操作,而在外层的模块中,并未依赖因此无法使用 ctx.database
属性。
当然你也可以指定多个函数主体,这将会验证上一节所讲的执行主体的识别顺序,因此这只会执行其中一个:
typescript export function main ( ctx : Context ) {
+ ctx . load ({
+ name : ' plugin1 ' ,
+ main : ( subCtx : Context ) => {
+ subCtx . logger . debug ( ' will not be loaded ' );
+ },
+ Main : class {
+ constructor ( subCtx : Context ) {
+ subCtx . logger . debug ( ' will not be loaded ' );
+ }
+ },
+ default : ( subCtx : Context ) => {
+ subCtx . logger . deug ( ' will be loaded ' );
+ }
+ });
+}
此外,也可以外层调用 CommonJS 规范的 require()
或 ESModule 规范的 import()
方法,两个方法将会返回动态导入文件的导出对象,区别在于前者是同步执行后者为异步执行,这将间接实现动态导入并加载外部 TypeScript/JavaScript 文件的插件。
typescript /** File structures
+ * src
+ * * index.ts
+ * * plugin.ts
+*/
+
+export async function main ( ctx : Context ) {
+ // Wrong way of writing
+ ctx . load ( require ( ' ./plugin.js ' ));
+ // or:
+ ctx . load ( await import ( ' ./plugin.ts ' ));
+
+ // Correct but not perfect writing
+ const file = \` ./plugin. \${ ctx . options . mode === ' dev ' ? ' .ts ' : ' .js ' } \` ;
+ ctx . load ( require ( file ));
+ // or:
+ ctx . load ( await import ( file ));
+}
[!WARN] 请慎重并正确使用该操作,绝对不可直接导入 .ts
或 .js
后缀的路径
因 Kotori 运行模式不同,直接导入带后缀的路径并不可取。在开发模式中,Kotori v1.5.0 及以上版本通过 tsx 运行,同时支持 TS/JS 文件,在 v1.5.0 以下版本通过 ts-node 运行,仅支持 TS 文件;在生产模式中,通过 Node.js 运行,仅支持 JS 文件。因此,为使你的模块更加坚固,考虑并适配不同情况是必要的。在上述代码中,通过上下文实例获取到当前运行模式以返回不同的文件扩展名动态导入,但这并不完全可靠和优雅。
typescript /** File structures
+ * src
+ * * index.ts
+ * * plugin
+ * * * index.ts
+*/
+
+import type { Context } from ' kotori-bot ' ;
+import { resolve } from ' node:path ' ;
+
+export function main ( ctx : Context ) {
+ ctx . load ( require ( resolve ( ' ./plugin ' )));
+ // Async version which better handled
+ import ( resolve ( ' ./plugin ' ))
+ . then (( plugin ) => ctx . load ( plugin ))
+ . catch (( err ) => ctx . logger . error ( ' Error in dynamic import plugin! ' , err ));
+
+}
在这一版中,通过改变文件目录结构并利用入口文件特性,以直接减少代码中多余的判断逻辑,并且通过 node:path
模块将输入路径处理成绝对路径。此外,在使用 import()
时进行异步处理与错误捕获,而非使用 await
关键字进行同步操作。对于两种方式,优缺点请自行甄别与选择使用,但值得一提的是,Kotori 加载器(@kotori-bbot/loader
)在实现自动加载目录下所有有效 npm 模块时,为杜绝异步操作的传染性,因而选择 require()
实现。
`,44)]))}const A=i(k,[["render",t]]);export{y as __pageData,A as default};
diff --git a/assets/guide_modules_decorator.md.C_mpKbTA.js b/assets/guide_modules_decorator.md.C_mpKbTA.js
new file mode 100644
index 00000000..45a8f62f
--- /dev/null
+++ b/assets/guide_modules_decorator.md.C_mpKbTA.js
@@ -0,0 +1 @@
+import{_ as t,c as r,j as a,a as o,o as d}from"./chunks/framework.p2VkXzrt.js";const f=JSON.parse('{"title":"装饰器","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/decorator.md","filePath":"guide/modules/decorator.md","lastUpdated":1724729588000}'),s={name:"guide/modules/decorator.md"};function c(n,e,i,l,m,p){return d(),r("div",null,e[0]||(e[0]=[a("h1",{id:"装饰器",tabindex:"-1"},[o("装饰器 "),a("a",{class:"header-anchor",href:"#装饰器","aria-label":'Permalink to "装饰器"'},"")],-1)]))}const _=t(s,[["render",c]]);export{f as __pageData,_ as default};
diff --git a/assets/guide_modules_decorator.md.C_mpKbTA.lean.js b/assets/guide_modules_decorator.md.C_mpKbTA.lean.js
new file mode 100644
index 00000000..45a8f62f
--- /dev/null
+++ b/assets/guide_modules_decorator.md.C_mpKbTA.lean.js
@@ -0,0 +1 @@
+import{_ as t,c as r,j as a,a as o,o as d}from"./chunks/framework.p2VkXzrt.js";const f=JSON.parse('{"title":"装饰器","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/decorator.md","filePath":"guide/modules/decorator.md","lastUpdated":1724729588000}'),s={name:"guide/modules/decorator.md"};function c(n,e,i,l,m,p){return d(),r("div",null,e[0]||(e[0]=[a("h1",{id:"装饰器",tabindex:"-1"},[o("装饰器 "),a("a",{class:"header-anchor",href:"#装饰器","aria-label":'Permalink to "装饰器"'},"")],-1)]))}const _=t(s,[["render",c]]);export{f as __pageData,_ as default};
diff --git a/assets/guide_modules_filter.md.BIGnsrTf.js b/assets/guide_modules_filter.md.BIGnsrTf.js
new file mode 100644
index 00000000..aa45b289
--- /dev/null
+++ b/assets/guide_modules_filter.md.BIGnsrTf.js
@@ -0,0 +1 @@
+import{_ as a,c as r,j as t,a as s,o}from"./chunks/framework.p2VkXzrt.js";const u=JSON.parse('{"title":"滤器","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/filter.md","filePath":"guide/modules/filter.md","lastUpdated":1723293723000}'),d={name:"guide/modules/filter.md"};function i(l,e,n,c,f,m){return o(),r("div",null,e[0]||(e[0]=[t("h1",{id:"滤器",tabindex:"-1"},[s("滤器 "),t("a",{class:"header-anchor",href:"#滤器","aria-label":'Permalink to "滤器"'},"")],-1)]))}const _=a(d,[["render",i]]);export{u as __pageData,_ as default};
diff --git a/assets/guide_modules_filter.md.BIGnsrTf.lean.js b/assets/guide_modules_filter.md.BIGnsrTf.lean.js
new file mode 100644
index 00000000..aa45b289
--- /dev/null
+++ b/assets/guide_modules_filter.md.BIGnsrTf.lean.js
@@ -0,0 +1 @@
+import{_ as a,c as r,j as t,a as s,o}from"./chunks/framework.p2VkXzrt.js";const u=JSON.parse('{"title":"滤器","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/filter.md","filePath":"guide/modules/filter.md","lastUpdated":1723293723000}'),d={name:"guide/modules/filter.md"};function i(l,e,n,c,f,m){return o(),r("div",null,e[0]||(e[0]=[t("h1",{id:"滤器",tabindex:"-1"},[s("滤器 "),t("a",{class:"header-anchor",href:"#滤器","aria-label":'Permalink to "滤器"'},"")],-1)]))}const _=a(d,[["render",i]]);export{u as __pageData,_ as default};
diff --git a/assets/guide_modules_i18n.md.BETTBqEz.js b/assets/guide_modules_i18n.md.BETTBqEz.js
new file mode 100644
index 00000000..9f11d001
--- /dev/null
+++ b/assets/guide_modules_i18n.md.BETTBqEz.js
@@ -0,0 +1 @@
+import{_ as t,c as r,j as a,a as s,o}from"./chunks/framework.p2VkXzrt.js";const f=JSON.parse('{"title":"国际化","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/i18n.md","filePath":"guide/modules/i18n.md","lastUpdated":1712229374000}'),d={name:"guide/modules/i18n.md"};function n(i,e,l,c,m,p){return o(),r("div",null,e[0]||(e[0]=[a("h1",{id:"国际化",tabindex:"-1"},[s("国际化 "),a("a",{class:"header-anchor",href:"#国际化","aria-label":'Permalink to "国际化"'},"")],-1)]))}const _=t(d,[["render",n]]);export{f as __pageData,_ as default};
diff --git a/assets/guide_modules_i18n.md.BETTBqEz.lean.js b/assets/guide_modules_i18n.md.BETTBqEz.lean.js
new file mode 100644
index 00000000..9f11d001
--- /dev/null
+++ b/assets/guide_modules_i18n.md.BETTBqEz.lean.js
@@ -0,0 +1 @@
+import{_ as t,c as r,j as a,a as s,o}from"./chunks/framework.p2VkXzrt.js";const f=JSON.parse('{"title":"国际化","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/i18n.md","filePath":"guide/modules/i18n.md","lastUpdated":1712229374000}'),d={name:"guide/modules/i18n.md"};function n(i,e,l,c,m,p){return o(),r("div",null,e[0]||(e[0]=[a("h1",{id:"国际化",tabindex:"-1"},[s("国际化 "),a("a",{class:"header-anchor",href:"#国际化","aria-label":'Permalink to "国际化"'},"")],-1)]))}const _=t(d,[["render",n]]);export{f as __pageData,_ as default};
diff --git a/assets/guide_modules_plugin.md.BW9Me5-D.js b/assets/guide_modules_plugin.md.BW9Me5-D.js
new file mode 100644
index 00000000..4ffeb3e5
--- /dev/null
+++ b/assets/guide_modules_plugin.md.BW9Me5-D.js
@@ -0,0 +1,176 @@
+import{_ as i,c as a,a0 as n,o as h}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"模块与插件","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/plugin.md","filePath":"guide/modules/plugin.md","lastUpdated":1723293723000}'),k={name:"guide/modules/plugin.md"};function t(l,s,p,e,d,r){return h(),a("div",null,s[0]||(s[0]=[n(`模块与插件 前言 恭喜你,只要学习完本章你将成为一名合格的「Kotori Developer」!在本章将围绕 Kotori 中最重要的概念「上下文」为你讲解一系列模块化内容。
package.json 规范 插件(Plugin) 是 Kotori 中的最小运行实例,它是模块的真子集,在真正学习到上下文之前,可暂且默认插件等同于模块。在第一章里你已通过 Cli 初步创建了一个 Kotori 模块工程,但那并不是最小的有效模块,现在,让一切重零开始。
这是一个最小且有效的 package.json 例子:
json {
+ " name " : " kotori-plugin-my-project " ,
+ " version " : " 1.0.0 " ,
+ " description " : " This is my first Kotori plugin " ,
+ " main " : " lib/index.js " ,
+ " keywords " : [ " kotori " , " chatbot " , " kotori-plugin " ],
+ " license " : " GPL-3.0 " ,
+ " files " : [ " lib " , " locales " , " LICENSE " , " README.md " ],
+ " author " : " Himeno " ,
+ " peerDependencies " : {
+ " kotori-bot " : " ^1.3.0 "
+ }
+}
TIP
请不要模仿,package.json 应附有更详尽的包信息。
一个对于 Kotori 而言合法的 package.json 的类型信息大概是这样子:
typescript interface ModulePackage {
+ name : string ;
+ version : string ;
+ description : string ;
+ main : string ;
+ license : ' GPL-3.0 ' ;
+ keywords : string [];
+ author : string | string [];
+ peerDependencies : Record < string , string >;
+ kotori ? : {
+ enforce ? : ' pre ' | ' post ' ;
+ meta ? : {
+ language ? : ' en_US ' | ' ja_JP ' | ' zh_TW ' | ' zh_CN ' ;
+ };
+ };
+}
但仅以 TypeScript 形式展现并不够全面,因为除此之外 Kotori 对合法的 package.json 有以下特殊要求:
name
必须满足 /kotori-plugin-[a-z]([a-z,0-9]{2,13})\\b/
,即以「kotori-plugin-」加一个小写字母开头,后接 2 ~ 13 个 小写字母与数字的组合license
必须为 'GPL-3.0'
,因为 Kotori 本身即使用的 GPL-3.0 协议keywords
中必须含有 'kotori'
、'chatbot'
、'kotori-plugin'
三个值,主要是为了 npm 包统计考虑peerDependencies
中必须含有名为 'kotori-bot'
的键,具体作用请参考 Peer Dependencies 对于包名,除去普通模块以外,往往会有一些非强制性规范的特殊值:
kotori-plugin-adapter-xxx
表示适配器服务kotori-plugin-database
表示数据库元数据信息 在上面例子中,可能你已注意到除了常规的属性以外,还有一个为 kotori
的属性,其会被 Kotori 读取用作模块的额外信息,目前其中仅有 meta
一个属性,meta
之下有两个属性:
enforce
模块加载顺序,对于某些前置性模块和自定义服务模块可能有用,Kotori 模块加载顺序:数据库服务 > 适配器服务 > 核心模块(模块列表请查看 Kotori 源码)> 'pre'
> undefined
> 'post'
language
模块加载列表,若为 undefined
或 []
则表示支持所有语言或无文字内容入口文件 一般地,使用 src/index.ts
作为默认入口文件,最终将由 tsc 或其它的打包工具编译成 lib/index.js
。以下是一个最基础的入口文件示例:
typescript import { Context } from ' kotori-bot ' ;
+
+export function main ( ctx : Context ) {}
入口文件一般导出一个名为 main()
的函数,接收一个 Context
实例作为参数,诸如之前介绍的事件系统、指令、中间件、正则匹配等功能均是在其上进行的操作。除此之外,入口文件还可以导出一些其他的变量,供其他模块调用。
注册国际化文件目录 typescript import { join } from ' path ' ;
+import { Context } from ' kotori-bot ' ;
+
+export function main ( ctx : Context ) {
+ ctx . i18n . use ( join ( __dirname , ' ../locales ' ));
+}
国际化文件目录(一般为 ../locales
文件夹)下有多份多个语言文件(一般为 json
文件)
此处在 main()
被调用后通过执行 ctx.i18n.use()
方法注册了当前模块的国际化文件目录,出于目录路径位置原因,此处还用到了 Node.js 内置的 path
模块的方法,但如果每个模块都需要这样做就很繁琐,Kotori 为此提供了语法糖:
typescript import { Context } from ' kotori-bot ' ;
+
+export const lang = [ __dirname , ' ../locales ' ];
+// equal to: export const lang = path.join(__dirname, '../locales');
+
+export function main ( ctx : Context ) {}
在入口文件中导出一个 lang
变量,使得 Kotori 在加载模块执行 main()
之前自动通过该变量注册国际化文件目录,lang
的值可以是字符串或数组,若为字符串则表示目录路径,若为数组则自动调用 path.join()
处理成路径字符串。
自定义模块配置 typescript import { Tsu } from ' kotori-bot ' ;
+
+/* ... */
+
+export const config = Tsu . Object ({
+ key1 : Tsu . String (),
+ key2 : Tsu . Number (). range ( 0 , 10 ),
+ key3 : Tsu . Boolean ()
+});
通过 config
变量定义模块的配置项,它是一个 Tsu.Object()
实例,并通过 Tsu.infer<>
类型推导获取配置项的类型。在模块中编写了配置项后便可直接在 Kotori 根目录的 kotori.yml
文件中进行模块配置:
toml # ...
+
+plugin:
+ my-project:
+ key1: value1
+ key2: 0
+ key3: true
通过 main()
函数的第二个参数 config
获取模块的实际配置信息:
typescript /* ... */
+
+export function main ( ctx : Context , cfg : Tsu . infer < typeof config >) {
+ ctx . logger . debug ( cfg . key1 , cfg );
+ // 'value1' { key1: 'value1', key2: 0, key3: true }
+}
设置依赖服务 typescript /* ... */
+
+export const inject = [ ' database ' ];
+
+export function main ( ctx : Context ) {
+ ctx . on ( ' ready ' , async () => {
+ if ( await ctx . db . schema . hasTable ( ' test ' )) return ;
+ await ctx . db . schema . createTable ( ' test ' , ( table ) => {
+ table . increments ();
+ table . string ( ' name ' );
+ table . timestamps ();
+ });
+ });
+}
通过 inject
变量定义模块的依赖服务,它是一个字符串数组,数组中的每个值都必须是已注册的服务名称,服务包括 Kotori 内置服务与第三方模块提供的服务。尽管服务实例只要一经定义就会因声明合并的缘故显示在 Context
实例上,但请注意,所有服务均不会自动挂载到 Context
实例上,无论是内置服务和还是第三方服务均需要使用 inject
进行声明后才可在 Context
上直接访问、使用。此处依赖了 database
数据库服务,并通过监听 ready
事件(当加载完所有模块时)进行数据库初始化操作。
模块风格与范式 Kotori 中大体上提供了三种额风格的模块范式:
导出式 整合一下上面写的所有代码:
typescript import { Context , Tsu } from ' kotori-bot ' ;
+
+export const lang = [ __dirname , ' ../locales ' ];
+
+export const config = Tsu . Object ({
+ /* ... */
+});
+
+export const inject = [ ' database ' ];
+
+export function main ( ctx : Context , cfg : Tsu . infer < typeof config >) {
+ /* ... */
+}
你会发现,无论是当前还是以往的所有演示代码都使用的导出式风格,或许称不上是 Kotori 官方推荐的模块风格,但它一定是在 Web 生态中最经典的一种风格,无论是 Vue、React 等前端响应式框架还是 Webpack、Rollup、eslint、Vite 这种工具链的插件系统都清一色的使用类似的导出式风格。就新人而言,是很推荐使用这种方式的,因为它很容易上手。
导出类式 导出式可细分成导出函数式和导出类式(这里的「导出」特指模块的执行主体),导出函数式相信你已见过太多演示就不再赘述。这里是一个与上面完全一致的导出类式示例:
typescript import { Context , Tsu } from ' kotori-bot ' ;
+
+/*
+export const lang = [__dirname, '../locales'];
+
+export const config = Tsu.Object({ /* ... */ }) ;
+
+export const inject = [ ' database ' ];
+*/
+
+export class Main {
+ public static lang = [ __dirname , ' ../locales ' ];
+
+ public static config = Tsu . Object ({ /* ... */ });
+
+ public static inject = [ ' database ' ];
+
+ public constructor (
+ private ctx : Context ,
+ private cfg : Tsu . infer < typeof config >
+ ) {
+ /* ... */
+ }
+}
在导出类式中,可同时在外部导出诸如 config
、lang
、inject
属性,也可在类中设置相应的静态属性,一般地,请使用后者。如若两者同时存在,类中的属性将会覆盖外部导出的属性。
诚然,Kotori 目前对导出类式的支持并不全面,它看起来仅仅是将原本的导出函数替换成导出类后调用其构造函数,并未充分发挥类的特性,但如果你很喜欢面向对象编程,这或许还是很适合你的。不过有一点注意,为与函数区分,导出函数式的函数名使用 main
而导出类式的类名使用 Main
,如若两者互换将不会被 Kotori 识别为有效的模块。
默认导出 无论是导出函数还是导出类,均将其称之为「模块的执行主体」,当入口文件中需要导出的只有执行主体本身时,你大可使用默认导出,此时函数名或类名都无关紧要,如:
typescript import { Context } from ' kotori-bot ' ;
+
+export default function main ( ctx : Context ) {}
又或者是默认导出一个类:
typescript import { Context } from ' kotori-bot ' ;
+
+export default class {
+ public constructor ( private ctx : Context ) {}
+}
对于执行主体的各种导出形式,以下是 Kotori 的识别顺序(一经识别成功将不再继续识别后续内容):
适配器类实现 默认导出类 默认导出函数 main()
导出函数Main
导出类直接调用式 typescript import Kotori from ' kotori-bot ' ;
+import { join } from ' path ' ;
+
+Kotori . i18n . use ( join ( __dirname , ' ../locales ' ));
+
+Kotori . on ( ' ready ' , () => {
+ const db = Kotori . get ( ' database ' );
+ if ( await db . schema . hasTable ( ' test ' )) return ;
+ /* ... */
+});
+
+Kotori . midware (( next , session ) => {
+ /* ... */
+}, 10 );
+
+Kotori . command ( /* ... */ );
+
+Kotori . regexp ( /* ... */ );
通过直接访问 kotori-bot
模块默认导出的 Kotori
对象进行各种操作,包括注册国际化文件目录、服务、中间件、指令、正则匹配等,对于服务实例则通过 ctx.get()
手动获取(或者通过 ctx.inject()
手动挂载,具体内容参考下一节)。Kotori
对象本身即为一个 Context
实例,但它并不是本体而是一个双重 Proxy
。这种方式的优点是简单和灵活,但缺点是不够模块化,且有副作用,对于开发 Kotori 模块强烈不推荐使用该方式,因为它违背了 Kotori 的原则。如果你基于 Kotori 为依赖库开发一个新的库,则推荐使用该方式。
将 Kotori 作为依赖开发请参考 深入了解
装饰器式 typescript import { Tsu , CommandAction , Context , MessageScope , plugins , SessionData } from ' kotori-bot ' ;
+
+const plugin = plugins ([ __dirname , ' ../ ' ]);
+
+@ plugin . import
+export default class Plugin {
+ private ctx : Context ;
+
+ private config : Tsu . infer < typeof Plugin . schema >;
+
+ @ plugin . lang
+ public static lang = [ __dirname , ' ../locales ' ];
+
+ @ plugin . schema
+ public static schema = Tsu . Object ({ /* ... */ });
+
+ @ plugin . inject
+ public static inject = [ ' database ' ];
+
+ public constructor ( ctx : Context , config : Tsu . infer < typeof Plugin . schema >) {
+ this . ctx = ctx ;
+ this . config = config ;
+ }
+
+ @ plugin . on ({ type : ' on_group_decrease ' })
+ public groupDecrease ( session : SessionData ) {
+ // ...
+ }
+
+ @ plugin . midware ({ priority : 10 })
+ public midware ( next : () => void , session : SessionData ) {
+ // ...
+ }
+
+ @ plugin . command ({
+ template : ' echo <content> [num:number=3] ' ,
+ scope : MessageScope . GROUP
+ })
+ public echo ( data : Parameters < CommandAction >[ 0 ], session : SessionData ) {
+ // ...
+ }
+
+ @ plugin . regexp ({ match : / ^ ( . * ) #print $ / })
+ public static print ( match : RegExpExecArray ) {
+ return match [ 1 ];
+ }
+}
以上是一个简单的装饰器式示例,与导出式相比,它的风格截然不同,语法上它足够的优雅。模块自己主动创造全局唯一的实例对象 plugin
,在其基础上使用装饰器注册的各种内容,天生即具有良好的扩展性和模块化性。装饰器特性更常见于后端或服务端语言中,在 Web 中使用较多的为 Angular、Nest.js 等深受后端架构思想(主要指 Spring)熏陶的框架。为数不多的缺点是它需要手动声明类型且对新手而言不容易上手,但如若你有足够的基础则强烈推荐使用。
当然,这并不算在此展开详细介绍,它还需要你了解一点其它内容作为基础,因而它被放在本章最后一节进行具体讲述。
`,63)]))}const o=i(k,[["render",t]]);export{y as __pageData,o as default};
diff --git a/assets/guide_modules_plugin.md.BW9Me5-D.lean.js b/assets/guide_modules_plugin.md.BW9Me5-D.lean.js
new file mode 100644
index 00000000..4ffeb3e5
--- /dev/null
+++ b/assets/guide_modules_plugin.md.BW9Me5-D.lean.js
@@ -0,0 +1,176 @@
+import{_ as i,c as a,a0 as n,o as h}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"模块与插件","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/plugin.md","filePath":"guide/modules/plugin.md","lastUpdated":1723293723000}'),k={name:"guide/modules/plugin.md"};function t(l,s,p,e,d,r){return h(),a("div",null,s[0]||(s[0]=[n(`模块与插件 前言 恭喜你,只要学习完本章你将成为一名合格的「Kotori Developer」!在本章将围绕 Kotori 中最重要的概念「上下文」为你讲解一系列模块化内容。
package.json 规范 插件(Plugin) 是 Kotori 中的最小运行实例,它是模块的真子集,在真正学习到上下文之前,可暂且默认插件等同于模块。在第一章里你已通过 Cli 初步创建了一个 Kotori 模块工程,但那并不是最小的有效模块,现在,让一切重零开始。
这是一个最小且有效的 package.json 例子:
json {
+ " name " : " kotori-plugin-my-project " ,
+ " version " : " 1.0.0 " ,
+ " description " : " This is my first Kotori plugin " ,
+ " main " : " lib/index.js " ,
+ " keywords " : [ " kotori " , " chatbot " , " kotori-plugin " ],
+ " license " : " GPL-3.0 " ,
+ " files " : [ " lib " , " locales " , " LICENSE " , " README.md " ],
+ " author " : " Himeno " ,
+ " peerDependencies " : {
+ " kotori-bot " : " ^1.3.0 "
+ }
+}
TIP
请不要模仿,package.json 应附有更详尽的包信息。
一个对于 Kotori 而言合法的 package.json 的类型信息大概是这样子:
typescript interface ModulePackage {
+ name : string ;
+ version : string ;
+ description : string ;
+ main : string ;
+ license : ' GPL-3.0 ' ;
+ keywords : string [];
+ author : string | string [];
+ peerDependencies : Record < string , string >;
+ kotori ? : {
+ enforce ? : ' pre ' | ' post ' ;
+ meta ? : {
+ language ? : ' en_US ' | ' ja_JP ' | ' zh_TW ' | ' zh_CN ' ;
+ };
+ };
+}
但仅以 TypeScript 形式展现并不够全面,因为除此之外 Kotori 对合法的 package.json 有以下特殊要求:
name
必须满足 /kotori-plugin-[a-z]([a-z,0-9]{2,13})\\b/
,即以「kotori-plugin-」加一个小写字母开头,后接 2 ~ 13 个 小写字母与数字的组合license
必须为 'GPL-3.0'
,因为 Kotori 本身即使用的 GPL-3.0 协议keywords
中必须含有 'kotori'
、'chatbot'
、'kotori-plugin'
三个值,主要是为了 npm 包统计考虑peerDependencies
中必须含有名为 'kotori-bot'
的键,具体作用请参考 Peer Dependencies 对于包名,除去普通模块以外,往往会有一些非强制性规范的特殊值:
kotori-plugin-adapter-xxx
表示适配器服务kotori-plugin-database
表示数据库元数据信息 在上面例子中,可能你已注意到除了常规的属性以外,还有一个为 kotori
的属性,其会被 Kotori 读取用作模块的额外信息,目前其中仅有 meta
一个属性,meta
之下有两个属性:
enforce
模块加载顺序,对于某些前置性模块和自定义服务模块可能有用,Kotori 模块加载顺序:数据库服务 > 适配器服务 > 核心模块(模块列表请查看 Kotori 源码)> 'pre'
> undefined
> 'post'
language
模块加载列表,若为 undefined
或 []
则表示支持所有语言或无文字内容入口文件 一般地,使用 src/index.ts
作为默认入口文件,最终将由 tsc 或其它的打包工具编译成 lib/index.js
。以下是一个最基础的入口文件示例:
typescript import { Context } from ' kotori-bot ' ;
+
+export function main ( ctx : Context ) {}
入口文件一般导出一个名为 main()
的函数,接收一个 Context
实例作为参数,诸如之前介绍的事件系统、指令、中间件、正则匹配等功能均是在其上进行的操作。除此之外,入口文件还可以导出一些其他的变量,供其他模块调用。
注册国际化文件目录 typescript import { join } from ' path ' ;
+import { Context } from ' kotori-bot ' ;
+
+export function main ( ctx : Context ) {
+ ctx . i18n . use ( join ( __dirname , ' ../locales ' ));
+}
国际化文件目录(一般为 ../locales
文件夹)下有多份多个语言文件(一般为 json
文件)
此处在 main()
被调用后通过执行 ctx.i18n.use()
方法注册了当前模块的国际化文件目录,出于目录路径位置原因,此处还用到了 Node.js 内置的 path
模块的方法,但如果每个模块都需要这样做就很繁琐,Kotori 为此提供了语法糖:
typescript import { Context } from ' kotori-bot ' ;
+
+export const lang = [ __dirname , ' ../locales ' ];
+// equal to: export const lang = path.join(__dirname, '../locales');
+
+export function main ( ctx : Context ) {}
在入口文件中导出一个 lang
变量,使得 Kotori 在加载模块执行 main()
之前自动通过该变量注册国际化文件目录,lang
的值可以是字符串或数组,若为字符串则表示目录路径,若为数组则自动调用 path.join()
处理成路径字符串。
自定义模块配置 typescript import { Tsu } from ' kotori-bot ' ;
+
+/* ... */
+
+export const config = Tsu . Object ({
+ key1 : Tsu . String (),
+ key2 : Tsu . Number (). range ( 0 , 10 ),
+ key3 : Tsu . Boolean ()
+});
通过 config
变量定义模块的配置项,它是一个 Tsu.Object()
实例,并通过 Tsu.infer<>
类型推导获取配置项的类型。在模块中编写了配置项后便可直接在 Kotori 根目录的 kotori.yml
文件中进行模块配置:
toml # ...
+
+plugin:
+ my-project:
+ key1: value1
+ key2: 0
+ key3: true
通过 main()
函数的第二个参数 config
获取模块的实际配置信息:
typescript /* ... */
+
+export function main ( ctx : Context , cfg : Tsu . infer < typeof config >) {
+ ctx . logger . debug ( cfg . key1 , cfg );
+ // 'value1' { key1: 'value1', key2: 0, key3: true }
+}
设置依赖服务 typescript /* ... */
+
+export const inject = [ ' database ' ];
+
+export function main ( ctx : Context ) {
+ ctx . on ( ' ready ' , async () => {
+ if ( await ctx . db . schema . hasTable ( ' test ' )) return ;
+ await ctx . db . schema . createTable ( ' test ' , ( table ) => {
+ table . increments ();
+ table . string ( ' name ' );
+ table . timestamps ();
+ });
+ });
+}
通过 inject
变量定义模块的依赖服务,它是一个字符串数组,数组中的每个值都必须是已注册的服务名称,服务包括 Kotori 内置服务与第三方模块提供的服务。尽管服务实例只要一经定义就会因声明合并的缘故显示在 Context
实例上,但请注意,所有服务均不会自动挂载到 Context
实例上,无论是内置服务和还是第三方服务均需要使用 inject
进行声明后才可在 Context
上直接访问、使用。此处依赖了 database
数据库服务,并通过监听 ready
事件(当加载完所有模块时)进行数据库初始化操作。
模块风格与范式 Kotori 中大体上提供了三种额风格的模块范式:
导出式 整合一下上面写的所有代码:
typescript import { Context , Tsu } from ' kotori-bot ' ;
+
+export const lang = [ __dirname , ' ../locales ' ];
+
+export const config = Tsu . Object ({
+ /* ... */
+});
+
+export const inject = [ ' database ' ];
+
+export function main ( ctx : Context , cfg : Tsu . infer < typeof config >) {
+ /* ... */
+}
你会发现,无论是当前还是以往的所有演示代码都使用的导出式风格,或许称不上是 Kotori 官方推荐的模块风格,但它一定是在 Web 生态中最经典的一种风格,无论是 Vue、React 等前端响应式框架还是 Webpack、Rollup、eslint、Vite 这种工具链的插件系统都清一色的使用类似的导出式风格。就新人而言,是很推荐使用这种方式的,因为它很容易上手。
导出类式 导出式可细分成导出函数式和导出类式(这里的「导出」特指模块的执行主体),导出函数式相信你已见过太多演示就不再赘述。这里是一个与上面完全一致的导出类式示例:
typescript import { Context , Tsu } from ' kotori-bot ' ;
+
+/*
+export const lang = [__dirname, '../locales'];
+
+export const config = Tsu.Object({ /* ... */ }) ;
+
+export const inject = [ ' database ' ];
+*/
+
+export class Main {
+ public static lang = [ __dirname , ' ../locales ' ];
+
+ public static config = Tsu . Object ({ /* ... */ });
+
+ public static inject = [ ' database ' ];
+
+ public constructor (
+ private ctx : Context ,
+ private cfg : Tsu . infer < typeof config >
+ ) {
+ /* ... */
+ }
+}
在导出类式中,可同时在外部导出诸如 config
、lang
、inject
属性,也可在类中设置相应的静态属性,一般地,请使用后者。如若两者同时存在,类中的属性将会覆盖外部导出的属性。
诚然,Kotori 目前对导出类式的支持并不全面,它看起来仅仅是将原本的导出函数替换成导出类后调用其构造函数,并未充分发挥类的特性,但如果你很喜欢面向对象编程,这或许还是很适合你的。不过有一点注意,为与函数区分,导出函数式的函数名使用 main
而导出类式的类名使用 Main
,如若两者互换将不会被 Kotori 识别为有效的模块。
默认导出 无论是导出函数还是导出类,均将其称之为「模块的执行主体」,当入口文件中需要导出的只有执行主体本身时,你大可使用默认导出,此时函数名或类名都无关紧要,如:
typescript import { Context } from ' kotori-bot ' ;
+
+export default function main ( ctx : Context ) {}
又或者是默认导出一个类:
typescript import { Context } from ' kotori-bot ' ;
+
+export default class {
+ public constructor ( private ctx : Context ) {}
+}
对于执行主体的各种导出形式,以下是 Kotori 的识别顺序(一经识别成功将不再继续识别后续内容):
适配器类实现 默认导出类 默认导出函数 main()
导出函数Main
导出类直接调用式 typescript import Kotori from ' kotori-bot ' ;
+import { join } from ' path ' ;
+
+Kotori . i18n . use ( join ( __dirname , ' ../locales ' ));
+
+Kotori . on ( ' ready ' , () => {
+ const db = Kotori . get ( ' database ' );
+ if ( await db . schema . hasTable ( ' test ' )) return ;
+ /* ... */
+});
+
+Kotori . midware (( next , session ) => {
+ /* ... */
+}, 10 );
+
+Kotori . command ( /* ... */ );
+
+Kotori . regexp ( /* ... */ );
通过直接访问 kotori-bot
模块默认导出的 Kotori
对象进行各种操作,包括注册国际化文件目录、服务、中间件、指令、正则匹配等,对于服务实例则通过 ctx.get()
手动获取(或者通过 ctx.inject()
手动挂载,具体内容参考下一节)。Kotori
对象本身即为一个 Context
实例,但它并不是本体而是一个双重 Proxy
。这种方式的优点是简单和灵活,但缺点是不够模块化,且有副作用,对于开发 Kotori 模块强烈不推荐使用该方式,因为它违背了 Kotori 的原则。如果你基于 Kotori 为依赖库开发一个新的库,则推荐使用该方式。
将 Kotori 作为依赖开发请参考 深入了解
装饰器式 typescript import { Tsu , CommandAction , Context , MessageScope , plugins , SessionData } from ' kotori-bot ' ;
+
+const plugin = plugins ([ __dirname , ' ../ ' ]);
+
+@ plugin . import
+export default class Plugin {
+ private ctx : Context ;
+
+ private config : Tsu . infer < typeof Plugin . schema >;
+
+ @ plugin . lang
+ public static lang = [ __dirname , ' ../locales ' ];
+
+ @ plugin . schema
+ public static schema = Tsu . Object ({ /* ... */ });
+
+ @ plugin . inject
+ public static inject = [ ' database ' ];
+
+ public constructor ( ctx : Context , config : Tsu . infer < typeof Plugin . schema >) {
+ this . ctx = ctx ;
+ this . config = config ;
+ }
+
+ @ plugin . on ({ type : ' on_group_decrease ' })
+ public groupDecrease ( session : SessionData ) {
+ // ...
+ }
+
+ @ plugin . midware ({ priority : 10 })
+ public midware ( next : () => void , session : SessionData ) {
+ // ...
+ }
+
+ @ plugin . command ({
+ template : ' echo <content> [num:number=3] ' ,
+ scope : MessageScope . GROUP
+ })
+ public echo ( data : Parameters < CommandAction >[ 0 ], session : SessionData ) {
+ // ...
+ }
+
+ @ plugin . regexp ({ match : / ^ ( . * ) #print $ / })
+ public static print ( match : RegExpExecArray ) {
+ return match [ 1 ];
+ }
+}
以上是一个简单的装饰器式示例,与导出式相比,它的风格截然不同,语法上它足够的优雅。模块自己主动创造全局唯一的实例对象 plugin
,在其基础上使用装饰器注册的各种内容,天生即具有良好的扩展性和模块化性。装饰器特性更常见于后端或服务端语言中,在 Web 中使用较多的为 Angular、Nest.js 等深受后端架构思想(主要指 Spring)熏陶的框架。为数不多的缺点是它需要手动声明类型且对新手而言不容易上手,但如若你有足够的基础则强烈推荐使用。
当然,这并不算在此展开详细介绍,它还需要你了解一点其它内容作为基础,因而它被放在本章最后一节进行具体讲述。
`,63)]))}const o=i(k,[["render",t]]);export{y as __pageData,o as default};
diff --git a/assets/guide_modules_rescript.md.zh3sC6a0.js b/assets/guide_modules_rescript.md.zh3sC6a0.js
new file mode 100644
index 00000000..03cbc2c8
--- /dev/null
+++ b/assets/guide_modules_rescript.md.zh3sC6a0.js
@@ -0,0 +1,8 @@
+import{_ as i,c as a,a0 as t,o as n}from"./chunks/framework.p2VkXzrt.js";const c=JSON.parse('{"title":"使用 ReScript 开发","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/rescript.md","filePath":"guide/modules/rescript.md","lastUpdated":1731827740000}'),h={name:"guide/modules/rescript.md"};function p(e,s,k,l,r,d){return n(),a("div",null,s[0]||(s[0]=[t(`使用 ReScript 开发 ReScript 是一门健壮的类型化语言,可以编译成高效易读的 JavaScript。相比于 TypeScript,ReScript 是 JavaScript 的子集,有着远比 TypeScript 更为严格和安全的类型系统。也是 OCaml 的方言之一,结合了大量函数式编程与现代化编程特性,同时保留了 C 系语言的花括号语法风格,这使你不会像面对其它函数式编程一样对其陌生语法感到茫然,变得极易上手和入门。如果你是一名 Rust 开发者将会对 ReScript 很多地方感到亲切(就像是没有所有权和生命周期的 Rust)。
基本使用 Kotori 从 v1.7 开始支持用 ReScript 编写插件,尽管这并非强制性,但如若你对函数式编程感兴趣或者对安全性有要求,那么使用 ReScript 编写 Kotori 插件将是不二之举。
res let main = ( ctx : Kotori . context ) => {
+ open Kotori . Utils ;
+
+ "echo <message> - print string" -> ctx . cmd . new -> ctx . cmd . action ( async ({ args : [ msg ]}, session ) => {
+ msg -> session . quick
+ ""
+ }) -> ignore
+}
`,6)]))}const o=i(h,[["render",p]]);export{c as __pageData,o as default};
diff --git a/assets/guide_modules_rescript.md.zh3sC6a0.lean.js b/assets/guide_modules_rescript.md.zh3sC6a0.lean.js
new file mode 100644
index 00000000..03cbc2c8
--- /dev/null
+++ b/assets/guide_modules_rescript.md.zh3sC6a0.lean.js
@@ -0,0 +1,8 @@
+import{_ as i,c as a,a0 as t,o as n}from"./chunks/framework.p2VkXzrt.js";const c=JSON.parse('{"title":"使用 ReScript 开发","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/rescript.md","filePath":"guide/modules/rescript.md","lastUpdated":1731827740000}'),h={name:"guide/modules/rescript.md"};function p(e,s,k,l,r,d){return n(),a("div",null,s[0]||(s[0]=[t(`使用 ReScript 开发 ReScript 是一门健壮的类型化语言,可以编译成高效易读的 JavaScript。相比于 TypeScript,ReScript 是 JavaScript 的子集,有着远比 TypeScript 更为严格和安全的类型系统。也是 OCaml 的方言之一,结合了大量函数式编程与现代化编程特性,同时保留了 C 系语言的花括号语法风格,这使你不会像面对其它函数式编程一样对其陌生语法感到茫然,变得极易上手和入门。如果你是一名 Rust 开发者将会对 ReScript 很多地方感到亲切(就像是没有所有权和生命周期的 Rust)。
基本使用 Kotori 从 v1.7 开始支持用 ReScript 编写插件,尽管这并非强制性,但如若你对函数式编程感兴趣或者对安全性有要求,那么使用 ReScript 编写 Kotori 插件将是不二之举。
res let main = ( ctx : Kotori . context ) => {
+ open Kotori . Utils ;
+
+ "echo <message> - print string" -> ctx . cmd . new -> ctx . cmd . action ( async ({ args : [ msg ]}, session ) => {
+ msg -> session . quick
+ ""
+ }) -> ignore
+}
`,6)]))}const o=i(h,[["render",p]]);export{c as __pageData,o as default};
diff --git a/assets/guide_modules_schema.md.B5KeilZE.js b/assets/guide_modules_schema.md.B5KeilZE.js
new file mode 100644
index 00000000..10aef533
--- /dev/null
+++ b/assets/guide_modules_schema.md.B5KeilZE.js
@@ -0,0 +1,49 @@
+import{_ as i,c as a,a0 as h,o as k}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"配置检测","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/schema.md","filePath":"guide/modules/schema.md","lastUpdated":1724729588000}'),n={name:"guide/modules/schema.md"};function t(l,s,p,e,r,d){return k(),a("div",null,s[0]||(s[0]=[h(`配置检测 配置检测(Schema) 是 Kotori 中的一个重要概念和功能,其相关的所有实现均来源于 Tsukiko 库。Kotori 对 Tsukiko 进行了重新导出,因此可直接在 Kotori 中使用。
Tsukiko 简介 Tsukiko 是一个基于 TypeScript 开发的运行时下动态类型检查库,最初作为 kotori 开发中的副产物诞生,其作用与应用场景类似于 io-ts 之类的库,常用于 JSON/YAML/TOML 文件数据格式检验、第三方 HTTP API 数据格式检验、数据库返回数据格式检验(尽管此处推荐直接用更为成熟的 ORM 框架)等。
视频介绍与演示:哔哩哔哩
项目名字取自于轻小说《変態王子と笑わない猫。》中的女主角——筒隠月子(Tsukakushi Tsukiko)
基本使用 类型检验 Tsukiko 中带有多种类型解析器,通过不同的解析器可以实现对未知值的类型校验与处理:
typescript import { Tsu } from ' kotori-bot '
+
+const strSchema = Tsu . String ()
+strSchema . check ( 233 ) // false
+strSchema . check ( ' Hello,Tsukiko! ' ) // true
schema.check()
接收一个参数,返回值表示该参数类型是否匹配。此外,与之类似的还有以下多种校验方法:
typescript /* ... */
+
+const value = strSchema . parse ( raw )
+// if passed the value must be a string
+// if not passrd: throw TsuError
schema.parse()
会处理传入值并判断是否符合要求,如若不符合将抛出错误(TsuError
)并附带详细原因。不过有时并不想直接抛出错误则可以使用 schema.parseSafe()
:
typescript /* ... */
+
+const result = strSchema . parseSafe ( raw )
+if ( result . value ) {
+ console . log ( ' Passed: ' , result . data )
+
+} else {
+ console . log ( " Error: " , result . error . message )
+}
该方法会返回一个对象,当 value
为 true
时,对象上存在 data
属性,其值即为处理后的结果,当 value
为 false
时,对象上存在 error
属性,其值即为错误信息。此外,还有一个异步版本 schema.parseAsync
:
typescript /* ... */
+
+strSchema . parseAsync ( raw )
+ . then (( data ) => console . log ( ' Passed ' , data ))
+ . catch (( error ) => console . log ( ' Fatal ' , error ))
上面有提到,schema.parse()
及相关的解析方法,在传入值符合要求时返回的数据会经过一定的处理,其主要体现为默认值处理:
typescript const schema = Tsu . Object ({
+ name : Tsu . String (). default ( ' Romi ' ),
+ age : Tsu . Stting (). default ( 16 )
+}). default ({
+ name : ' Romi ' ,
+ age : 16
+})
+
+schema . parse ({ name : ' Yuki ' , age : 17 }) // Passed { name: 'Yuki', age: 17 }
+schema . parse ({ name : ' Kisaki ' }) // Passed { name: 'Kisaki', age: 16 }
+schema . parse ({}) // Passed { name: 'Romi', age: 16 }
+schema . parse ([]) // Error
在不同的解析器下也有一定的体现,如:
typescript const strSchema = Tsu . String ()
+
+strSchema ( 233 ) // Passed '233'
+strSchema ( true ) // Error
Tsu.String()
解析器默认允许数字传入(出于兼容性考虑),并会将其处理成字符串返回。
类型修饰 最典型的修饰方法为 schema.default()
与 schema.optional()
,前者用于设置默认值,后者用于设置可选类型:
typescript const numSchema = Tsu . Number (). default ( 2333 )
+
+numSchema . parse () // 2333
+
+const strSchema = Tsu . String (). optional ()
+
+strSchema . parse ( undefined ) // Passed
+strSchema . parse ( null ) // Passed
同样是出于兼容性考虑,解析器默认会允许 null
,如若想只允许 undefined
作为空值,则可以使用 schema.empty()
:
typescript
+const strSchema = Tsu . String (). optional (). empty ()
+
+strSchema . parse ( undefined ) // Passed
+strSchema . parse ( null ) // Error
除去以上所有解析器共有方法以外,每个解析器也有自己专门的修饰方法,详情可查看下文。可以看到,在 schema.optional()
之后可以继续调用方法,因为包括以上在内的绝大部分修饰方法都会返回当前实例,链式调用便是 Tsukiko 最大特点。不过,同一个修饰方法应当在同一个解析器中仅调用一次,因为不同的修饰方法其执行行为有所不同:
开关解析器上的某一内部属性(如 Tsu.String()
与 Tsu.Object()
上均存在的 .strict()
),这使得可以在调用方法时传入一个 Boolean
值 ,一般地,这些方法的参数会有默认值 单向设置解析器上的某一内部属性(如上文的 schema.optional()
、schema.empty()
、schema.default()
),大部分不需要传参,但也有些需要传参 多个同级方法间合并解析器上的多个内部属性(如很多解析器上都存在的 .min()
、.max()
、range()
) 类型导出 JSON Schema 是用于验证 JSON 数据结构的强大工具。在必要时可通过 schema.schema()
将任意解析器导出成 JSON Schema。不过在此之前,Tsukiko 提供了额外两个关于 JSON Schema 的新方法:
typescript const config = Tsu . Object ({
+ port : Tsu . Number (). port (). describe ( ' Server port ' )
+ address : Tsu . String (). describe ( ' Server display address ' )
+}). title ( ' Plugin configuration ' )
schema.
解析器 基础类型 引用类型 标准类型 高级类型 在 Kotori 中的引用 配置文件 数据检验 `,40)]))}const c=i(n,[["render",t]]);export{y as __pageData,c as default};
diff --git a/assets/guide_modules_schema.md.B5KeilZE.lean.js b/assets/guide_modules_schema.md.B5KeilZE.lean.js
new file mode 100644
index 00000000..10aef533
--- /dev/null
+++ b/assets/guide_modules_schema.md.B5KeilZE.lean.js
@@ -0,0 +1,49 @@
+import{_ as i,c as a,a0 as h,o as k}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"配置检测","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/schema.md","filePath":"guide/modules/schema.md","lastUpdated":1724729588000}'),n={name:"guide/modules/schema.md"};function t(l,s,p,e,r,d){return k(),a("div",null,s[0]||(s[0]=[h(`配置检测 配置检测(Schema) 是 Kotori 中的一个重要概念和功能,其相关的所有实现均来源于 Tsukiko 库。Kotori 对 Tsukiko 进行了重新导出,因此可直接在 Kotori 中使用。
Tsukiko 简介 Tsukiko 是一个基于 TypeScript 开发的运行时下动态类型检查库,最初作为 kotori 开发中的副产物诞生,其作用与应用场景类似于 io-ts 之类的库,常用于 JSON/YAML/TOML 文件数据格式检验、第三方 HTTP API 数据格式检验、数据库返回数据格式检验(尽管此处推荐直接用更为成熟的 ORM 框架)等。
视频介绍与演示:哔哩哔哩
项目名字取自于轻小说《変態王子と笑わない猫。》中的女主角——筒隠月子(Tsukakushi Tsukiko)
基本使用 类型检验 Tsukiko 中带有多种类型解析器,通过不同的解析器可以实现对未知值的类型校验与处理:
typescript import { Tsu } from ' kotori-bot '
+
+const strSchema = Tsu . String ()
+strSchema . check ( 233 ) // false
+strSchema . check ( ' Hello,Tsukiko! ' ) // true
schema.check()
接收一个参数,返回值表示该参数类型是否匹配。此外,与之类似的还有以下多种校验方法:
typescript /* ... */
+
+const value = strSchema . parse ( raw )
+// if passed the value must be a string
+// if not passrd: throw TsuError
schema.parse()
会处理传入值并判断是否符合要求,如若不符合将抛出错误(TsuError
)并附带详细原因。不过有时并不想直接抛出错误则可以使用 schema.parseSafe()
:
typescript /* ... */
+
+const result = strSchema . parseSafe ( raw )
+if ( result . value ) {
+ console . log ( ' Passed: ' , result . data )
+
+} else {
+ console . log ( " Error: " , result . error . message )
+}
该方法会返回一个对象,当 value
为 true
时,对象上存在 data
属性,其值即为处理后的结果,当 value
为 false
时,对象上存在 error
属性,其值即为错误信息。此外,还有一个异步版本 schema.parseAsync
:
typescript /* ... */
+
+strSchema . parseAsync ( raw )
+ . then (( data ) => console . log ( ' Passed ' , data ))
+ . catch (( error ) => console . log ( ' Fatal ' , error ))
上面有提到,schema.parse()
及相关的解析方法,在传入值符合要求时返回的数据会经过一定的处理,其主要体现为默认值处理:
typescript const schema = Tsu . Object ({
+ name : Tsu . String (). default ( ' Romi ' ),
+ age : Tsu . Stting (). default ( 16 )
+}). default ({
+ name : ' Romi ' ,
+ age : 16
+})
+
+schema . parse ({ name : ' Yuki ' , age : 17 }) // Passed { name: 'Yuki', age: 17 }
+schema . parse ({ name : ' Kisaki ' }) // Passed { name: 'Kisaki', age: 16 }
+schema . parse ({}) // Passed { name: 'Romi', age: 16 }
+schema . parse ([]) // Error
在不同的解析器下也有一定的体现,如:
typescript const strSchema = Tsu . String ()
+
+strSchema ( 233 ) // Passed '233'
+strSchema ( true ) // Error
Tsu.String()
解析器默认允许数字传入(出于兼容性考虑),并会将其处理成字符串返回。
类型修饰 最典型的修饰方法为 schema.default()
与 schema.optional()
,前者用于设置默认值,后者用于设置可选类型:
typescript const numSchema = Tsu . Number (). default ( 2333 )
+
+numSchema . parse () // 2333
+
+const strSchema = Tsu . String (). optional ()
+
+strSchema . parse ( undefined ) // Passed
+strSchema . parse ( null ) // Passed
同样是出于兼容性考虑,解析器默认会允许 null
,如若想只允许 undefined
作为空值,则可以使用 schema.empty()
:
typescript
+const strSchema = Tsu . String (). optional (). empty ()
+
+strSchema . parse ( undefined ) // Passed
+strSchema . parse ( null ) // Error
除去以上所有解析器共有方法以外,每个解析器也有自己专门的修饰方法,详情可查看下文。可以看到,在 schema.optional()
之后可以继续调用方法,因为包括以上在内的绝大部分修饰方法都会返回当前实例,链式调用便是 Tsukiko 最大特点。不过,同一个修饰方法应当在同一个解析器中仅调用一次,因为不同的修饰方法其执行行为有所不同:
开关解析器上的某一内部属性(如 Tsu.String()
与 Tsu.Object()
上均存在的 .strict()
),这使得可以在调用方法时传入一个 Boolean
值 ,一般地,这些方法的参数会有默认值 单向设置解析器上的某一内部属性(如上文的 schema.optional()
、schema.empty()
、schema.default()
),大部分不需要传参,但也有些需要传参 多个同级方法间合并解析器上的多个内部属性(如很多解析器上都存在的 .min()
、.max()
、range()
) 类型导出 JSON Schema 是用于验证 JSON 数据结构的强大工具。在必要时可通过 schema.schema()
将任意解析器导出成 JSON Schema。不过在此之前,Tsukiko 提供了额外两个关于 JSON Schema 的新方法:
typescript const config = Tsu . Object ({
+ port : Tsu . Number (). port (). describe ( ' Server port ' )
+ address : Tsu . String (). describe ( ' Server display address ' )
+}). title ( ' Plugin configuration ' )
schema.
解析器 基础类型 引用类型 标准类型 高级类型 在 Kotori 中的引用 配置文件 数据检验 `,40)]))}const c=i(n,[["render",t]]);export{y as __pageData,c as default};
diff --git a/assets/guide_modules_service.md.DxB9k1Hz.js b/assets/guide_modules_service.md.DxB9k1Hz.js
new file mode 100644
index 00000000..2525c93f
--- /dev/null
+++ b/assets/guide_modules_service.md.DxB9k1Hz.js
@@ -0,0 +1 @@
+import{_ as t,c as r,j as a,a as s,o}from"./chunks/framework.p2VkXzrt.js";const f=JSON.parse('{"title":"依赖与服务","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/service.md","filePath":"guide/modules/service.md","lastUpdated":1712229374000}'),d={name:"guide/modules/service.md"};function i(c,e,n,l,m,p){return o(),r("div",null,e[0]||(e[0]=[a("h1",{id:"依赖与服务",tabindex:"-1"},[s("依赖与服务 "),a("a",{class:"header-anchor",href:"#依赖与服务","aria-label":'Permalink to "依赖与服务"'},"")],-1)]))}const _=t(d,[["render",i]]);export{f as __pageData,_ as default};
diff --git a/assets/guide_modules_service.md.DxB9k1Hz.lean.js b/assets/guide_modules_service.md.DxB9k1Hz.lean.js
new file mode 100644
index 00000000..2525c93f
--- /dev/null
+++ b/assets/guide_modules_service.md.DxB9k1Hz.lean.js
@@ -0,0 +1 @@
+import{_ as t,c as r,j as a,a as s,o}from"./chunks/framework.p2VkXzrt.js";const f=JSON.parse('{"title":"依赖与服务","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/service.md","filePath":"guide/modules/service.md","lastUpdated":1712229374000}'),d={name:"guide/modules/service.md"};function i(c,e,n,l,m,p){return o(),r("div",null,e[0]||(e[0]=[a("h1",{id:"依赖与服务",tabindex:"-1"},[s("依赖与服务 "),a("a",{class:"header-anchor",href:"#依赖与服务","aria-label":'Permalink to "依赖与服务"'},"")],-1)]))}const _=t(d,[["render",i]]);export{f as __pageData,_ as default};
diff --git a/assets/guide_start_environment.md.DbqmpXqr.js b/assets/guide_start_environment.md.DbqmpXqr.js
new file mode 100644
index 00000000..aa195a50
--- /dev/null
+++ b/assets/guide_start_environment.md.DbqmpXqr.js
@@ -0,0 +1 @@
+import{_ as e,c as a,a0 as r,o as i}from"./chunks/framework.p2VkXzrt.js";const u=JSON.parse('{"title":"环境搭建","description":"","frontmatter":{},"headers":[],"relativePath":"guide/start/environment.md","filePath":"guide/start/environment.md","lastUpdated":1712229374000}'),o={name:"guide/start/environment.md"};function n(s,t,l,d,p,m){return i(),a("div",null,t[0]||(t[0]=[r('环境搭建 Node.js & pnpm 在 使用指南 中你已安装并部署了 Node.js 环境与 pnpm,此处不再赘述。
Git & GitHub Git 是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。版本控制可方便的实现协作开发、版本回退等,其重要性对每一位开发者都是不言而喻。GitHub 是一个面向开源及私有软件项目的托管平台,拥有着全球最大的开源社区,使用 Git 可轻松将你的项目推送至 GitHub 远程仓库,你与你的项目也将成为开源社区的一份子。Git 与 GitHub 具体使用流程此处不逐一赘述。
IDE & Editor 显然 Kotori 并不属于 Web 前端的范畴,但依旧隶属于 JavaScript 生态,因此推荐 世界上最好的 Web 开发 IDE 「Visual Studio Code」(以下简称「VSC」)。虽然 VSC 本质上只是文本编辑器,但因其强大的扩展商店使其能做到大部分 IDE 的功能,当然你也可以根据你的喜好选择,如:
',9)]))}const c=e(o,[["render",n]]);export{u as __pageData,c as default};
diff --git a/assets/guide_start_environment.md.DbqmpXqr.lean.js b/assets/guide_start_environment.md.DbqmpXqr.lean.js
new file mode 100644
index 00000000..aa195a50
--- /dev/null
+++ b/assets/guide_start_environment.md.DbqmpXqr.lean.js
@@ -0,0 +1 @@
+import{_ as e,c as a,a0 as r,o as i}from"./chunks/framework.p2VkXzrt.js";const u=JSON.parse('{"title":"环境搭建","description":"","frontmatter":{},"headers":[],"relativePath":"guide/start/environment.md","filePath":"guide/start/environment.md","lastUpdated":1712229374000}'),o={name:"guide/start/environment.md"};function n(s,t,l,d,p,m){return i(),a("div",null,t[0]||(t[0]=[r('环境搭建 Node.js & pnpm 在 使用指南 中你已安装并部署了 Node.js 环境与 pnpm,此处不再赘述。
Git & GitHub Git 是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。版本控制可方便的实现协作开发、版本回退等,其重要性对每一位开发者都是不言而喻。GitHub 是一个面向开源及私有软件项目的托管平台,拥有着全球最大的开源社区,使用 Git 可轻松将你的项目推送至 GitHub 远程仓库,你与你的项目也将成为开源社区的一份子。Git 与 GitHub 具体使用流程此处不逐一赘述。
IDE & Editor 显然 Kotori 并不属于 Web 前端的范畴,但依旧隶属于 JavaScript 生态,因此推荐 世界上最好的 Web 开发 IDE 「Visual Studio Code」(以下简称「VSC」)。虽然 VSC 本质上只是文本编辑器,但因其强大的扩展商店使其能做到大部分 IDE 的功能,当然你也可以根据你的喜好选择,如:
',9)]))}const c=e(o,[["render",n]]);export{u as __pageData,c as default};
diff --git a/assets/guide_start_publish.md.XXQZNH0i.js b/assets/guide_start_publish.md.XXQZNH0i.js
new file mode 100644
index 00000000..8ae5fd6b
--- /dev/null
+++ b/assets/guide_start_publish.md.XXQZNH0i.js
@@ -0,0 +1,40 @@
+import{_ as i,c as a,a0 as n,o as e}from"./chunks/framework.p2VkXzrt.js";const c=JSON.parse('{"title":"模块发布","description":"","frontmatter":{},"headers":[],"relativePath":"guide/start/publish.md","filePath":"guide/start/publish.md","lastUpdated":1716217371000}'),t={name:"guide/start/publish.md"};function p(l,s,h,k,r,o){return e(),a("div",null,s[0]||(s[0]=[n(`模块发布 当开发完毕模块后,可以将它发布至社区,一个 Kotori 模块一般会同时发布到如下三个平台:
优先级(重要程度):npm > 模块中心 > 开源社区。每一个公开的 Kotori 模块都应发布至 npm 并作为模块的主要获取途径。Kotori 使用 GPL-3.0 协议,该协议要求 Kotori 的所有模块及其二次开发项目也必须使用 GPL-3.0 协议且开源,因此发布到开源社区是必要的,开源行为本身也是一种无私奉献、共享知识和回馈社区的体现。
构建产物 「构建产物」在 JavaScript 生态中指将源码(Kotori 模块开发中一般为 TypeScript 文件)进行处理以适用于生产环境中(处理过程一般有 TypeScript 转为 JavaScript、向下兼容语法、压缩代码等)。JavaScript 生态中构建工具非常多,你可以选择喜欢的构建工具并自习配置,当然如果你对此并不了解也可以使用 Kotori 默认的构建方式(通过TypeScript 自带的 tsc 程序),在你的模块根目录中输入以下指令:
一般地,你将会发现在模块根目录出现了一个 lib
文件夹,这在上一节已有提到,它是构建产物的输出目录,有必要的话可在 tsconfig.json
文件中更改:
json {
+ // ...
+ " compilerOptions " : {
+ " rootDir " : " ./src " , // 输入目录
+ " outDir " : " ./lib " // 输出目录
+ // ...
+ }
+}
关于 tsconfig.json
的更多内容:TypeScript Documentation
文件忽略 对于模块发布主要分为发布构建产物(publish)与推送源码(push),两种情况下需要发布的文件内容会有些许不同,因此便引入了「文件忽略」。
.npmignore 用于指定在发布构建产物时忽略的文件与文件夹,在模块根目录创建一个 .npmignore
文件:
int node_modules
+src
+test
+
+tsconfig.json
+!README.md
实际上在发布构建产物时只需要附带少数文件即可,而 .npmignore
采用的是黑名单机制显得很繁琐,因此 Kotori 模块的默认模板中并未使用该方式,也并不推荐。
package.files 在上一节的 package.json
示例中会发现有一个以字符串数组为值的 files
配置项,其用于指定在使用 publish
时需要附带的文件与文件夹。
typescript {
+ " files " : [ " lib " , " LICENSE " , " README.md " ],
+}
files
配置项优先级高于 .npmignore
,其直接写在 package.json
中显得十分简洁也会减少整个模块目录的文件冗余。
.gitignore 不同于前两者,.gitignore
用于指定在使用 Git 进行版本控制时需要忽略的文件,语法与 .npmignore
类似,同样位于模块根目录:
ini node_modules
+dist
+lib
+.husky/_
+
+.vscode/*
+.vs/*
+!.vscode/extensions.json
+
+*.tgz
+tsconfig.tsbuildinfo
+*.log
+
+kotori.dev.yml
发布构建产物 使用工作区开发时,需确保当前为待发布模块根目录,而非工作区根目录。首先检查 npm 源是否为 http://registry.npmjs.org
:
bash npm config get registry
+# If not:
+# npm config set registry=http://registry.npmjs.org
前往 npmjs.org 注册账号,然后根据提示在浏览器内登录:
当一切就绪时:
当没有任何意外问题时,访问 npm 个人页即可查看刚才发布的插件: kotori-plugin-my-project 。
发布源码 使用 Git 前务必先配置好你的账号、邮箱和与 GitHub 通信的 ssh,可参考 手把手教你配置 git 和 git 仓库 。使用工作区开发时,可选择发布整个工作区也可仅发布单个模块,切换到相应目录即可。首先在 GitHub New 页面创建一个远程仓库,接着在本地仓库中关联到该远程仓库:
bash git remote add origin git@github.com:kotorijs/kotori-plugin-my-project
提交并推送至远程仓库
bash git add .
+git commit -m ' feat: create a project '
+git push origin master
当然,你也可以为本次提交添加一个 tag:
bash git tag v1.0.0
+git push --tags
收录至模块市场 前往 Kotori Docs 仓库将其 fork 到你的账号名下,修改 fork 的仓库中的 src/public/data.json
文件,在该文件中追加你的模块的包名与描述:
json {
+ // ...
+ {
+ " name " : " kotori-plugin-my-project " ,
+ " description " : " 这是一个 "
+ }
+ // ...
+}
name
务必与发布到 npm 的包名一致请按照包名的字母依次排序,如若有命名空间(@xxxx/)请提到最前,并根据包命名空间、包名的字母依次排序 description
不应过长,但需大致概括模块内容请注意 JSON 格式规范 完成文件更改后向源仓库发起 pull request 等待审定。所有新的 pull request 一般会在十二小时内审定完毕,当上述注意事项均无误时将会被合并到源仓库,届时你可在 Kotori 模块中心 查看你的模块。
放在最后 `,45)]))}const g=i(t,[["render",p]]);export{c as __pageData,g as default};
diff --git a/assets/guide_start_publish.md.XXQZNH0i.lean.js b/assets/guide_start_publish.md.XXQZNH0i.lean.js
new file mode 100644
index 00000000..8ae5fd6b
--- /dev/null
+++ b/assets/guide_start_publish.md.XXQZNH0i.lean.js
@@ -0,0 +1,40 @@
+import{_ as i,c as a,a0 as n,o as e}from"./chunks/framework.p2VkXzrt.js";const c=JSON.parse('{"title":"模块发布","description":"","frontmatter":{},"headers":[],"relativePath":"guide/start/publish.md","filePath":"guide/start/publish.md","lastUpdated":1716217371000}'),t={name:"guide/start/publish.md"};function p(l,s,h,k,r,o){return e(),a("div",null,s[0]||(s[0]=[n(`模块发布 当开发完毕模块后,可以将它发布至社区,一个 Kotori 模块一般会同时发布到如下三个平台:
优先级(重要程度):npm > 模块中心 > 开源社区。每一个公开的 Kotori 模块都应发布至 npm 并作为模块的主要获取途径。Kotori 使用 GPL-3.0 协议,该协议要求 Kotori 的所有模块及其二次开发项目也必须使用 GPL-3.0 协议且开源,因此发布到开源社区是必要的,开源行为本身也是一种无私奉献、共享知识和回馈社区的体现。
构建产物 「构建产物」在 JavaScript 生态中指将源码(Kotori 模块开发中一般为 TypeScript 文件)进行处理以适用于生产环境中(处理过程一般有 TypeScript 转为 JavaScript、向下兼容语法、压缩代码等)。JavaScript 生态中构建工具非常多,你可以选择喜欢的构建工具并自习配置,当然如果你对此并不了解也可以使用 Kotori 默认的构建方式(通过TypeScript 自带的 tsc 程序),在你的模块根目录中输入以下指令:
一般地,你将会发现在模块根目录出现了一个 lib
文件夹,这在上一节已有提到,它是构建产物的输出目录,有必要的话可在 tsconfig.json
文件中更改:
json {
+ // ...
+ " compilerOptions " : {
+ " rootDir " : " ./src " , // 输入目录
+ " outDir " : " ./lib " // 输出目录
+ // ...
+ }
+}
关于 tsconfig.json
的更多内容:TypeScript Documentation
文件忽略 对于模块发布主要分为发布构建产物(publish)与推送源码(push),两种情况下需要发布的文件内容会有些许不同,因此便引入了「文件忽略」。
.npmignore 用于指定在发布构建产物时忽略的文件与文件夹,在模块根目录创建一个 .npmignore
文件:
int node_modules
+src
+test
+
+tsconfig.json
+!README.md
实际上在发布构建产物时只需要附带少数文件即可,而 .npmignore
采用的是黑名单机制显得很繁琐,因此 Kotori 模块的默认模板中并未使用该方式,也并不推荐。
package.files 在上一节的 package.json
示例中会发现有一个以字符串数组为值的 files
配置项,其用于指定在使用 publish
时需要附带的文件与文件夹。
typescript {
+ " files " : [ " lib " , " LICENSE " , " README.md " ],
+}
files
配置项优先级高于 .npmignore
,其直接写在 package.json
中显得十分简洁也会减少整个模块目录的文件冗余。
.gitignore 不同于前两者,.gitignore
用于指定在使用 Git 进行版本控制时需要忽略的文件,语法与 .npmignore
类似,同样位于模块根目录:
ini node_modules
+dist
+lib
+.husky/_
+
+.vscode/*
+.vs/*
+!.vscode/extensions.json
+
+*.tgz
+tsconfig.tsbuildinfo
+*.log
+
+kotori.dev.yml
发布构建产物 使用工作区开发时,需确保当前为待发布模块根目录,而非工作区根目录。首先检查 npm 源是否为 http://registry.npmjs.org
:
bash npm config get registry
+# If not:
+# npm config set registry=http://registry.npmjs.org
前往 npmjs.org 注册账号,然后根据提示在浏览器内登录:
当一切就绪时:
当没有任何意外问题时,访问 npm 个人页即可查看刚才发布的插件: kotori-plugin-my-project 。
发布源码 使用 Git 前务必先配置好你的账号、邮箱和与 GitHub 通信的 ssh,可参考 手把手教你配置 git 和 git 仓库 。使用工作区开发时,可选择发布整个工作区也可仅发布单个模块,切换到相应目录即可。首先在 GitHub New 页面创建一个远程仓库,接着在本地仓库中关联到该远程仓库:
bash git remote add origin git@github.com:kotorijs/kotori-plugin-my-project
提交并推送至远程仓库
bash git add .
+git commit -m ' feat: create a project '
+git push origin master
当然,你也可以为本次提交添加一个 tag:
bash git tag v1.0.0
+git push --tags
收录至模块市场 前往 Kotori Docs 仓库将其 fork 到你的账号名下,修改 fork 的仓库中的 src/public/data.json
文件,在该文件中追加你的模块的包名与描述:
json {
+ // ...
+ {
+ " name " : " kotori-plugin-my-project " ,
+ " description " : " 这是一个 "
+ }
+ // ...
+}
name
务必与发布到 npm 的包名一致请按照包名的字母依次排序,如若有命名空间(@xxxx/)请提到最前,并根据包命名空间、包名的字母依次排序 description
不应过长,但需大致概括模块内容请注意 JSON 格式规范 完成文件更改后向源仓库发起 pull request 等待审定。所有新的 pull request 一般会在十二小时内审定完毕,当上述注意事项均无误时将会被合并到源仓库,届时你可在 Kotori 模块中心 查看你的模块。
放在最后 `,45)]))}const g=i(t,[["render",p]]);export{c as __pageData,g as default};
diff --git a/assets/guide_start_setup.md.DUpgviI7.js b/assets/guide_start_setup.md.DUpgviI7.js
new file mode 100644
index 00000000..cb6bf3b7
--- /dev/null
+++ b/assets/guide_start_setup.md.DUpgviI7.js
@@ -0,0 +1,144 @@
+import{_ as i,c as a,a0 as n,o as h}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"项目构建","description":"","frontmatter":{},"headers":[],"relativePath":"guide/start/setup.md","filePath":"guide/start/setup.md","lastUpdated":1723345558000}'),k={name:"guide/start/setup.md"};function t(p,s,l,e,r,d){return h(),a("div",null,s[0]||(s[0]=[n(`项目构建 在本阶段中开发模块一并通过搭建工作区开发,此外还可通过克隆 Kotori 源码或单独创建包进行开发,对于前者请参考 深入了解 ,对于后者则无需赘述。
基于 create-kotori 快速搭建工作区 「create-kotori 」是专用于构建 Kotori 模块的 Cli 工具。
命令语法:create-kotori <project-name>
bash pnpm create kotori@latest
除此之外,也可以将其安装在全局使用:
bash npm install create-kotori -g
+create-kotori my-project
项目结构 text my-project
+├── package.json
+├── tsconfig.json
+├── tsconfig.node.json
+├── pnpm-workspace.json
+├── kotori.yml
+├── LICENSE
+├── README.md
+├── .gitignore
+└── scripts
+ └── dev.ts
+└── project
+ └── my-project
+ ├── package.json
+ ├── LICENSE
+ ├── README.md
+ ├── tsconfig.json
+ ├── lib
+ │ ├── ...
+ ├── locales
+ │ ├── en_US.json
+ │ ├── ja_JP.json
+ │ ├── zh_CN.json
+ │ └── zh_TW.json
+ └── src
+ └── index.ts
kotori.yml
Kotori 配置文件kotori.dev.yml
Kotori Dev 模式下配置文件package.json
包信息文件tsconfig.json
TypeScript 配置文件LICENSE
协议文件README.md
自述文件.gitignore
git 忽略文件lib
构建产物输出目录(前端为 dist
,后端为 lib
)locales
国际化文件夹,将在后面的章节中讲解src
工程文件夹,代码存放处 package.json 以下为默认创建的 package.json
:
json {
+ " name " : " kotori-plugin-my-project " ,
+ " version " : " 1.0.0 " ,
+ " description " : " This is my first Kotori plugin " ,
+ " main " : " lib/index.js " ,
+ " scripts " : {
+ " build " : " tsc "
+ },
+ " keywords " : [
+ " kotori " ,
+ " chatbot " ,
+ " kotori-plugin "
+ ],
+ " license " : " GPL-3.0 " ,
+ " files " : [
+ " lib " ,
+ " locales " ,
+ " LICENSE " ,
+ " README.md "
+ ],
+ " author " : " Himeno " ,
+ " peerDependencies " : {
+ " kotori-bot " : " ^1.3.0 "
+ }
+}
添加一些非必要配置项以完善包信息:
json {
+ " author " : " Himeno <biyuehuya@gmail.com> " ,
+ " bugs " : {
+ " url " : " https://github.com/kotorijs/my-project/issues "
+ },
+ " repository " : {
+ " type " : " git " ,
+ " url " : " git+https://github.com/kotorijs/my-project.git "
+ },
+ " homepage " : " https://github.com/kotorijs/my-project/ "
+}
添加用于传给 Kotori 的元数据:
json {
+ " kotori " : {
+ " meta " : {
+ " languages " : [
+ " en_US " ,
+ " ja_JP " ,
+ " zh_TW " ,
+ " zh_CN "
+ ]
+ }
+ }
+}
一个合法的 Kotori 模块其 package.json
需要满足一系列来自 Kotori 的约定,Kotori 程序只有在其合法时才会加载该模块。不过当前你无需关心这个问题,元数据与 package.json
约定将放在第三章中讲解。以下是该 package.json 的完整效果:
json {
+ " name " : " kotori-plugin-my-project " ,
+ " version " : " 1.0.0 " ,
+ " description " : " This is my first Kotori plugin " ,
+ " main " : " lib/index.js " ,
+ " scripts " : {
+ " build " : " tsc "
+ },
+ " keywords " : [
+ " kotori " ,
+ " chatbot " ,
+ " kotori-plugin "
+ ],
+ " license " : " GPL-3.0 " ,
+ " files " : [
+ " lib " ,
+ " locales " ,
+ " LICENSE " ,
+ " README.md "
+ ],
+ " peerDependencies " : {
+ " kotori-bot " : " ^1.3.0 "
+ },
+ " author " : " Himeno <biyuehuya@gmail.com> " ,
+ " bugs " : {
+ " url " : " https://github.com/kotorijs/my-project/issues "
+ },
+ " repository " : {
+ " type " : " git " ,
+ " url " : " git+https://github.com/kotorijs/my-project.git "
+ },
+ " homepage " : " https://github.com/kotorijs/my-project/ " ,
+ " kotori " : {
+ " meta " : {
+ " languages " : [
+ " en_US " ,
+ " ja_JP " ,
+ " zh_TW " ,
+ " zh_CN "
+ ]
+ }
+ }
+}
关于 package.json
的默认配置项与更多信息请参考 npm Docs
index.ts 以下为默认创建的 index.ts,当前你还无需理解其具体含义:
typescript import type { Context } from ' kotori-bot ' ;
+import config from ' ./config.ts ' ;
+import types from ' ./types.ts ' ;
+
+export function main ( ctx : Context ) {
+ ctx
+ . command ( ' echo <content> [num:number=3] ' )
+ . action (( data , message ) => {
+ ctx . logger . debug ( data , data . args [ 0 ]);
+ ctx . logger . debug ( message );
+ return [
+ \` 返回消息:~%message% \` ,
+ {
+ message : data . args [ 0 ]
+ }
+ ];
+ })
+ . alias ( ' print ' )
+ . scope ( ' group ' );
+
+ ctx . regexp ( / ^ ( . * ) #print $ / , ( match ) => match [ 1 ]);
+
+ ctx . command ( ' ison ' ). action (( _ , events ) => {
+ if ( events . api . adapter . config . master === events . userId ) return \` 在的哟主人~ \` ;
+ return ' 你是...谁? ' ;
+ });
+}
模块测试 在入门教程中提到过使用「@kotori-bot/kotori-plugin-adapter-cmd 」适配器可以在命令行中测试指令,但命令行本身仅支持纯文字交互因此并不友好也不便于开发者调试。同样的,Kotori 已默认安装「@kotori-bot/kotori-plugin-adapter-sandbox 」适配器,它提供了一个极为方便、全面的机器人沙盒测试环境,只需在 kotori.yml
中设置该适配器即可:
toml adapter:
+ developer:
+ extends: sandbox
+ master: 1
+ port: 2333
运行模式 [!WARN] 以下内容有待更新
运行模式分为 「生产模式(Build)」与「开发模式(Dev)」两种:
Build 模式将显示更少的日志输出,有利于减少不必要信息方便用户使用;Dev 模式会有详尽的错误日志与开发日志输出,有利于开发者快速找到问题。 Build 模式有更牢固的错误捕获与进程守护,长期运行更加稳定;Dev 模式下在遇到某些关键性错误时会退出整个 Kotori 程序。 Dev 模式会有实时的代码文件变动监听与模块自动重载(热更新),为开发者提供犹如前端开发般的便捷体验。 Dev 模式能够直接运行 TypeScript 文件,在加载模块时会优先检测模块文件夹内是否有 src/.ts
。 Build 模式下读取 kotori.yml
,Dev 模式下读取 kotori.dev.yml
,两者用法与实际效果均一致,旨在区分不同模式下不同配置。 从 Dev 模式下启动 Kotori:
在浏览器中打开 http://localhost:2333
即可进入沙盒环境,输入 /echo Hello,Kotori!
以查看效果:
`,35)]))}const o=i(k,[["render",t]]);export{y as __pageData,o as default};
diff --git a/assets/guide_start_setup.md.DUpgviI7.lean.js b/assets/guide_start_setup.md.DUpgviI7.lean.js
new file mode 100644
index 00000000..cb6bf3b7
--- /dev/null
+++ b/assets/guide_start_setup.md.DUpgviI7.lean.js
@@ -0,0 +1,144 @@
+import{_ as i,c as a,a0 as n,o as h}from"./chunks/framework.p2VkXzrt.js";const y=JSON.parse('{"title":"项目构建","description":"","frontmatter":{},"headers":[],"relativePath":"guide/start/setup.md","filePath":"guide/start/setup.md","lastUpdated":1723345558000}'),k={name:"guide/start/setup.md"};function t(p,s,l,e,r,d){return h(),a("div",null,s[0]||(s[0]=[n(`项目构建 在本阶段中开发模块一并通过搭建工作区开发,此外还可通过克隆 Kotori 源码或单独创建包进行开发,对于前者请参考 深入了解 ,对于后者则无需赘述。
基于 create-kotori 快速搭建工作区 「create-kotori 」是专用于构建 Kotori 模块的 Cli 工具。
命令语法:create-kotori <project-name>
bash pnpm create kotori@latest
除此之外,也可以将其安装在全局使用:
bash npm install create-kotori -g
+create-kotori my-project
项目结构 text my-project
+├── package.json
+├── tsconfig.json
+├── tsconfig.node.json
+├── pnpm-workspace.json
+├── kotori.yml
+├── LICENSE
+├── README.md
+├── .gitignore
+└── scripts
+ └── dev.ts
+└── project
+ └── my-project
+ ├── package.json
+ ├── LICENSE
+ ├── README.md
+ ├── tsconfig.json
+ ├── lib
+ │ ├── ...
+ ├── locales
+ │ ├── en_US.json
+ │ ├── ja_JP.json
+ │ ├── zh_CN.json
+ │ └── zh_TW.json
+ └── src
+ └── index.ts
kotori.yml
Kotori 配置文件kotori.dev.yml
Kotori Dev 模式下配置文件package.json
包信息文件tsconfig.json
TypeScript 配置文件LICENSE
协议文件README.md
自述文件.gitignore
git 忽略文件lib
构建产物输出目录(前端为 dist
,后端为 lib
)locales
国际化文件夹,将在后面的章节中讲解src
工程文件夹,代码存放处 package.json 以下为默认创建的 package.json
:
json {
+ " name " : " kotori-plugin-my-project " ,
+ " version " : " 1.0.0 " ,
+ " description " : " This is my first Kotori plugin " ,
+ " main " : " lib/index.js " ,
+ " scripts " : {
+ " build " : " tsc "
+ },
+ " keywords " : [
+ " kotori " ,
+ " chatbot " ,
+ " kotori-plugin "
+ ],
+ " license " : " GPL-3.0 " ,
+ " files " : [
+ " lib " ,
+ " locales " ,
+ " LICENSE " ,
+ " README.md "
+ ],
+ " author " : " Himeno " ,
+ " peerDependencies " : {
+ " kotori-bot " : " ^1.3.0 "
+ }
+}
添加一些非必要配置项以完善包信息:
json {
+ " author " : " Himeno <biyuehuya@gmail.com> " ,
+ " bugs " : {
+ " url " : " https://github.com/kotorijs/my-project/issues "
+ },
+ " repository " : {
+ " type " : " git " ,
+ " url " : " git+https://github.com/kotorijs/my-project.git "
+ },
+ " homepage " : " https://github.com/kotorijs/my-project/ "
+}
添加用于传给 Kotori 的元数据:
json {
+ " kotori " : {
+ " meta " : {
+ " languages " : [
+ " en_US " ,
+ " ja_JP " ,
+ " zh_TW " ,
+ " zh_CN "
+ ]
+ }
+ }
+}
一个合法的 Kotori 模块其 package.json
需要满足一系列来自 Kotori 的约定,Kotori 程序只有在其合法时才会加载该模块。不过当前你无需关心这个问题,元数据与 package.json
约定将放在第三章中讲解。以下是该 package.json 的完整效果:
json {
+ " name " : " kotori-plugin-my-project " ,
+ " version " : " 1.0.0 " ,
+ " description " : " This is my first Kotori plugin " ,
+ " main " : " lib/index.js " ,
+ " scripts " : {
+ " build " : " tsc "
+ },
+ " keywords " : [
+ " kotori " ,
+ " chatbot " ,
+ " kotori-plugin "
+ ],
+ " license " : " GPL-3.0 " ,
+ " files " : [
+ " lib " ,
+ " locales " ,
+ " LICENSE " ,
+ " README.md "
+ ],
+ " peerDependencies " : {
+ " kotori-bot " : " ^1.3.0 "
+ },
+ " author " : " Himeno <biyuehuya@gmail.com> " ,
+ " bugs " : {
+ " url " : " https://github.com/kotorijs/my-project/issues "
+ },
+ " repository " : {
+ " type " : " git " ,
+ " url " : " git+https://github.com/kotorijs/my-project.git "
+ },
+ " homepage " : " https://github.com/kotorijs/my-project/ " ,
+ " kotori " : {
+ " meta " : {
+ " languages " : [
+ " en_US " ,
+ " ja_JP " ,
+ " zh_TW " ,
+ " zh_CN "
+ ]
+ }
+ }
+}
关于 package.json
的默认配置项与更多信息请参考 npm Docs
index.ts 以下为默认创建的 index.ts,当前你还无需理解其具体含义:
typescript import type { Context } from ' kotori-bot ' ;
+import config from ' ./config.ts ' ;
+import types from ' ./types.ts ' ;
+
+export function main ( ctx : Context ) {
+ ctx
+ . command ( ' echo <content> [num:number=3] ' )
+ . action (( data , message ) => {
+ ctx . logger . debug ( data , data . args [ 0 ]);
+ ctx . logger . debug ( message );
+ return [
+ \` 返回消息:~%message% \` ,
+ {
+ message : data . args [ 0 ]
+ }
+ ];
+ })
+ . alias ( ' print ' )
+ . scope ( ' group ' );
+
+ ctx . regexp ( / ^ ( . * ) #print $ / , ( match ) => match [ 1 ]);
+
+ ctx . command ( ' ison ' ). action (( _ , events ) => {
+ if ( events . api . adapter . config . master === events . userId ) return \` 在的哟主人~ \` ;
+ return ' 你是...谁? ' ;
+ });
+}
模块测试 在入门教程中提到过使用「@kotori-bot/kotori-plugin-adapter-cmd 」适配器可以在命令行中测试指令,但命令行本身仅支持纯文字交互因此并不友好也不便于开发者调试。同样的,Kotori 已默认安装「@kotori-bot/kotori-plugin-adapter-sandbox 」适配器,它提供了一个极为方便、全面的机器人沙盒测试环境,只需在 kotori.yml
中设置该适配器即可:
toml adapter:
+ developer:
+ extends: sandbox
+ master: 1
+ port: 2333
运行模式 [!WARN] 以下内容有待更新
运行模式分为 「生产模式(Build)」与「开发模式(Dev)」两种:
Build 模式将显示更少的日志输出,有利于减少不必要信息方便用户使用;Dev 模式会有详尽的错误日志与开发日志输出,有利于开发者快速找到问题。 Build 模式有更牢固的错误捕获与进程守护,长期运行更加稳定;Dev 模式下在遇到某些关键性错误时会退出整个 Kotori 程序。 Dev 模式会有实时的代码文件变动监听与模块自动重载(热更新),为开发者提供犹如前端开发般的便捷体验。 Dev 模式能够直接运行 TypeScript 文件,在加载模块时会优先检测模块文件夹内是否有 src/.ts
。 Build 模式下读取 kotori.yml
,Dev 模式下读取 kotori.dev.yml
,两者用法与实际效果均一致,旨在区分不同模式下不同配置。 从 Dev 模式下启动 Kotori:
在浏览器中打开 http://localhost:2333
即可进入沙盒环境,输入 /echo Hello,Kotori!
以查看效果:
`,35)]))}const o=i(k,[["render",t]]);export{y as __pageData,o as default};
diff --git a/assets/index.md.Dr7K_nS5.js b/assets/index.md.Dr7K_nS5.js
new file mode 100644
index 00000000..ba1104d8
--- /dev/null
+++ b/assets/index.md.Dr7K_nS5.js
@@ -0,0 +1 @@
+import{_ as t,c as e,o as i}from"./chunks/framework.p2VkXzrt.js";const l=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"CustomHome","hero":{"image":{"src":"/favicon.svg","alt":"KotoriBot"},"name":"小鳥 · KotoriBot","tagline":"基于 Node.js + TypeScript 的跨平台聊天机器人框架","actions":[{"theme":"brand","text":"开始使用👉","link":"/basic/"},{"theme":"alt","text":"发行下载🐦","link":"https://github.com/kotorijs/kotori/releases"}]},"features":[{"icon":"🚀","title":"跨平台","details":"得益于模块化支持,通过编写各种模块实现不同的功能与聊天平台接入"},{"icon":"🧩","title":"解耦合","details":"基于控制反转与面向切面编程思想,减少代码冗余与复杂度"},{"icon":"🛠️","title":"现代化","details":"使用现代化的 ECMAScript 语法规范与强大的 TypeScript 类型支持"}],"images":[{"src":"https://pic.imgdb.cn/item/6739964ad29ded1a8c704df2.png","alt":"Webpage Home"},{"src":"https://pic.imgdb.cn/item/6739964bd29ded1a8c704ece.png","alt":"Command Management"},{"src":"https://pic.imgdb.cn/item/6739964cd29ded1a8c704f69.png","alt":"Virtual Control"},{"src":"https://pic.imgdb.cn/item/6739964dd29ded1a8c704fff.png","alt":"Modules Center"},{"src":"https://pic.imgdb.cn/item/6739964ed29ded1a8c70509b.png","alt":"Instance List"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1731827740000}'),a={name:"index.md"};function c(n,d,s,o,r,p){return i(),e("div")}const g=t(a,[["render",c]]);export{l as __pageData,g as default};
diff --git a/assets/index.md.Dr7K_nS5.lean.js b/assets/index.md.Dr7K_nS5.lean.js
new file mode 100644
index 00000000..ba1104d8
--- /dev/null
+++ b/assets/index.md.Dr7K_nS5.lean.js
@@ -0,0 +1 @@
+import{_ as t,c as e,o as i}from"./chunks/framework.p2VkXzrt.js";const l=JSON.parse('{"title":"","description":"","frontmatter":{"layout":"CustomHome","hero":{"image":{"src":"/favicon.svg","alt":"KotoriBot"},"name":"小鳥 · KotoriBot","tagline":"基于 Node.js + TypeScript 的跨平台聊天机器人框架","actions":[{"theme":"brand","text":"开始使用👉","link":"/basic/"},{"theme":"alt","text":"发行下载🐦","link":"https://github.com/kotorijs/kotori/releases"}]},"features":[{"icon":"🚀","title":"跨平台","details":"得益于模块化支持,通过编写各种模块实现不同的功能与聊天平台接入"},{"icon":"🧩","title":"解耦合","details":"基于控制反转与面向切面编程思想,减少代码冗余与复杂度"},{"icon":"🛠️","title":"现代化","details":"使用现代化的 ECMAScript 语法规范与强大的 TypeScript 类型支持"}],"images":[{"src":"https://pic.imgdb.cn/item/6739964ad29ded1a8c704df2.png","alt":"Webpage Home"},{"src":"https://pic.imgdb.cn/item/6739964bd29ded1a8c704ece.png","alt":"Command Management"},{"src":"https://pic.imgdb.cn/item/6739964cd29ded1a8c704f69.png","alt":"Virtual Control"},{"src":"https://pic.imgdb.cn/item/6739964dd29ded1a8c704fff.png","alt":"Modules Center"},{"src":"https://pic.imgdb.cn/item/6739964ed29ded1a8c70509b.png","alt":"Instance List"}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":1731827740000}'),a={name:"index.md"};function c(n,d,s,o,r,p){return i(),e("div")}const g=t(a,[["render",c]]);export{l as __pageData,g as default};
diff --git a/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 b/assets/inter-italic-cyrillic-ext.r48I6akx.woff2
new file mode 100644
index 00000000..b6b603d5
Binary files /dev/null and b/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 differ
diff --git a/assets/inter-italic-cyrillic.By2_1cv3.woff2 b/assets/inter-italic-cyrillic.By2_1cv3.woff2
new file mode 100644
index 00000000..def40a4f
Binary files /dev/null and b/assets/inter-italic-cyrillic.By2_1cv3.woff2 differ
diff --git a/assets/inter-italic-greek-ext.1u6EdAuj.woff2 b/assets/inter-italic-greek-ext.1u6EdAuj.woff2
new file mode 100644
index 00000000..e070c3d3
Binary files /dev/null and b/assets/inter-italic-greek-ext.1u6EdAuj.woff2 differ
diff --git a/assets/inter-italic-greek.DJ8dCoTZ.woff2 b/assets/inter-italic-greek.DJ8dCoTZ.woff2
new file mode 100644
index 00000000..a3c16ca4
Binary files /dev/null and b/assets/inter-italic-greek.DJ8dCoTZ.woff2 differ
diff --git a/assets/inter-italic-latin-ext.CN1xVJS-.woff2 b/assets/inter-italic-latin-ext.CN1xVJS-.woff2
new file mode 100644
index 00000000..2210a899
Binary files /dev/null and b/assets/inter-italic-latin-ext.CN1xVJS-.woff2 differ
diff --git a/assets/inter-italic-latin.C2AdPX0b.woff2 b/assets/inter-italic-latin.C2AdPX0b.woff2
new file mode 100644
index 00000000..790d62dc
Binary files /dev/null and b/assets/inter-italic-latin.C2AdPX0b.woff2 differ
diff --git a/assets/inter-italic-vietnamese.BSbpV94h.woff2 b/assets/inter-italic-vietnamese.BSbpV94h.woff2
new file mode 100644
index 00000000..1eec0775
Binary files /dev/null and b/assets/inter-italic-vietnamese.BSbpV94h.woff2 differ
diff --git a/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 b/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2
new file mode 100644
index 00000000..2cfe6153
Binary files /dev/null and b/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 differ
diff --git a/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 b/assets/inter-roman-cyrillic.C5lxZ8CY.woff2
new file mode 100644
index 00000000..e3886dd1
Binary files /dev/null and b/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 differ
diff --git a/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 b/assets/inter-roman-greek-ext.CqjqNYQ-.woff2
new file mode 100644
index 00000000..36d67487
Binary files /dev/null and b/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 differ
diff --git a/assets/inter-roman-greek.BBVDIX6e.woff2 b/assets/inter-roman-greek.BBVDIX6e.woff2
new file mode 100644
index 00000000..2bed1e85
Binary files /dev/null and b/assets/inter-roman-greek.BBVDIX6e.woff2 differ
diff --git a/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 b/assets/inter-roman-latin-ext.4ZJIpNVo.woff2
new file mode 100644
index 00000000..9a8d1e2b
Binary files /dev/null and b/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 differ
diff --git a/assets/inter-roman-latin.Di8DUHzh.woff2 b/assets/inter-roman-latin.Di8DUHzh.woff2
new file mode 100644
index 00000000..07d3c53a
Binary files /dev/null and b/assets/inter-roman-latin.Di8DUHzh.woff2 differ
diff --git a/assets/inter-roman-vietnamese.BjW4sHH5.woff2 b/assets/inter-roman-vietnamese.BjW4sHH5.woff2
new file mode 100644
index 00000000..57bdc22a
Binary files /dev/null and b/assets/inter-roman-vietnamese.BjW4sHH5.woff2 differ
diff --git a/assets/kotori.mp3 b/assets/kotori.mp3
new file mode 100644
index 00000000..3359a0da
Binary files /dev/null and b/assets/kotori.mp3 differ
diff --git a/assets/modules_index.md.A8sDrAic.js b/assets/modules_index.md.A8sDrAic.js
new file mode 100644
index 00000000..87c557f2
--- /dev/null
+++ b/assets/modules_index.md.A8sDrAic.js
@@ -0,0 +1 @@
+import{d as h,p as d,o as s,c as o,j as e,a as i,t as n,F as k,C as y,e as v,_ as w,G as b}from"./chunks/framework.p2VkXzrt.js";const x={class:"app-container"},j={key:0,class:"loading"},D={key:1,class:"content"},L={key:0},N={class:"title"},S={class:"module-list"},C={class:"module-items"},B=["href"],V={class:""},$={key:1},F={key:0},M={key:0},P={key:0},A={class:"module-download"},G=["href"],I={key:0},K=["href"],O=["href"],U={key:2,class:"not-found"},z=h({__name:"Modules",setup(c){const p={adapter:"适配器",plugin:"插件",official:"官方"};function m(u){return u.split("#").length>1?u.split("#")[1]:""}async function _(){const{meta:u,list:l}=await(await fetch("/assets/data_details.json")).json();g.value=u,t.value=r.value?l.find(a=>a.name===r.value):l,f.value=!1}const f=d(!0),r=d(m(location.href)),g=d(),t=d();return setInterval(()=>{const u=m(location.href);r.value!==u&&(r.value=u,_())},500),_(),(u,l)=>(s(),o("div",x,[f.value?(s(),o("div",j,"加载数据中...")):(s(),o("div",D,[l[13]||(l[13]=e("h2",{class:"title"},"Kotori | 模块中心",-1)),t.value&&Array.isArray(t.value)?(s(),o("div",L,[e("h5",N,[i(" 收录插件总数:"+n(t.value.length)+" ",1),l[0]||(l[0]=e("br",null,null,-1)),i(" 最后更新时间:"+n(new Date(g.value.time).toLocaleString()),1)]),e("div",S,[e("div",C,[(s(!0),o(k,null,y(t.value,a=>(s(),o("div",{key:a.name,class:"module-item"},[e("a",{href:`/modules/#${a.name}`,class:"module-link"},[e("h3",null,n(a.name),1),e("p",V,n(a.description),1),e("p",null,"v"+n(a.version)+" "+n(a.author.name),1)],8,B)]))),128))])])])):t.value?(s(),o("div",$,[e("div",null,[e("h1",null,n(t.value.name),1),e("p",null,n(t.value.description),1)]),e("div",null,[e("ul",null,[e("li",null,[l[1]||(l[1]=e("strong",null,"类别:",-1)),i(n(t.value.category.map(a=>p[a]).join("、")),1)]),e("li",null,[l[2]||(l[2]=e("strong",null,"作者:",-1)),i(n(t.value.author.name),1)]),t.value.keywords.length?(s(),o("li",F,[l[3]||(l[3]=e("strong",null,"关键词:",-1)),i(n(t.value.keywords.join("、")),1)])):v("",!0)])]),e("div",null,[l[7]||(l[7]=e("h3",null,"版本信息",-1)),e("ul",null,[e("li",null,[l[4]||(l[4]=e("strong",null,"最新版本:",-1)),i("v"+n(t.value.version),1)]),e("li",null,[l[5]||(l[5]=e("strong",null,"创建时间:",-1)),i(n(new Date(t.value.time.created).toLocaleString()),1)]),e("li",null,[l[6]||(l[6]=e("strong",null,"更新时间:",-1)),i(n(new Date(t.value.time.modified).toLocaleString()),1)])])]),t.value.dist?(s(),o("div",M,[l[11]||(l[11]=e("h3",null,"文件信息",-1)),e("ul",null,[t.value.dist.dependencies?(s(),o("li",P,[l[8]||(l[8]=e("strong",null,"依赖数:",-1)),i(n(t.value.dist.dependencies),1)])):v("",!0),e("li",null,[l[9]||(l[9]=e("strong",null,"文件数:",-1)),i(n(t.value.dist.fileCount),1)]),e("li",null,[l[10]||(l[10]=e("strong",null,"大小:",-1)),i(n((t.value.dist.unpackedSize/1024).toFixed(2))+" KB",1)])])])):v("",!0),e("div",A,[l[12]||(l[12]=e("h3",null,"下载链接:",-1)),e("ul",null,[e("li",null,[e("a",{href:`https://www.npmjs.com/package/${t.value.name}`,target:"_blank"},"npm",8,G)]),t.value.repository?(s(),o("li",I,[e("a",{href:`https://github.com/${t.value.repository}`,target:"_blank"},"源码:Github",8,K)])):v("",!0),e("li",null,[e("a",{href:t.value.dist.tarball,target:"_blank"},"直接下载",8,O)])])])])):(s(),o("div",U,"未找到需要的模块 "+n(r.value),1))]))]))}}),E=w(z,[["__scopeId","data-v-fe1b43c9"]]),R=JSON.parse('{"title":"","description":"","frontmatter":{"editLink":false},"headers":[],"relativePath":"modules/index.md","filePath":"modules/index.md","lastUpdated":1712229374000}'),J={name:"modules/index.md"},T=Object.assign(J,{setup(c){return(p,m)=>(s(),o("div",null,[b(E)]))}});export{R as __pageData,T as default};
diff --git a/assets/modules_index.md.A8sDrAic.lean.js b/assets/modules_index.md.A8sDrAic.lean.js
new file mode 100644
index 00000000..87c557f2
--- /dev/null
+++ b/assets/modules_index.md.A8sDrAic.lean.js
@@ -0,0 +1 @@
+import{d as h,p as d,o as s,c as o,j as e,a as i,t as n,F as k,C as y,e as v,_ as w,G as b}from"./chunks/framework.p2VkXzrt.js";const x={class:"app-container"},j={key:0,class:"loading"},D={key:1,class:"content"},L={key:0},N={class:"title"},S={class:"module-list"},C={class:"module-items"},B=["href"],V={class:""},$={key:1},F={key:0},M={key:0},P={key:0},A={class:"module-download"},G=["href"],I={key:0},K=["href"],O=["href"],U={key:2,class:"not-found"},z=h({__name:"Modules",setup(c){const p={adapter:"适配器",plugin:"插件",official:"官方"};function m(u){return u.split("#").length>1?u.split("#")[1]:""}async function _(){const{meta:u,list:l}=await(await fetch("/assets/data_details.json")).json();g.value=u,t.value=r.value?l.find(a=>a.name===r.value):l,f.value=!1}const f=d(!0),r=d(m(location.href)),g=d(),t=d();return setInterval(()=>{const u=m(location.href);r.value!==u&&(r.value=u,_())},500),_(),(u,l)=>(s(),o("div",x,[f.value?(s(),o("div",j,"加载数据中...")):(s(),o("div",D,[l[13]||(l[13]=e("h2",{class:"title"},"Kotori | 模块中心",-1)),t.value&&Array.isArray(t.value)?(s(),o("div",L,[e("h5",N,[i(" 收录插件总数:"+n(t.value.length)+" ",1),l[0]||(l[0]=e("br",null,null,-1)),i(" 最后更新时间:"+n(new Date(g.value.time).toLocaleString()),1)]),e("div",S,[e("div",C,[(s(!0),o(k,null,y(t.value,a=>(s(),o("div",{key:a.name,class:"module-item"},[e("a",{href:`/modules/#${a.name}`,class:"module-link"},[e("h3",null,n(a.name),1),e("p",V,n(a.description),1),e("p",null,"v"+n(a.version)+" "+n(a.author.name),1)],8,B)]))),128))])])])):t.value?(s(),o("div",$,[e("div",null,[e("h1",null,n(t.value.name),1),e("p",null,n(t.value.description),1)]),e("div",null,[e("ul",null,[e("li",null,[l[1]||(l[1]=e("strong",null,"类别:",-1)),i(n(t.value.category.map(a=>p[a]).join("、")),1)]),e("li",null,[l[2]||(l[2]=e("strong",null,"作者:",-1)),i(n(t.value.author.name),1)]),t.value.keywords.length?(s(),o("li",F,[l[3]||(l[3]=e("strong",null,"关键词:",-1)),i(n(t.value.keywords.join("、")),1)])):v("",!0)])]),e("div",null,[l[7]||(l[7]=e("h3",null,"版本信息",-1)),e("ul",null,[e("li",null,[l[4]||(l[4]=e("strong",null,"最新版本:",-1)),i("v"+n(t.value.version),1)]),e("li",null,[l[5]||(l[5]=e("strong",null,"创建时间:",-1)),i(n(new Date(t.value.time.created).toLocaleString()),1)]),e("li",null,[l[6]||(l[6]=e("strong",null,"更新时间:",-1)),i(n(new Date(t.value.time.modified).toLocaleString()),1)])])]),t.value.dist?(s(),o("div",M,[l[11]||(l[11]=e("h3",null,"文件信息",-1)),e("ul",null,[t.value.dist.dependencies?(s(),o("li",P,[l[8]||(l[8]=e("strong",null,"依赖数:",-1)),i(n(t.value.dist.dependencies),1)])):v("",!0),e("li",null,[l[9]||(l[9]=e("strong",null,"文件数:",-1)),i(n(t.value.dist.fileCount),1)]),e("li",null,[l[10]||(l[10]=e("strong",null,"大小:",-1)),i(n((t.value.dist.unpackedSize/1024).toFixed(2))+" KB",1)])])])):v("",!0),e("div",A,[l[12]||(l[12]=e("h3",null,"下载链接:",-1)),e("ul",null,[e("li",null,[e("a",{href:`https://www.npmjs.com/package/${t.value.name}`,target:"_blank"},"npm",8,G)]),t.value.repository?(s(),o("li",I,[e("a",{href:`https://github.com/${t.value.repository}`,target:"_blank"},"源码:Github",8,K)])):v("",!0),e("li",null,[e("a",{href:t.value.dist.tarball,target:"_blank"},"直接下载",8,O)])])])])):(s(),o("div",U,"未找到需要的模块 "+n(r.value),1))]))]))}}),E=w(z,[["__scopeId","data-v-fe1b43c9"]]),R=JSON.parse('{"title":"","description":"","frontmatter":{"editLink":false},"headers":[],"relativePath":"modules/index.md","filePath":"modules/index.md","lastUpdated":1712229374000}'),J={name:"modules/index.md"},T=Object.assign(J,{setup(c){return(p,m)=>(s(),o("div",null,[b(E)]))}});export{R as __pageData,T as default};
diff --git a/assets/style.Dq-3doOB.css b/assets/style.Dq-3doOB.css
new file mode 100644
index 00000000..129837b1
--- /dev/null
+++ b/assets/style.Dq-3doOB.css
@@ -0,0 +1 @@
+.npm-badge[data-v-8bf061d3]{margin-right:.5rem}.app-container[data-v-fe1b43c9]{max-width:95vw;width:1200px;margin:0 auto;padding:20px}.app-container li[data-v-fe1b43c9]{list-style:none}.loading[data-v-fe1b43c9]{text-align:center;padding:50px;font-size:1.2em;color:#47caff}.content[data-v-fe1b43c9]{padding:20px;border-radius:8px;box-shadow:0 2px 4px #0000001a}.title[data-v-fe1b43c9]{text-align:center;margin-bottom:20px}.module-list[data-v-fe1b43c9]{display:flex;flex-wrap:wrap;gap:20px;justify-content:center}.module-items[data-v-fe1b43c9]{display:contents}.module-item[data-v-fe1b43c9]{flex:1 1 250px;max-width:calc(33.333% - 20px);border:1px solid #ddd;border-radius:8px;padding:10px;text-align:center}@media (max-width: 1200px){.module-item[data-v-fe1b43c9]{max-width:calc(50% - 20px)}}@media (max-width: 768px){.module-item[data-v-fe1b43c9]{max-width:100%}}.module-link[data-v-fe1b43c9]{display:block;color:inherit;text-decoration:none}.module-link[data-v-fe1b43c9]:hover{text-decoration:none}.module-details[data-v-fe1b43c9]{margin-bottom:20px}.module-download a[data-v-fe1b43c9]{text-decoration:none}.module-download a[data-v-fe1b43c9]:hover{text-decoration:underline}.not-found[data-v-fe1b43c9]{text-align:center;color:red}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/assets/inter-roman-cyrillic.C5lxZ8CY.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/assets/inter-roman-greek-ext.CqjqNYQ-.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/assets/inter-roman-greek.BBVDIX6e.woff2) format("woff2");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/assets/inter-roman-vietnamese.BjW4sHH5.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/assets/inter-roman-latin-ext.4ZJIpNVo.woff2) format("woff2");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(/assets/inter-roman-latin.Di8DUHzh.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/assets/inter-italic-cyrillic-ext.r48I6akx.woff2) format("woff2");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/assets/inter-italic-cyrillic.By2_1cv3.woff2) format("woff2");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/assets/inter-italic-greek-ext.1u6EdAuj.woff2) format("woff2");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/assets/inter-italic-greek.DJ8dCoTZ.woff2) format("woff2");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/assets/inter-italic-vietnamese.BSbpV94h.woff2) format("woff2");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/assets/inter-italic-latin-ext.CN1xVJS-.woff2) format("woff2");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter;font-style:italic;font-weight:100 900;font-display:swap;src:url(/assets/inter-italic-latin.C2AdPX0b.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Punctuation SC;font-weight:400;src:local("PingFang SC Regular"),local("Noto Sans CJK SC"),local("Microsoft YaHei");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}@font-face{font-family:Punctuation SC;font-weight:500;src:local("PingFang SC Medium"),local("Noto Sans CJK SC"),local("Microsoft YaHei");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}@font-face{font-family:Punctuation SC;font-weight:600;src:local("PingFang SC Semibold"),local("Noto Sans CJK SC Bold"),local("Microsoft YaHei Bold");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}@font-face{font-family:Punctuation SC;font-weight:700;src:local("PingFang SC Semibold"),local("Noto Sans CJK SC Bold"),local("Microsoft YaHei Bold");unicode-range:U+201C,U+201D,U+2018,U+2019,U+2E3A,U+2014,U+2013,U+2026,U+00B7,U+007E,U+002F}:root{--vp-c-white: #ffffff;--vp-c-black: #000000;--vp-c-neutral: var(--vp-c-black);--vp-c-neutral-inverse: var(--vp-c-white)}.dark{--vp-c-neutral: var(--vp-c-white);--vp-c-neutral-inverse: var(--vp-c-black)}:root{--vp-c-gray-1: #dddde3;--vp-c-gray-2: #e4e4e9;--vp-c-gray-3: #ebebef;--vp-c-gray-soft: rgba(142, 150, 170, .14);--vp-c-indigo-1: #3451b2;--vp-c-indigo-2: #3a5ccc;--vp-c-indigo-3: #5672cd;--vp-c-indigo-soft: rgba(100, 108, 255, .14);--vp-c-purple-1: #6f42c1;--vp-c-purple-2: #7e4cc9;--vp-c-purple-3: #8e5cd9;--vp-c-purple-soft: rgba(159, 122, 234, .14);--vp-c-green-1: #18794e;--vp-c-green-2: #299764;--vp-c-green-3: #30a46c;--vp-c-green-soft: rgba(16, 185, 129, .14);--vp-c-yellow-1: #915930;--vp-c-yellow-2: #946300;--vp-c-yellow-3: #9f6a00;--vp-c-yellow-soft: rgba(234, 179, 8, .14);--vp-c-red-1: #b8272c;--vp-c-red-2: #d5393e;--vp-c-red-3: #e0575b;--vp-c-red-soft: rgba(244, 63, 94, .14);--vp-c-sponsor: #db2777}.dark{--vp-c-gray-1: #515c67;--vp-c-gray-2: #414853;--vp-c-gray-3: #32363f;--vp-c-gray-soft: rgba(101, 117, 133, .16);--vp-c-indigo-1: #a8b1ff;--vp-c-indigo-2: #5c73e7;--vp-c-indigo-3: #3e63dd;--vp-c-indigo-soft: rgba(100, 108, 255, .16);--vp-c-purple-1: #c8abfa;--vp-c-purple-2: #a879e6;--vp-c-purple-3: #8e5cd9;--vp-c-purple-soft: rgba(159, 122, 234, .16);--vp-c-green-1: #3dd68c;--vp-c-green-2: #30a46c;--vp-c-green-3: #298459;--vp-c-green-soft: rgba(16, 185, 129, .16);--vp-c-yellow-1: #f9b44e;--vp-c-yellow-2: #da8b17;--vp-c-yellow-3: #a46a0a;--vp-c-yellow-soft: rgba(234, 179, 8, .16);--vp-c-red-1: #f66f81;--vp-c-red-2: #f14158;--vp-c-red-3: #b62a3c;--vp-c-red-soft: rgba(244, 63, 94, .16)}:root{--vp-c-bg: #ffffff;--vp-c-bg-alt: #f6f6f7;--vp-c-bg-elv: #ffffff;--vp-c-bg-soft: #f6f6f7}.dark{--vp-c-bg: #1b1b1f;--vp-c-bg-alt: #161618;--vp-c-bg-elv: #202127;--vp-c-bg-soft: #202127}:root{--vp-c-border: #c2c2c4;--vp-c-divider: #e2e2e3;--vp-c-gutter: #e2e2e3}.dark{--vp-c-border: #3c3f44;--vp-c-divider: #2e2e32;--vp-c-gutter: #000000}:root{--vp-c-text-1: rgba(60, 60, 67);--vp-c-text-2: rgba(60, 60, 67, .78);--vp-c-text-3: rgba(60, 60, 67, .56)}.dark{--vp-c-text-1: rgba(255, 255, 245, .86);--vp-c-text-2: rgba(235, 235, 245, .6);--vp-c-text-3: rgba(235, 235, 245, .38)}:root{--vp-c-default-1: var(--vp-c-gray-1);--vp-c-default-2: var(--vp-c-gray-2);--vp-c-default-3: var(--vp-c-gray-3);--vp-c-default-soft: var(--vp-c-gray-soft);--vp-c-brand-1: var(--vp-c-indigo-1);--vp-c-brand-2: var(--vp-c-indigo-2);--vp-c-brand-3: var(--vp-c-indigo-3);--vp-c-brand-soft: var(--vp-c-indigo-soft);--vp-c-brand: var(--vp-c-brand-1);--vp-c-tip-1: var(--vp-c-brand-1);--vp-c-tip-2: var(--vp-c-brand-2);--vp-c-tip-3: var(--vp-c-brand-3);--vp-c-tip-soft: var(--vp-c-brand-soft);--vp-c-note-1: var(--vp-c-brand-1);--vp-c-note-2: var(--vp-c-brand-2);--vp-c-note-3: var(--vp-c-brand-3);--vp-c-note-soft: var(--vp-c-brand-soft);--vp-c-success-1: var(--vp-c-green-1);--vp-c-success-2: var(--vp-c-green-2);--vp-c-success-3: var(--vp-c-green-3);--vp-c-success-soft: var(--vp-c-green-soft);--vp-c-important-1: var(--vp-c-purple-1);--vp-c-important-2: var(--vp-c-purple-2);--vp-c-important-3: var(--vp-c-purple-3);--vp-c-important-soft: var(--vp-c-purple-soft);--vp-c-warning-1: var(--vp-c-yellow-1);--vp-c-warning-2: var(--vp-c-yellow-2);--vp-c-warning-3: var(--vp-c-yellow-3);--vp-c-warning-soft: var(--vp-c-yellow-soft);--vp-c-danger-1: var(--vp-c-red-1);--vp-c-danger-2: var(--vp-c-red-2);--vp-c-danger-3: var(--vp-c-red-3);--vp-c-danger-soft: var(--vp-c-red-soft);--vp-c-caution-1: var(--vp-c-red-1);--vp-c-caution-2: var(--vp-c-red-2);--vp-c-caution-3: var(--vp-c-red-3);--vp-c-caution-soft: var(--vp-c-red-soft)}:root{--vp-font-family-base: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--vp-font-family-mono: ui-monospace, "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace;font-optical-sizing:auto}:root:where(:lang(zh)){--vp-font-family-base: "Punctuation SC", "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"}:root{--vp-shadow-1: 0 1px 2px rgba(0, 0, 0, .04), 0 1px 2px rgba(0, 0, 0, .06);--vp-shadow-2: 0 3px 12px rgba(0, 0, 0, .07), 0 1px 4px rgba(0, 0, 0, .07);--vp-shadow-3: 0 12px 32px rgba(0, 0, 0, .1), 0 2px 6px rgba(0, 0, 0, .08);--vp-shadow-4: 0 14px 44px rgba(0, 0, 0, .12), 0 3px 9px rgba(0, 0, 0, .12);--vp-shadow-5: 0 18px 56px rgba(0, 0, 0, .16), 0 4px 12px rgba(0, 0, 0, .16)}:root{--vp-z-index-footer: 10;--vp-z-index-local-nav: 20;--vp-z-index-nav: 30;--vp-z-index-layout-top: 40;--vp-z-index-backdrop: 50;--vp-z-index-sidebar: 60}@media (min-width: 960px){:root{--vp-z-index-sidebar: 25}}:root{--vp-layout-max-width: 1440px}:root{--vp-header-anchor-symbol: "#"}:root{--vp-code-line-height: 1.7;--vp-code-font-size: .875em;--vp-code-color: var(--vp-c-brand-1);--vp-code-link-color: var(--vp-c-brand-1);--vp-code-link-hover-color: var(--vp-c-brand-2);--vp-code-bg: var(--vp-c-default-soft);--vp-code-block-color: var(--vp-c-text-2);--vp-code-block-bg: var(--vp-c-bg-alt);--vp-code-block-divider-color: var(--vp-c-gutter);--vp-code-lang-color: var(--vp-c-text-3);--vp-code-line-highlight-color: var(--vp-c-default-soft);--vp-code-line-number-color: var(--vp-c-text-3);--vp-code-line-diff-add-color: var(--vp-c-success-soft);--vp-code-line-diff-add-symbol-color: var(--vp-c-success-1);--vp-code-line-diff-remove-color: var(--vp-c-danger-soft);--vp-code-line-diff-remove-symbol-color: var(--vp-c-danger-1);--vp-code-line-warning-color: var(--vp-c-warning-soft);--vp-code-line-error-color: var(--vp-c-danger-soft);--vp-code-copy-code-border-color: var(--vp-c-divider);--vp-code-copy-code-bg: var(--vp-c-bg-soft);--vp-code-copy-code-hover-border-color: var(--vp-c-divider);--vp-code-copy-code-hover-bg: var(--vp-c-bg);--vp-code-copy-code-active-text: var(--vp-c-text-2);--vp-code-copy-copied-text-content: "Copied";--vp-code-tab-divider: var(--vp-code-block-divider-color);--vp-code-tab-text-color: var(--vp-c-text-2);--vp-code-tab-bg: var(--vp-code-block-bg);--vp-code-tab-hover-text-color: var(--vp-c-text-1);--vp-code-tab-active-text-color: var(--vp-c-text-1);--vp-code-tab-active-bar-color: var(--vp-c-brand-1)}:root{--vp-button-brand-border: transparent;--vp-button-brand-text: var(--vp-c-white);--vp-button-brand-bg: var(--vp-c-brand-3);--vp-button-brand-hover-border: transparent;--vp-button-brand-hover-text: var(--vp-c-white);--vp-button-brand-hover-bg: var(--vp-c-brand-2);--vp-button-brand-active-border: transparent;--vp-button-brand-active-text: var(--vp-c-white);--vp-button-brand-active-bg: var(--vp-c-brand-1);--vp-button-alt-border: transparent;--vp-button-alt-text: var(--vp-c-text-1);--vp-button-alt-bg: var(--vp-c-default-3);--vp-button-alt-hover-border: transparent;--vp-button-alt-hover-text: var(--vp-c-text-1);--vp-button-alt-hover-bg: var(--vp-c-default-2);--vp-button-alt-active-border: transparent;--vp-button-alt-active-text: var(--vp-c-text-1);--vp-button-alt-active-bg: var(--vp-c-default-1);--vp-button-sponsor-border: var(--vp-c-text-2);--vp-button-sponsor-text: var(--vp-c-text-2);--vp-button-sponsor-bg: transparent;--vp-button-sponsor-hover-border: var(--vp-c-sponsor);--vp-button-sponsor-hover-text: var(--vp-c-sponsor);--vp-button-sponsor-hover-bg: transparent;--vp-button-sponsor-active-border: var(--vp-c-sponsor);--vp-button-sponsor-active-text: var(--vp-c-sponsor);--vp-button-sponsor-active-bg: transparent}:root{--vp-custom-block-font-size: 14px;--vp-custom-block-code-font-size: 13px;--vp-custom-block-info-border: transparent;--vp-custom-block-info-text: var(--vp-c-text-1);--vp-custom-block-info-bg: var(--vp-c-default-soft);--vp-custom-block-info-code-bg: var(--vp-c-default-soft);--vp-custom-block-note-border: transparent;--vp-custom-block-note-text: var(--vp-c-text-1);--vp-custom-block-note-bg: var(--vp-c-default-soft);--vp-custom-block-note-code-bg: var(--vp-c-default-soft);--vp-custom-block-tip-border: transparent;--vp-custom-block-tip-text: var(--vp-c-text-1);--vp-custom-block-tip-bg: var(--vp-c-tip-soft);--vp-custom-block-tip-code-bg: var(--vp-c-tip-soft);--vp-custom-block-important-border: transparent;--vp-custom-block-important-text: var(--vp-c-text-1);--vp-custom-block-important-bg: var(--vp-c-important-soft);--vp-custom-block-important-code-bg: var(--vp-c-important-soft);--vp-custom-block-warning-border: transparent;--vp-custom-block-warning-text: var(--vp-c-text-1);--vp-custom-block-warning-bg: var(--vp-c-warning-soft);--vp-custom-block-warning-code-bg: var(--vp-c-warning-soft);--vp-custom-block-danger-border: transparent;--vp-custom-block-danger-text: var(--vp-c-text-1);--vp-custom-block-danger-bg: var(--vp-c-danger-soft);--vp-custom-block-danger-code-bg: var(--vp-c-danger-soft);--vp-custom-block-caution-border: transparent;--vp-custom-block-caution-text: var(--vp-c-text-1);--vp-custom-block-caution-bg: var(--vp-c-caution-soft);--vp-custom-block-caution-code-bg: var(--vp-c-caution-soft);--vp-custom-block-details-border: var(--vp-custom-block-info-border);--vp-custom-block-details-text: var(--vp-custom-block-info-text);--vp-custom-block-details-bg: var(--vp-custom-block-info-bg);--vp-custom-block-details-code-bg: var(--vp-custom-block-info-code-bg)}:root{--vp-input-border-color: var(--vp-c-border);--vp-input-bg-color: var(--vp-c-bg-alt);--vp-input-switch-bg-color: var(--vp-c-default-soft)}:root{--vp-nav-height: 64px;--vp-nav-bg-color: var(--vp-c-bg);--vp-nav-screen-bg-color: var(--vp-c-bg);--vp-nav-logo-height: 24px}.hide-nav{--vp-nav-height: 0px}.hide-nav .VPSidebar{--vp-nav-height: 22px}:root{--vp-local-nav-bg-color: var(--vp-c-bg)}:root{--vp-sidebar-width: 272px;--vp-sidebar-bg-color: var(--vp-c-bg-alt)}:root{--vp-backdrop-bg-color: rgba(0, 0, 0, .6)}:root{--vp-home-hero-name-color: var(--vp-c-brand-1);--vp-home-hero-name-background: transparent;--vp-home-hero-image-background-image: none;--vp-home-hero-image-filter: none}:root{--vp-badge-info-border: transparent;--vp-badge-info-text: var(--vp-c-text-2);--vp-badge-info-bg: var(--vp-c-default-soft);--vp-badge-tip-border: transparent;--vp-badge-tip-text: var(--vp-c-tip-1);--vp-badge-tip-bg: var(--vp-c-tip-soft);--vp-badge-warning-border: transparent;--vp-badge-warning-text: var(--vp-c-warning-1);--vp-badge-warning-bg: var(--vp-c-warning-soft);--vp-badge-danger-border: transparent;--vp-badge-danger-text: var(--vp-c-danger-1);--vp-badge-danger-bg: var(--vp-c-danger-soft)}:root{--vp-carbon-ads-text-color: var(--vp-c-text-1);--vp-carbon-ads-poweredby-color: var(--vp-c-text-2);--vp-carbon-ads-bg-color: var(--vp-c-bg-soft);--vp-carbon-ads-hover-text-color: var(--vp-c-brand-1);--vp-carbon-ads-hover-poweredby-color: var(--vp-c-text-1)}:root{--vp-local-search-bg: var(--vp-c-bg);--vp-local-search-result-bg: var(--vp-c-bg);--vp-local-search-result-border: var(--vp-c-divider);--vp-local-search-result-selected-bg: var(--vp-c-bg);--vp-local-search-result-selected-border: var(--vp-c-brand-1);--vp-local-search-highlight-bg: var(--vp-c-brand-1);--vp-local-search-highlight-text: var(--vp-c-neutral-inverse)}@media (prefers-reduced-motion: reduce){*,:before,:after{animation-delay:-1ms!important;animation-duration:1ms!important;animation-iteration-count:1!important;background-attachment:initial!important;scroll-behavior:auto!important;transition-duration:0s!important;transition-delay:0s!important}}*,:before,:after{box-sizing:border-box}html{line-height:1.4;font-size:16px;-webkit-text-size-adjust:100%}html.dark{color-scheme:dark}body{margin:0;width:100%;min-width:320px;min-height:100vh;line-height:24px;font-family:var(--vp-font-family-base);font-size:16px;font-weight:400;color:var(--vp-c-text-1);background-color:var(--vp-c-bg);font-synthesis:style;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}main{display:block}h1,h2,h3,h4,h5,h6{margin:0;line-height:24px;font-size:16px;font-weight:400}p{margin:0}strong,b{font-weight:600}a,area,button,[role=button],input,label,select,summary,textarea{touch-action:manipulation}a{color:inherit;text-decoration:inherit}ol,ul{list-style:none;margin:0;padding:0}blockquote{margin:0}pre,code,kbd,samp{font-family:var(--vp-font-family-mono)}img,svg,video,canvas,audio,iframe,embed,object{display:block}figure{margin:0}img,video{max-width:100%;height:auto}button,input,optgroup,select,textarea{border:0;padding:0;line-height:inherit;color:inherit}button{padding:0;font-family:inherit;background-color:transparent;background-image:none}button:enabled,[role=button]:enabled{cursor:pointer}button:focus,button:focus-visible{outline:1px dotted;outline:4px auto -webkit-focus-ring-color}button:focus:not(:focus-visible){outline:none!important}input:focus,textarea:focus,select:focus{outline:none}table{border-collapse:collapse}input{background-color:transparent}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:var(--vp-c-text-3)}input::-ms-input-placeholder,textarea::-ms-input-placeholder{color:var(--vp-c-text-3)}input::placeholder,textarea::placeholder{color:var(--vp-c-text-3)}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=number]{-moz-appearance:textfield}textarea{resize:vertical}select{-webkit-appearance:none}fieldset{margin:0;padding:0}h1,h2,h3,h4,h5,h6,li,p{overflow-wrap:break-word}vite-error-overlay{z-index:9999}mjx-container{overflow-x:auto}mjx-container>svg{display:inline-block;margin:auto}[class^=vpi-],[class*=" vpi-"],.vp-icon{width:1em;height:1em}[class^=vpi-].bg,[class*=" vpi-"].bg,.vp-icon.bg{background-size:100% 100%;background-color:transparent}[class^=vpi-]:not(.bg),[class*=" vpi-"]:not(.bg),.vp-icon:not(.bg){-webkit-mask:var(--icon) no-repeat;mask:var(--icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit}.vpi-align-left{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M21 6H3M15 12H3M17 18H3'/%3E%3C/svg%3E")}.vpi-arrow-right,.vpi-arrow-down,.vpi-arrow-left,.vpi-arrow-up{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5l7 7-7 7'/%3E%3C/svg%3E")}.vpi-chevron-right,.vpi-chevron-down,.vpi-chevron-left,.vpi-chevron-up{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E")}.vpi-chevron-down,.vpi-arrow-down{transform:rotate(90deg)}.vpi-chevron-left,.vpi-arrow-left{transform:rotate(180deg)}.vpi-chevron-up,.vpi-arrow-up{transform:rotate(-90deg)}.vpi-square-pen{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7'/%3E%3Cpath d='M18.375 2.625a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4Z'/%3E%3C/svg%3E")}.vpi-plus{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5v14'/%3E%3C/svg%3E")}.vpi-sun{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Cpath d='M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41'/%3E%3C/svg%3E")}.vpi-moon{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z'/%3E%3C/svg%3E")}.vpi-more-horizontal{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='1'/%3E%3Ccircle cx='19' cy='12' r='1'/%3E%3Ccircle cx='5' cy='12' r='1'/%3E%3C/svg%3E")}.vpi-languages{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m5 8 6 6M4 14l6-6 2-3M2 5h12M7 2h1M22 22l-5-10-5 10M14 18h6'/%3E%3C/svg%3E")}.vpi-heart{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z'/%3E%3C/svg%3E")}.vpi-search{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E")}.vpi-layout-list{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='7' height='7' x='3' y='3' rx='1'/%3E%3Crect width='7' height='7' x='3' y='14' rx='1'/%3E%3Cpath d='M14 4h7M14 9h7M14 15h7M14 20h7'/%3E%3C/svg%3E")}.vpi-delete{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M20 5H9l-7 7 7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2ZM18 9l-6 6M12 9l6 6'/%3E%3C/svg%3E")}.vpi-corner-down-left{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 10-5 5 5 5'/%3E%3Cpath d='M20 4v7a4 4 0 0 1-4 4H4'/%3E%3C/svg%3E")}:root{--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E");--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E")}.visually-hidden{position:absolute;width:1px;height:1px;white-space:nowrap;clip:rect(0 0 0 0);clip-path:inset(50%);overflow:hidden}.custom-block{border:1px solid transparent;border-radius:8px;padding:16px 16px 8px;line-height:24px;font-size:var(--vp-custom-block-font-size);color:var(--vp-c-text-2)}.custom-block.info{border-color:var(--vp-custom-block-info-border);color:var(--vp-custom-block-info-text);background-color:var(--vp-custom-block-info-bg)}.custom-block.info a,.custom-block.info code{color:var(--vp-c-brand-1)}.custom-block.info a:hover,.custom-block.info a:hover>code{color:var(--vp-c-brand-2)}.custom-block.info code{background-color:var(--vp-custom-block-info-code-bg)}.custom-block.note{border-color:var(--vp-custom-block-note-border);color:var(--vp-custom-block-note-text);background-color:var(--vp-custom-block-note-bg)}.custom-block.note a,.custom-block.note code{color:var(--vp-c-brand-1)}.custom-block.note a:hover,.custom-block.note a:hover>code{color:var(--vp-c-brand-2)}.custom-block.note code{background-color:var(--vp-custom-block-note-code-bg)}.custom-block.tip{border-color:var(--vp-custom-block-tip-border);color:var(--vp-custom-block-tip-text);background-color:var(--vp-custom-block-tip-bg)}.custom-block.tip a,.custom-block.tip code{color:var(--vp-c-tip-1)}.custom-block.tip a:hover,.custom-block.tip a:hover>code{color:var(--vp-c-tip-2)}.custom-block.tip code{background-color:var(--vp-custom-block-tip-code-bg)}.custom-block.important{border-color:var(--vp-custom-block-important-border);color:var(--vp-custom-block-important-text);background-color:var(--vp-custom-block-important-bg)}.custom-block.important a,.custom-block.important code{color:var(--vp-c-important-1)}.custom-block.important a:hover,.custom-block.important a:hover>code{color:var(--vp-c-important-2)}.custom-block.important code{background-color:var(--vp-custom-block-important-code-bg)}.custom-block.warning{border-color:var(--vp-custom-block-warning-border);color:var(--vp-custom-block-warning-text);background-color:var(--vp-custom-block-warning-bg)}.custom-block.warning a,.custom-block.warning code{color:var(--vp-c-warning-1)}.custom-block.warning a:hover,.custom-block.warning a:hover>code{color:var(--vp-c-warning-2)}.custom-block.warning code{background-color:var(--vp-custom-block-warning-code-bg)}.custom-block.danger{border-color:var(--vp-custom-block-danger-border);color:var(--vp-custom-block-danger-text);background-color:var(--vp-custom-block-danger-bg)}.custom-block.danger a,.custom-block.danger code{color:var(--vp-c-danger-1)}.custom-block.danger a:hover,.custom-block.danger a:hover>code{color:var(--vp-c-danger-2)}.custom-block.danger code{background-color:var(--vp-custom-block-danger-code-bg)}.custom-block.caution{border-color:var(--vp-custom-block-caution-border);color:var(--vp-custom-block-caution-text);background-color:var(--vp-custom-block-caution-bg)}.custom-block.caution a,.custom-block.caution code{color:var(--vp-c-caution-1)}.custom-block.caution a:hover,.custom-block.caution a:hover>code{color:var(--vp-c-caution-2)}.custom-block.caution code{background-color:var(--vp-custom-block-caution-code-bg)}.custom-block.details{border-color:var(--vp-custom-block-details-border);color:var(--vp-custom-block-details-text);background-color:var(--vp-custom-block-details-bg)}.custom-block.details a{color:var(--vp-c-brand-1)}.custom-block.details a:hover,.custom-block.details a:hover>code{color:var(--vp-c-brand-2)}.custom-block.details code{background-color:var(--vp-custom-block-details-code-bg)}.custom-block-title{font-weight:600}.custom-block p+p{margin:8px 0}.custom-block.details summary{margin:0 0 8px;font-weight:700;cursor:pointer;-webkit-user-select:none;user-select:none}.custom-block.details summary+p{margin:8px 0}.custom-block a{color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px;transition:opacity .25s}.custom-block a:hover{opacity:.75}.custom-block code{font-size:var(--vp-custom-block-code-font-size)}.custom-block.custom-block th,.custom-block.custom-block blockquote>p{font-size:var(--vp-custom-block-font-size);color:inherit}.dark .vp-code span{color:var(--shiki-dark, inherit)}html:not(.dark) .vp-code span{color:var(--shiki-light, inherit)}.vp-code-group{margin-top:16px}.vp-code-group .tabs{position:relative;display:flex;margin-right:-24px;margin-left:-24px;padding:0 12px;background-color:var(--vp-code-tab-bg);overflow-x:auto;overflow-y:hidden;box-shadow:inset 0 -1px var(--vp-code-tab-divider)}@media (min-width: 640px){.vp-code-group .tabs{margin-right:0;margin-left:0;border-radius:8px 8px 0 0}}.vp-code-group .tabs input{position:fixed;opacity:0;pointer-events:none}.vp-code-group .tabs label{position:relative;display:inline-block;border-bottom:1px solid transparent;padding:0 12px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-code-tab-text-color);white-space:nowrap;cursor:pointer;transition:color .25s}.vp-code-group .tabs label:after{position:absolute;right:8px;bottom:-1px;left:8px;z-index:1;height:2px;border-radius:2px;content:"";background-color:transparent;transition:background-color .25s}.vp-code-group label:hover{color:var(--vp-code-tab-hover-text-color)}.vp-code-group input:checked+label{color:var(--vp-code-tab-active-text-color)}.vp-code-group input:checked+label:after{background-color:var(--vp-code-tab-active-bar-color)}.vp-code-group div[class*=language-],.vp-block{display:none;margin-top:0!important;border-top-left-radius:0!important;border-top-right-radius:0!important}.vp-code-group div[class*=language-].active,.vp-block.active{display:block}.vp-block{padding:20px 24px}.vp-doc h1,.vp-doc h2,.vp-doc h3,.vp-doc h4,.vp-doc h5,.vp-doc h6{position:relative;font-weight:600;outline:none}.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:28px}.vp-doc h2{margin:48px 0 16px;border-top:1px solid var(--vp-c-divider);padding-top:24px;letter-spacing:-.02em;line-height:32px;font-size:24px}.vp-doc h3{margin:32px 0 0;letter-spacing:-.01em;line-height:28px;font-size:20px}.vp-doc h4{margin:24px 0 0;letter-spacing:-.01em;line-height:24px;font-size:18px}.vp-doc .header-anchor{position:absolute;top:0;left:0;margin-left:-.87em;font-weight:500;-webkit-user-select:none;user-select:none;opacity:0;text-decoration:none;transition:color .25s,opacity .25s}.vp-doc .header-anchor:before{content:var(--vp-header-anchor-symbol)}.vp-doc h1:hover .header-anchor,.vp-doc h1 .header-anchor:focus,.vp-doc h2:hover .header-anchor,.vp-doc h2 .header-anchor:focus,.vp-doc h3:hover .header-anchor,.vp-doc h3 .header-anchor:focus,.vp-doc h4:hover .header-anchor,.vp-doc h4 .header-anchor:focus,.vp-doc h5:hover .header-anchor,.vp-doc h5 .header-anchor:focus,.vp-doc h6:hover .header-anchor,.vp-doc h6 .header-anchor:focus{opacity:1}@media (min-width: 768px){.vp-doc h1{letter-spacing:-.02em;line-height:40px;font-size:32px}}.vp-doc h2 .header-anchor{top:24px}.vp-doc p,.vp-doc summary{margin:16px 0}.vp-doc p{line-height:28px}.vp-doc blockquote{margin:16px 0;border-left:2px solid var(--vp-c-divider);padding-left:16px;transition:border-color .5s;color:var(--vp-c-text-2)}.vp-doc blockquote>p{margin:0;font-size:16px;transition:color .5s}.vp-doc a{font-weight:500;color:var(--vp-c-brand-1);text-decoration:underline;text-underline-offset:2px;transition:color .25s,opacity .25s}.vp-doc a:hover{color:var(--vp-c-brand-2)}.vp-doc strong{font-weight:600}.vp-doc ul,.vp-doc ol{padding-left:1.25rem;margin:16px 0}.vp-doc ul{list-style:disc}.vp-doc ol{list-style:decimal}.vp-doc li+li{margin-top:8px}.vp-doc li>ol,.vp-doc li>ul{margin:8px 0 0}.vp-doc table{display:block;border-collapse:collapse;margin:20px 0;overflow-x:auto}.vp-doc tr{background-color:var(--vp-c-bg);border-top:1px solid var(--vp-c-divider);transition:background-color .5s}.vp-doc tr:nth-child(2n){background-color:var(--vp-c-bg-soft)}.vp-doc th,.vp-doc td{border:1px solid var(--vp-c-divider);padding:8px 16px}.vp-doc th{text-align:left;font-size:14px;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-doc td{font-size:14px}.vp-doc hr{margin:16px 0;border:none;border-top:1px solid var(--vp-c-divider)}.vp-doc .custom-block{margin:16px 0}.vp-doc .custom-block p{margin:8px 0;line-height:24px}.vp-doc .custom-block p:first-child{margin:0}.vp-doc .custom-block div[class*=language-]{margin:8px 0;border-radius:8px}.vp-doc .custom-block div[class*=language-] code{font-weight:400;background-color:transparent}.vp-doc .custom-block .vp-code-group .tabs{margin:0;border-radius:8px 8px 0 0}.vp-doc :not(pre,h1,h2,h3,h4,h5,h6)>code{font-size:var(--vp-code-font-size);color:var(--vp-code-color)}.vp-doc :not(pre)>code{border-radius:4px;padding:3px 6px;background-color:var(--vp-code-bg);transition:color .25s,background-color .5s}.vp-doc a>code{color:var(--vp-code-link-color)}.vp-doc a:hover>code{color:var(--vp-code-link-hover-color)}.vp-doc h1>code,.vp-doc h2>code,.vp-doc h3>code,.vp-doc h4>code{font-size:.9em}.vp-doc div[class*=language-],.vp-block{position:relative;margin:16px -24px;background-color:var(--vp-code-block-bg);overflow-x:auto;transition:background-color .5s}@media (min-width: 640px){.vp-doc div[class*=language-],.vp-block{border-radius:8px;margin:16px 0}}@media (max-width: 639px){.vp-doc li div[class*=language-]{border-radius:8px 0 0 8px}}.vp-doc div[class*=language-]+div[class*=language-],.vp-doc div[class$=-api]+div[class*=language-],.vp-doc div[class*=language-]+div[class$=-api]>div[class*=language-]{margin-top:-8px}.vp-doc [class*=language-] pre,.vp-doc [class*=language-] code{direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}.vp-doc [class*=language-] pre{position:relative;z-index:1;margin:0;padding:20px 0;background:transparent;overflow-x:auto}.vp-doc [class*=language-] code{display:block;padding:0 24px;width:fit-content;min-width:100%;line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-block-color);transition:color .5s}.vp-doc [class*=language-] code .highlighted{background-color:var(--vp-code-line-highlight-color);transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .highlighted.error{background-color:var(--vp-code-line-error-color)}.vp-doc [class*=language-] code .highlighted.warning{background-color:var(--vp-code-line-warning-color)}.vp-doc [class*=language-] code .diff{transition:background-color .5s;margin:0 -24px;padding:0 24px;width:calc(100% + 48px);display:inline-block}.vp-doc [class*=language-] code .diff:before{position:absolute;left:10px}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){filter:blur(.095rem);opacity:.4;transition:filter .35s,opacity .35s}.vp-doc [class*=language-] .has-focused-lines .line:not(.has-focus){opacity:.7;transition:filter .35s,opacity .35s}.vp-doc [class*=language-]:hover .has-focused-lines .line:not(.has-focus){filter:blur(0);opacity:1}.vp-doc [class*=language-] code .diff.remove{background-color:var(--vp-code-line-diff-remove-color);opacity:.7}.vp-doc [class*=language-] code .diff.remove:before{content:"-";color:var(--vp-code-line-diff-remove-symbol-color)}.vp-doc [class*=language-] code .diff.add{background-color:var(--vp-code-line-diff-add-color)}.vp-doc [class*=language-] code .diff.add:before{content:"+";color:var(--vp-code-line-diff-add-symbol-color)}.vp-doc div[class*=language-].line-numbers-mode{padding-left:32px}.vp-doc .line-numbers-wrapper{position:absolute;top:0;bottom:0;left:0;z-index:3;border-right:1px solid var(--vp-code-block-divider-color);padding-top:20px;width:32px;text-align:center;font-family:var(--vp-font-family-mono);line-height:var(--vp-code-line-height);font-size:var(--vp-code-font-size);color:var(--vp-code-line-number-color);transition:border-color .5s,color .5s}.vp-doc [class*=language-]>button.copy{direction:ltr;position:absolute;top:12px;right:12px;z-index:3;border:1px solid var(--vp-code-copy-code-border-color);border-radius:4px;width:40px;height:40px;background-color:var(--vp-code-copy-code-bg);opacity:0;cursor:pointer;background-image:var(--vp-icon-copy);background-position:50%;background-size:20px;background-repeat:no-repeat;transition:border-color .25s,background-color .25s,opacity .25s}.vp-doc [class*=language-]:hover>button.copy,.vp-doc [class*=language-]>button.copy:focus{opacity:1}.vp-doc [class*=language-]>button.copy:hover,.vp-doc [class*=language-]>button.copy.copied{border-color:var(--vp-code-copy-code-hover-border-color);background-color:var(--vp-code-copy-code-hover-bg)}.vp-doc [class*=language-]>button.copy.copied,.vp-doc [class*=language-]>button.copy:hover.copied{border-radius:0 4px 4px 0;background-color:var(--vp-code-copy-code-hover-bg);background-image:var(--vp-icon-copied)}.vp-doc [class*=language-]>button.copy.copied:before,.vp-doc [class*=language-]>button.copy:hover.copied:before{position:relative;top:-1px;transform:translate(calc(-100% - 1px));display:flex;justify-content:center;align-items:center;border:1px solid var(--vp-code-copy-code-hover-border-color);border-right:0;border-radius:4px 0 0 4px;padding:0 10px;width:fit-content;height:40px;text-align:center;font-size:12px;font-weight:500;color:var(--vp-code-copy-code-active-text);background-color:var(--vp-code-copy-code-hover-bg);white-space:nowrap;content:var(--vp-code-copy-copied-text-content)}.vp-doc [class*=language-]>span.lang{position:absolute;top:2px;right:8px;z-index:2;font-size:12px;font-weight:500;-webkit-user-select:none;user-select:none;color:var(--vp-code-lang-color);transition:color .4s,opacity .4s}.vp-doc [class*=language-]:hover>button.copy+span.lang,.vp-doc [class*=language-]>button.copy:focus+span.lang{opacity:0}.vp-doc .VPTeamMembers{margin-top:24px}.vp-doc .VPTeamMembers.small.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}.vp-doc .VPTeamMembers.small.count-2 .container,.vp-doc .VPTeamMembers.small.count-3 .container{max-width:100%!important}.vp-doc .VPTeamMembers.medium.count-1 .container{margin:0!important;max-width:calc((100% - 24px)/2)!important}:is(.vp-external-link-icon,.vp-doc a[href*="://"],.vp-doc a[target=_blank]):not(.no-icon):after{display:inline-block;margin-top:-1px;margin-left:4px;width:11px;height:11px;background:currentColor;color:var(--vp-c-text-3);flex-shrink:0;--icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E");-webkit-mask-image:var(--icon);mask-image:var(--icon)}.vp-external-link-icon:after{content:""}.external-link-icon-enabled :is(.vp-doc a[href*="://"],.vp-doc a[target=_blank]):after{content:"";color:currentColor}.vp-sponsor{border-radius:16px;overflow:hidden}.vp-sponsor.aside{border-radius:12px}.vp-sponsor-section+.vp-sponsor-section{margin-top:4px}.vp-sponsor-tier{margin:0 0 4px!important;text-align:center;letter-spacing:1px!important;line-height:24px;width:100%;font-weight:600;color:var(--vp-c-text-2);background-color:var(--vp-c-bg-soft)}.vp-sponsor.normal .vp-sponsor-tier{padding:13px 0 11px;font-size:14px}.vp-sponsor.aside .vp-sponsor-tier{padding:9px 0 7px;font-size:12px}.vp-sponsor-grid+.vp-sponsor-tier{margin-top:4px}.vp-sponsor-grid{display:flex;flex-wrap:wrap;gap:4px}.vp-sponsor-grid.xmini .vp-sponsor-grid-link{height:64px}.vp-sponsor-grid.xmini .vp-sponsor-grid-image{max-width:64px;max-height:22px}.vp-sponsor-grid.mini .vp-sponsor-grid-link{height:72px}.vp-sponsor-grid.mini .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.small .vp-sponsor-grid-link{height:96px}.vp-sponsor-grid.small .vp-sponsor-grid-image{max-width:96px;max-height:24px}.vp-sponsor-grid.medium .vp-sponsor-grid-link{height:112px}.vp-sponsor-grid.medium .vp-sponsor-grid-image{max-width:120px;max-height:36px}.vp-sponsor-grid.big .vp-sponsor-grid-link{height:184px}.vp-sponsor-grid.big .vp-sponsor-grid-image{max-width:192px;max-height:56px}.vp-sponsor-grid[data-vp-grid="2"] .vp-sponsor-grid-item{width:calc((100% - 4px)/2)}.vp-sponsor-grid[data-vp-grid="3"] .vp-sponsor-grid-item{width:calc((100% - 4px * 2) / 3)}.vp-sponsor-grid[data-vp-grid="4"] .vp-sponsor-grid-item{width:calc((100% - 12px)/4)}.vp-sponsor-grid[data-vp-grid="5"] .vp-sponsor-grid-item{width:calc((100% - 16px)/5)}.vp-sponsor-grid[data-vp-grid="6"] .vp-sponsor-grid-item{width:calc((100% - 4px * 5) / 6)}.vp-sponsor-grid-item{flex-shrink:0;width:100%;background-color:var(--vp-c-bg-soft);transition:background-color .25s}.vp-sponsor-grid-item:hover{background-color:var(--vp-c-default-soft)}.vp-sponsor-grid-item:hover .vp-sponsor-grid-image{filter:grayscale(0) invert(0)}.vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.dark .vp-sponsor-grid-item:hover{background-color:var(--vp-c-white)}.dark .vp-sponsor-grid-item.empty:hover{background-color:var(--vp-c-bg-soft)}.vp-sponsor-grid-link{display:flex}.vp-sponsor-grid-box{display:flex;justify-content:center;align-items:center;width:100%}.vp-sponsor-grid-image{max-width:100%;filter:grayscale(1);transition:filter .25s}.dark .vp-sponsor-grid-image{filter:grayscale(1) invert(1)}.VPBadge{display:inline-block;margin-left:2px;border:1px solid transparent;border-radius:12px;padding:0 10px;line-height:22px;font-size:12px;font-weight:500;transform:translateY(-2px)}.VPBadge.small{padding:0 6px;line-height:18px;font-size:10px;transform:translateY(-8px)}.VPDocFooter .VPBadge{display:none}.vp-doc h1>.VPBadge{margin-top:4px;vertical-align:top}.vp-doc h2>.VPBadge{margin-top:3px;padding:0 8px;vertical-align:top}.vp-doc h3>.VPBadge{vertical-align:middle}.vp-doc h4>.VPBadge,.vp-doc h5>.VPBadge,.vp-doc h6>.VPBadge{vertical-align:middle;line-height:18px}.VPBadge.info{border-color:var(--vp-badge-info-border);color:var(--vp-badge-info-text);background-color:var(--vp-badge-info-bg)}.VPBadge.tip{border-color:var(--vp-badge-tip-border);color:var(--vp-badge-tip-text);background-color:var(--vp-badge-tip-bg)}.VPBadge.warning{border-color:var(--vp-badge-warning-border);color:var(--vp-badge-warning-text);background-color:var(--vp-badge-warning-bg)}.VPBadge.danger{border-color:var(--vp-badge-danger-border);color:var(--vp-badge-danger-text);background-color:var(--vp-badge-danger-bg)}.VPBackdrop[data-v-045f5fb9]{position:fixed;top:0;right:0;bottom:0;left:0;z-index:var(--vp-z-index-backdrop);background:var(--vp-backdrop-bg-color);transition:opacity .5s}.VPBackdrop.fade-enter-from[data-v-045f5fb9],.VPBackdrop.fade-leave-to[data-v-045f5fb9]{opacity:0}.VPBackdrop.fade-leave-active[data-v-045f5fb9]{transition-duration:.25s}@media (min-width: 1280px){.VPBackdrop[data-v-045f5fb9]{display:none}}.NotFound[data-v-f6ba6842]{padding:64px 24px 96px;text-align:center}@media (min-width: 768px){.NotFound[data-v-f6ba6842]{padding:96px 32px 168px}}.code[data-v-f6ba6842]{line-height:64px;font-size:64px;font-weight:600}.title[data-v-f6ba6842]{padding-top:12px;letter-spacing:2px;line-height:20px;font-size:20px;font-weight:700}.divider[data-v-f6ba6842]{margin:24px auto 18px;width:64px;height:1px;background-color:var(--vp-c-divider)}.quote[data-v-f6ba6842]{margin:0 auto;max-width:256px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.action[data-v-f6ba6842]{padding-top:20px}.link[data-v-f6ba6842]{display:inline-block;border:1px solid var(--vp-c-brand-1);border-radius:16px;padding:3px 16px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:border-color .25s,color .25s}.link[data-v-f6ba6842]:hover{border-color:var(--vp-c-brand-2);color:var(--vp-c-brand-2)}.root[data-v-b3dcc18f]{position:relative;z-index:1}.nested[data-v-b3dcc18f]{padding-right:16px;padding-left:16px}.outline-link[data-v-b3dcc18f]{display:block;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .5s}.outline-link[data-v-b3dcc18f]:hover,.outline-link.active[data-v-b3dcc18f]{color:var(--vp-c-text-1);transition:color .25s}.outline-link.nested[data-v-b3dcc18f]{padding-left:13px}.VPDocAsideOutline[data-v-dc7ddf28]{display:none}.VPDocAsideOutline.has-outline[data-v-dc7ddf28]{display:block}.content[data-v-dc7ddf28]{position:relative;border-left:1px solid var(--vp-c-divider);padding-left:16px;font-size:13px;font-weight:500}.outline-marker[data-v-dc7ddf28]{position:absolute;top:32px;left:-1px;z-index:0;opacity:0;width:2px;border-radius:2px;height:18px;background-color:var(--vp-c-brand-1);transition:top .25s cubic-bezier(0,1,.5,1),background-color .5s,opacity .25s}.outline-title[data-v-dc7ddf28]{line-height:32px;font-size:14px;font-weight:600}.VPDocAside[data-v-4ab932ff]{display:flex;flex-direction:column;flex-grow:1}.spacer[data-v-4ab932ff]{flex-grow:1}.VPDocAside[data-v-4ab932ff] .spacer+.VPDocAsideSponsors,.VPDocAside[data-v-4ab932ff] .spacer+.VPDocAsideCarbonAds{margin-top:24px}.VPDocAside[data-v-4ab932ff] .VPDocAsideSponsors+.VPDocAsideCarbonAds{margin-top:16px}.VPLastUpdated[data-v-d820616a]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 640px){.VPLastUpdated[data-v-d820616a]{line-height:32px;font-size:14px;font-weight:500}}.VPDocFooter[data-v-1e45e8a5]{margin-top:64px}.edit-info[data-v-1e45e8a5]{padding-bottom:18px}@media (min-width: 640px){.edit-info[data-v-1e45e8a5]{display:flex;justify-content:space-between;align-items:center;padding-bottom:14px}}.edit-link-button[data-v-1e45e8a5]{display:flex;align-items:center;border:0;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.edit-link-button[data-v-1e45e8a5]:hover{color:var(--vp-c-brand-2)}.edit-link-icon[data-v-1e45e8a5]{margin-right:8px}.prev-next[data-v-1e45e8a5]{border-top:1px solid var(--vp-c-divider);padding-top:24px;display:grid;grid-row-gap:8px}@media (min-width: 640px){.prev-next[data-v-1e45e8a5]{grid-template-columns:repeat(2,1fr);grid-column-gap:16px}}.pager-link[data-v-1e45e8a5]{display:block;border:1px solid var(--vp-c-divider);border-radius:8px;padding:11px 16px 13px;width:100%;height:100%;transition:border-color .25s}.pager-link[data-v-1e45e8a5]:hover{border-color:var(--vp-c-brand-1)}.pager-link.next[data-v-1e45e8a5]{margin-left:auto;text-align:right}.desc[data-v-1e45e8a5]{display:block;line-height:20px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.title[data-v-1e45e8a5]{display:block;line-height:20px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.VPDoc[data-v-6cdb07e7]{padding:32px 24px 96px;width:100%}@media (min-width: 768px){.VPDoc[data-v-6cdb07e7]{padding:48px 32px 128px}}@media (min-width: 960px){.VPDoc[data-v-6cdb07e7]{padding:48px 32px 0}.VPDoc:not(.has-sidebar) .container[data-v-6cdb07e7]{display:flex;justify-content:center;max-width:992px}.VPDoc:not(.has-sidebar) .content[data-v-6cdb07e7]{max-width:752px}}@media (min-width: 1280px){.VPDoc .container[data-v-6cdb07e7]{display:flex;justify-content:center}.VPDoc .aside[data-v-6cdb07e7]{display:block}}@media (min-width: 1440px){.VPDoc:not(.has-sidebar) .content[data-v-6cdb07e7]{max-width:784px}.VPDoc:not(.has-sidebar) .container[data-v-6cdb07e7]{max-width:1104px}}.container[data-v-6cdb07e7]{margin:0 auto;width:100%}.aside[data-v-6cdb07e7]{position:relative;display:none;order:2;flex-grow:1;padding-left:32px;width:100%;max-width:256px}.left-aside[data-v-6cdb07e7]{order:1;padding-left:unset;padding-right:32px}.aside-container[data-v-6cdb07e7]{position:fixed;top:0;padding-top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + var(--vp-doc-top-height, 0px) + 48px);width:224px;height:100vh;overflow-x:hidden;overflow-y:auto;scrollbar-width:none}.aside-container[data-v-6cdb07e7]::-webkit-scrollbar{display:none}.aside-curtain[data-v-6cdb07e7]{position:fixed;bottom:0;z-index:10;width:224px;height:32px;background:linear-gradient(transparent,var(--vp-c-bg) 70%)}.aside-content[data-v-6cdb07e7]{display:flex;flex-direction:column;min-height:calc(100vh - (var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px));padding-bottom:32px}.content[data-v-6cdb07e7]{position:relative;margin:0 auto;width:100%}@media (min-width: 960px){.content[data-v-6cdb07e7]{padding:0 32px 128px}}@media (min-width: 1280px){.content[data-v-6cdb07e7]{order:1;margin:0;min-width:640px}}.content-container[data-v-6cdb07e7]{margin:0 auto}.VPDoc.has-aside .content-container[data-v-6cdb07e7]{max-width:688px}.VPButton[data-v-0cfdd755]{display:inline-block;border:1px solid transparent;text-align:center;font-weight:600;white-space:nowrap;transition:color .25s,border-color .25s,background-color .25s}.VPButton[data-v-0cfdd755]:active{transition:color .1s,border-color .1s,background-color .1s}.VPButton.medium[data-v-0cfdd755]{border-radius:20px;padding:0 20px;line-height:38px;font-size:14px}.VPButton.big[data-v-0cfdd755]{border-radius:24px;padding:0 24px;line-height:46px;font-size:16px}.VPButton.brand[data-v-0cfdd755]{border-color:var(--vp-button-brand-border);color:var(--vp-button-brand-text);background-color:var(--vp-button-brand-bg)}.VPButton.brand[data-v-0cfdd755]:hover{border-color:var(--vp-button-brand-hover-border);color:var(--vp-button-brand-hover-text);background-color:var(--vp-button-brand-hover-bg)}.VPButton.brand[data-v-0cfdd755]:active{border-color:var(--vp-button-brand-active-border);color:var(--vp-button-brand-active-text);background-color:var(--vp-button-brand-active-bg)}.VPButton.alt[data-v-0cfdd755]{border-color:var(--vp-button-alt-border);color:var(--vp-button-alt-text);background-color:var(--vp-button-alt-bg)}.VPButton.alt[data-v-0cfdd755]:hover{border-color:var(--vp-button-alt-hover-border);color:var(--vp-button-alt-hover-text);background-color:var(--vp-button-alt-hover-bg)}.VPButton.alt[data-v-0cfdd755]:active{border-color:var(--vp-button-alt-active-border);color:var(--vp-button-alt-active-text);background-color:var(--vp-button-alt-active-bg)}.VPButton.sponsor[data-v-0cfdd755]{border-color:var(--vp-button-sponsor-border);color:var(--vp-button-sponsor-text);background-color:var(--vp-button-sponsor-bg)}.VPButton.sponsor[data-v-0cfdd755]:hover{border-color:var(--vp-button-sponsor-hover-border);color:var(--vp-button-sponsor-hover-text);background-color:var(--vp-button-sponsor-hover-bg)}.VPButton.sponsor[data-v-0cfdd755]:active{border-color:var(--vp-button-sponsor-active-border);color:var(--vp-button-sponsor-active-text);background-color:var(--vp-button-sponsor-active-bg)}html:not(.dark) .VPImage.dark[data-v-744f05e2]{display:none}.dark .VPImage.light[data-v-744f05e2]{display:none}.VPHero[data-v-728b9a44]{margin-top:calc((var(--vp-nav-height) + var(--vp-layout-top-height, 0px)) * -1);padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px) 24px 48px}@media (min-width: 640px){.VPHero[data-v-728b9a44]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px}}@media (min-width: 960px){.VPHero[data-v-728b9a44]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px}}.container[data-v-728b9a44]{display:flex;flex-direction:column;margin:0 auto;max-width:1152px}@media (min-width: 960px){.container[data-v-728b9a44]{flex-direction:row}}.main[data-v-728b9a44]{position:relative;z-index:10;order:2;flex-grow:1;flex-shrink:0}.VPHero.has-image .container[data-v-728b9a44]{text-align:center}@media (min-width: 960px){.VPHero.has-image .container[data-v-728b9a44]{text-align:left}}@media (min-width: 960px){.main[data-v-728b9a44]{order:1;width:calc((100% / 3) * 2)}.VPHero.has-image .main[data-v-728b9a44]{max-width:592px}}.name[data-v-728b9a44],.text[data-v-728b9a44]{max-width:392px;letter-spacing:-.4px;line-height:40px;font-size:32px;font-weight:700;white-space:pre-wrap}.VPHero.has-image .name[data-v-728b9a44],.VPHero.has-image .text[data-v-728b9a44]{margin:0 auto}.name[data-v-728b9a44]{color:var(--vp-home-hero-name-color)}.clip[data-v-728b9a44]{background:var(--vp-home-hero-name-background);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:var(--vp-home-hero-name-color)}@media (min-width: 640px){.name[data-v-728b9a44],.text[data-v-728b9a44]{max-width:576px;line-height:56px;font-size:48px}}@media (min-width: 960px){.name[data-v-728b9a44],.text[data-v-728b9a44]{line-height:64px;font-size:56px}.VPHero.has-image .name[data-v-728b9a44],.VPHero.has-image .text[data-v-728b9a44]{margin:0}}.tagline[data-v-728b9a44]{padding-top:8px;max-width:392px;line-height:28px;font-size:18px;font-weight:500;white-space:pre-wrap;color:var(--vp-c-text-2)}.VPHero.has-image .tagline[data-v-728b9a44]{margin:0 auto}@media (min-width: 640px){.tagline[data-v-728b9a44]{padding-top:12px;max-width:576px;line-height:32px;font-size:20px}}@media (min-width: 960px){.tagline[data-v-728b9a44]{line-height:36px;font-size:24px}.VPHero.has-image .tagline[data-v-728b9a44]{margin:0}}.actions[data-v-728b9a44]{display:flex;flex-wrap:wrap;margin:-6px;padding-top:24px}.VPHero.has-image .actions[data-v-728b9a44]{justify-content:center}@media (min-width: 640px){.actions[data-v-728b9a44]{padding-top:32px}}@media (min-width: 960px){.VPHero.has-image .actions[data-v-728b9a44]{justify-content:flex-start}}.action[data-v-728b9a44]{flex-shrink:0;padding:6px}.image[data-v-728b9a44]{order:1;margin:-76px -24px -48px}@media (min-width: 640px){.image[data-v-728b9a44]{margin:-108px -24px -48px}}@media (min-width: 960px){.image[data-v-728b9a44]{flex-grow:1;order:2;margin:0;min-height:100%}}.image-container[data-v-728b9a44]{position:relative;margin:0 auto;width:320px;height:320px}@media (min-width: 640px){.image-container[data-v-728b9a44]{width:392px;height:392px}}@media (min-width: 960px){.image-container[data-v-728b9a44]{display:flex;justify-content:center;align-items:center;width:100%;height:100%;transform:translate(-32px,-32px)}}.image-bg[data-v-728b9a44]{position:absolute;top:50%;left:50%;border-radius:50%;width:192px;height:192px;background-image:var(--vp-home-hero-image-background-image);filter:var(--vp-home-hero-image-filter);transform:translate(-50%,-50%)}@media (min-width: 640px){.image-bg[data-v-728b9a44]{width:256px;height:256px}}@media (min-width: 960px){.image-bg[data-v-728b9a44]{width:320px;height:320px}}[data-v-728b9a44] .image-src{position:absolute;top:50%;left:50%;max-width:192px;max-height:192px;transform:translate(-50%,-50%)}@media (min-width: 640px){[data-v-728b9a44] .image-src{max-width:256px;max-height:256px}}@media (min-width: 960px){[data-v-728b9a44] .image-src{max-width:320px;max-height:320px}}.VPFeature[data-v-aca5adb4]{display:block;border:1px solid var(--vp-c-bg-soft);border-radius:12px;height:100%;background-color:var(--vp-c-bg-soft);transition:border-color .25s,background-color .25s}.VPFeature.link[data-v-aca5adb4]:hover{border-color:var(--vp-c-brand-1)}.box[data-v-aca5adb4]{display:flex;flex-direction:column;padding:24px;height:100%}.box[data-v-aca5adb4]>.VPImage{margin-bottom:20px}.icon[data-v-aca5adb4]{display:flex;justify-content:center;align-items:center;margin-bottom:20px;border-radius:6px;background-color:var(--vp-c-default-soft);width:48px;height:48px;font-size:24px;transition:background-color .25s}.title[data-v-aca5adb4]{line-height:24px;font-size:16px;font-weight:600}.details[data-v-aca5adb4]{flex-grow:1;padding-top:8px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.link-text[data-v-aca5adb4]{padding-top:8px}.link-text-value[data-v-aca5adb4]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.link-text-icon[data-v-aca5adb4]{margin-left:6px}.VPFeatures[data-v-1e7b2819]{position:relative;padding:0 24px}@media (min-width: 640px){.VPFeatures[data-v-1e7b2819]{padding:0 48px}}@media (min-width: 960px){.VPFeatures[data-v-1e7b2819]{padding:0 64px}}.container[data-v-1e7b2819]{margin:0 auto;max-width:1152px}.items[data-v-1e7b2819]{display:flex;flex-wrap:wrap;margin:-8px}.item[data-v-1e7b2819]{padding:8px;width:100%}@media (min-width: 640px){.item.grid-2[data-v-1e7b2819],.item.grid-4[data-v-1e7b2819],.item.grid-6[data-v-1e7b2819]{width:50%}}@media (min-width: 768px){.item.grid-2[data-v-1e7b2819],.item.grid-4[data-v-1e7b2819]{width:50%}.item.grid-3[data-v-1e7b2819],.item.grid-6[data-v-1e7b2819]{width:calc(100% / 3)}}@media (min-width: 960px){.item.grid-4[data-v-1e7b2819]{width:25%}}.container[data-v-2ecb6a76]{margin:auto;width:100%;max-width:1280px;padding:0 24px}@media (min-width: 640px){.container[data-v-2ecb6a76]{padding:0 48px}}@media (min-width: 960px){.container[data-v-2ecb6a76]{width:100%;padding:0 64px}}.vp-doc[data-v-2ecb6a76] .VPHomeSponsors,.vp-doc[data-v-2ecb6a76] .VPTeamPage{margin-left:var(--vp-offset, calc(50% - 50vw) );margin-right:var(--vp-offset, calc(50% - 50vw) )}.vp-doc[data-v-2ecb6a76] .VPHomeSponsors h2{border-top:none;letter-spacing:normal}.vp-doc[data-v-2ecb6a76] .VPHomeSponsors a,.vp-doc[data-v-2ecb6a76] .VPTeamPage a{text-decoration:none}.VPHome[data-v-52976480]{margin-bottom:96px}@media (min-width: 768px){.VPHome[data-v-52976480]{margin-bottom:128px}}.VPContent[data-v-a2887130]{flex-grow:1;flex-shrink:0;margin:var(--vp-layout-top-height, 0px) auto 0;width:100%}.VPContent.is-home[data-v-a2887130]{width:100%;max-width:100%}.VPContent.has-sidebar[data-v-a2887130]{margin:0}@media (min-width: 960px){.VPContent[data-v-a2887130]{padding-top:var(--vp-nav-height)}.VPContent.has-sidebar[data-v-a2887130]{margin:var(--vp-layout-top-height, 0px) 0 0;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPContent.has-sidebar[data-v-a2887130]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.VPFooter[data-v-acd94d69]{position:relative;z-index:var(--vp-z-index-footer);border-top:1px solid var(--vp-c-gutter);padding:32px 24px;background-color:var(--vp-c-bg)}.VPFooter.has-sidebar[data-v-acd94d69]{display:none}.VPFooter[data-v-acd94d69] a{text-decoration-line:underline;text-underline-offset:2px;transition:color .25s}.VPFooter[data-v-acd94d69] a:hover{color:var(--vp-c-text-1)}@media (min-width: 768px){.VPFooter[data-v-acd94d69]{padding:32px}}.container[data-v-acd94d69]{margin:0 auto;max-width:var(--vp-layout-max-width);text-align:center}.message[data-v-acd94d69],.copyright[data-v-acd94d69]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.VPLocalNavOutlineDropdown[data-v-1376b69e]{padding:12px 20px 11px}@media (min-width: 960px){.VPLocalNavOutlineDropdown[data-v-1376b69e]{padding:12px 36px 11px}}.VPLocalNavOutlineDropdown button[data-v-1376b69e]{display:block;font-size:12px;font-weight:500;line-height:24px;color:var(--vp-c-text-2);transition:color .5s;position:relative}.VPLocalNavOutlineDropdown button[data-v-1376b69e]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPLocalNavOutlineDropdown button.open[data-v-1376b69e]{color:var(--vp-c-text-1)}.icon[data-v-1376b69e]{display:inline-block;vertical-align:middle;margin-left:2px;font-size:14px;transform:rotate(0);transition:transform .25s}@media (min-width: 960px){.VPLocalNavOutlineDropdown button[data-v-1376b69e]{font-size:14px}.icon[data-v-1376b69e]{font-size:16px}}.open>.icon[data-v-1376b69e]{transform:rotate(90deg)}.items[data-v-1376b69e]{position:absolute;top:40px;right:16px;left:16px;display:grid;gap:1px;border:1px solid var(--vp-c-border);border-radius:8px;background-color:var(--vp-c-gutter);max-height:calc(var(--vp-vh, 100vh) - 86px);overflow:hidden auto;box-shadow:var(--vp-shadow-3)}@media (min-width: 960px){.items[data-v-1376b69e]{right:auto;left:calc(var(--vp-sidebar-width) + 32px);width:320px}}.header[data-v-1376b69e]{background-color:var(--vp-c-bg-soft)}.top-link[data-v-1376b69e]{display:block;padding:0 16px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.outline[data-v-1376b69e]{padding:8px 0;background-color:var(--vp-c-bg-soft)}.flyout-enter-active[data-v-1376b69e]{transition:all .2s ease-out}.flyout-leave-active[data-v-1376b69e]{transition:all .15s ease-in}.flyout-enter-from[data-v-1376b69e],.flyout-leave-to[data-v-1376b69e]{opacity:0;transform:translateY(-16px)}.VPLocalNav[data-v-c67e5e0d]{position:sticky;top:0;left:0;z-index:var(--vp-z-index-local-nav);border-bottom:1px solid var(--vp-c-gutter);padding-top:var(--vp-layout-top-height, 0px);width:100%;background-color:var(--vp-local-nav-bg-color)}.VPLocalNav.fixed[data-v-c67e5e0d]{position:fixed}@media (min-width: 960px){.VPLocalNav[data-v-c67e5e0d]{top:var(--vp-nav-height)}.VPLocalNav.has-sidebar[data-v-c67e5e0d]{padding-left:var(--vp-sidebar-width)}.VPLocalNav.empty[data-v-c67e5e0d]{display:none}}@media (min-width: 1280px){.VPLocalNav[data-v-c67e5e0d]{display:none}}@media (min-width: 1440px){.VPLocalNav.has-sidebar[data-v-c67e5e0d]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.container[data-v-c67e5e0d]{display:flex;justify-content:space-between;align-items:center}.menu[data-v-c67e5e0d]{display:flex;align-items:center;padding:12px 24px 11px;line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.menu[data-v-c67e5e0d]:hover{color:var(--vp-c-text-1);transition:color .25s}@media (min-width: 768px){.menu[data-v-c67e5e0d]{padding:0 32px}}@media (min-width: 960px){.menu[data-v-c67e5e0d]{display:none}}.menu-icon[data-v-c67e5e0d]{margin-right:8px;font-size:14px}.VPOutlineDropdown[data-v-c67e5e0d]{padding:12px 24px 11px}@media (min-width: 768px){.VPOutlineDropdown[data-v-c67e5e0d]{padding:12px 32px 11px}}.VPSwitch[data-v-336fbbfb]{position:relative;border-radius:11px;display:block;width:40px;height:22px;flex-shrink:0;border:1px solid var(--vp-input-border-color);background-color:var(--vp-input-switch-bg-color);transition:border-color .25s!important}.VPSwitch[data-v-336fbbfb]:hover{border-color:var(--vp-c-brand-1)}.check[data-v-336fbbfb]{position:absolute;top:1px;left:1px;width:18px;height:18px;border-radius:50%;background-color:var(--vp-c-neutral-inverse);box-shadow:var(--vp-shadow-1);transition:transform .25s!important}.icon[data-v-336fbbfb]{position:relative;display:block;width:18px;height:18px;border-radius:50%;overflow:hidden}.icon[data-v-336fbbfb] [class^=vpi-]{position:absolute;top:3px;left:3px;width:12px;height:12px;color:var(--vp-c-text-2)}.dark .icon[data-v-336fbbfb] [class^=vpi-]{color:var(--vp-c-text-1);transition:opacity .25s!important}.sun[data-v-b922f102]{opacity:1}.moon[data-v-b922f102],.dark .sun[data-v-b922f102]{opacity:0}.dark .moon[data-v-b922f102]{opacity:1}.dark .VPSwitchAppearance[data-v-b922f102] .check{transform:translate(18px)}.VPNavBarAppearance[data-v-f67a2989]{display:none}@media (min-width: 1280px){.VPNavBarAppearance[data-v-f67a2989]{display:flex;align-items:center}}.VPMenuGroup+.VPMenuLink[data-v-ed7f5076]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.link[data-v-ed7f5076]{display:block;border-radius:6px;padding:0 12px;line-height:32px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);white-space:nowrap;transition:background-color .25s,color .25s}.link[data-v-ed7f5076]:hover{color:var(--vp-c-brand-1);background-color:var(--vp-c-default-soft)}.link.active[data-v-ed7f5076]{color:var(--vp-c-brand-1)}.VPMenuGroup[data-v-01fa1225]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.VPMenuGroup[data-v-01fa1225]:first-child{margin-top:0;border-top:0;padding-top:0}.VPMenuGroup+.VPMenuGroup[data-v-01fa1225]{margin-top:12px;border-top:1px solid var(--vp-c-divider)}.title[data-v-01fa1225]{padding:0 12px;line-height:32px;font-size:14px;font-weight:600;color:var(--vp-c-text-2);white-space:nowrap;transition:color .25s}.VPMenu[data-v-b0b78a79]{border-radius:12px;padding:12px;min-width:128px;border:1px solid var(--vp-c-divider);background-color:var(--vp-c-bg-elv);box-shadow:var(--vp-shadow-3);transition:background-color .5s;max-height:calc(100vh - var(--vp-nav-height));overflow-y:auto}.VPMenu[data-v-b0b78a79] .group{margin:0 -12px;padding:0 12px 12px}.VPMenu[data-v-b0b78a79] .group+.group{border-top:1px solid var(--vp-c-divider);padding:11px 12px 12px}.VPMenu[data-v-b0b78a79] .group:last-child{padding-bottom:0}.VPMenu[data-v-b0b78a79] .group+.item{border-top:1px solid var(--vp-c-divider);padding:11px 16px 0}.VPMenu[data-v-b0b78a79] .item{padding:0 16px;white-space:nowrap}.VPMenu[data-v-b0b78a79] .label{flex-grow:1;line-height:28px;font-size:12px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.VPMenu[data-v-b0b78a79] .action{padding-left:24px}.VPFlyout[data-v-94a6bd0a]{position:relative}.VPFlyout[data-v-94a6bd0a]:hover{color:var(--vp-c-brand-1);transition:color .25s}.VPFlyout:hover .text[data-v-94a6bd0a]{color:var(--vp-c-text-2)}.VPFlyout:hover .icon[data-v-94a6bd0a]{fill:var(--vp-c-text-2)}.VPFlyout.active .text[data-v-94a6bd0a]{color:var(--vp-c-brand-1)}.VPFlyout.active:hover .text[data-v-94a6bd0a]{color:var(--vp-c-brand-2)}.button[aria-expanded=false]+.menu[data-v-94a6bd0a]{opacity:0;visibility:hidden;transform:translateY(0)}.VPFlyout:hover .menu[data-v-94a6bd0a],.button[aria-expanded=true]+.menu[data-v-94a6bd0a]{opacity:1;visibility:visible;transform:translateY(0)}.button[data-v-94a6bd0a]{display:flex;align-items:center;padding:0 12px;height:var(--vp-nav-height);color:var(--vp-c-text-1);transition:color .5s}.text[data-v-94a6bd0a]{display:flex;align-items:center;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.option-icon[data-v-94a6bd0a]{margin-right:0;font-size:16px}.text-icon[data-v-94a6bd0a]{margin-left:4px;font-size:14px}.icon[data-v-94a6bd0a]{font-size:20px;transition:fill .25s}.menu[data-v-94a6bd0a]{position:absolute;top:calc(var(--vp-nav-height) / 2 + 20px);right:0;opacity:0;visibility:hidden;transition:opacity .25s,visibility .25s,transform .25s}.VPSocialLink[data-v-4e10ce7b]{display:flex;justify-content:center;align-items:center;width:36px;height:36px;color:var(--vp-c-text-2);transition:color .5s}.VPSocialLink[data-v-4e10ce7b]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPSocialLink[data-v-4e10ce7b]>svg,.VPSocialLink[data-v-4e10ce7b]>[class^=vpi-social-]{width:20px;height:20px;fill:currentColor}.VPSocialLinks[data-v-d567ec5c]{display:flex;justify-content:center}.VPNavBarExtra[data-v-b837bb9a]{display:none;margin-right:-12px}@media (min-width: 768px){.VPNavBarExtra[data-v-b837bb9a]{display:block}}@media (min-width: 1280px){.VPNavBarExtra[data-v-b837bb9a]{display:none}}.trans-title[data-v-b837bb9a]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.item.appearance[data-v-b837bb9a],.item.social-links[data-v-b837bb9a]{display:flex;align-items:center;padding:0 12px}.item.appearance[data-v-b837bb9a]{min-width:176px}.appearance-action[data-v-b837bb9a]{margin-right:-2px}.social-links-list[data-v-b837bb9a]{margin:-4px -8px}.VPNavBarHamburger[data-v-3753352a]{display:flex;justify-content:center;align-items:center;width:48px;height:var(--vp-nav-height)}@media (min-width: 768px){.VPNavBarHamburger[data-v-3753352a]{display:none}}.container[data-v-3753352a]{position:relative;width:16px;height:14px;overflow:hidden}.VPNavBarHamburger:hover .top[data-v-3753352a]{top:0;left:0;transform:translate(4px)}.VPNavBarHamburger:hover .middle[data-v-3753352a]{top:6px;left:0;transform:translate(0)}.VPNavBarHamburger:hover .bottom[data-v-3753352a]{top:12px;left:0;transform:translate(8px)}.VPNavBarHamburger.active .top[data-v-3753352a]{top:6px;transform:translate(0) rotate(225deg)}.VPNavBarHamburger.active .middle[data-v-3753352a]{top:6px;transform:translate(16px)}.VPNavBarHamburger.active .bottom[data-v-3753352a]{top:6px;transform:translate(0) rotate(135deg)}.VPNavBarHamburger.active:hover .top[data-v-3753352a],.VPNavBarHamburger.active:hover .middle[data-v-3753352a],.VPNavBarHamburger.active:hover .bottom[data-v-3753352a]{background-color:var(--vp-c-text-2);transition:top .25s,background-color .25s,transform .25s}.top[data-v-3753352a],.middle[data-v-3753352a],.bottom[data-v-3753352a]{position:absolute;width:16px;height:2px;background-color:var(--vp-c-text-1);transition:top .25s,background-color .5s,transform .25s}.top[data-v-3753352a]{top:0;left:0;transform:translate(0)}.middle[data-v-3753352a]{top:6px;left:0;transform:translate(8px)}.bottom[data-v-3753352a]{top:12px;left:0;transform:translate(4px)}.VPNavBarMenuLink[data-v-2cd80f22]{display:flex;align-items:center;padding:0 12px;line-height:var(--vp-nav-height);font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.VPNavBarMenuLink.active[data-v-2cd80f22],.VPNavBarMenuLink[data-v-2cd80f22]:hover{color:var(--vp-c-brand-1)}.VPNavBarMenu[data-v-9a02246e]{display:none}@media (min-width: 768px){.VPNavBarMenu[data-v-9a02246e]{display:flex}}/*! @docsearch/css 3.8.0 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */:root{--docsearch-primary-color:#5468ff;--docsearch-text-color:#1c1e21;--docsearch-spacing:12px;--docsearch-icon-stroke-width:1.4;--docsearch-highlight-color:var(--docsearch-primary-color);--docsearch-muted-color:#969faf;--docsearch-container-background:rgba(101,108,133,.8);--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 hsla(0,0%,100%,.5),0 3px 8px 0 #555a64;--docsearch-searchbox-height:56px;--docsearch-searchbox-background:#ebedf0;--docsearch-searchbox-focus-background:#fff;--docsearch-searchbox-shadow:inset 0 0 0 2px var(--docsearch-primary-color);--docsearch-hit-height:56px;--docsearch-hit-color:#444950;--docsearch-hit-active-color:#fff;--docsearch-hit-background:#fff;--docsearch-hit-shadow:0 1px 3px 0 #d4d9e1;--docsearch-key-gradient:linear-gradient(-225deg,#d5dbe4,#f8f8f8);--docsearch-key-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px rgba(30,35,90,.4);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 1px 0 rgba(30,35,90,.4);--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 rgba(69,98,155,.12)}html[data-theme=dark]{--docsearch-text-color:#f5f6f7;--docsearch-container-background:rgba(9,10,17,.8);--docsearch-modal-background:#15172a;--docsearch-modal-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--docsearch-searchbox-background:#090a11;--docsearch-searchbox-focus-background:#000;--docsearch-hit-color:#bec3c9;--docsearch-hit-shadow:none;--docsearch-hit-background:#090a11;--docsearch-key-gradient:linear-gradient(-26.5deg,#565872,#31355b);--docsearch-key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba(3,4,9,.3);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 1px 1px 0 #0304094d;--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 rgba(73,76,106,.5),0 -4px 8px 0 rgba(0,0,0,.2);--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}.DocSearch-Button{align-items:center;background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;display:flex;font-weight:500;height:36px;justify-content:space-between;margin:0 0 0 16px;padding:0 8px;-webkit-user-select:none;user-select:none}.DocSearch-Button:active,.DocSearch-Button:focus,.DocSearch-Button:hover{background:var(--docsearch-searchbox-focus-background);box-shadow:var(--docsearch-searchbox-shadow);color:var(--docsearch-text-color);outline:none}.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 2px;position:relative;top:-1px;width:20px}.DocSearch-Button-Key--pressed{box-shadow:var(--docsearch-key-pressed-shadow);transform:translate3d(0,1px,0)}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder{display:none}}.DocSearch--active{overflow:hidden!important}.DocSearch-Container,.DocSearch-Container *{box-sizing:border-box}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Container a{text-decoration:none}.DocSearch-Link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;font:inherit;margin:0;padding:0}.DocSearch-Modal{background:var(--docsearch-modal-background);border-radius:6px;box-shadow:var(--docsearch-modal-shadow);flex-direction:column;margin:60px auto auto;max-width:var(--docsearch-modal-width);position:relative}.DocSearch-SearchBar{display:flex;padding:var(--docsearch-spacing) var(--docsearch-spacing) 0}.DocSearch-Form{align-items:center;background:var(--docsearch-searchbox-focus-background);border-radius:4px;box-shadow:var(--docsearch-searchbox-shadow);display:flex;height:var(--docsearch-searchbox-height);margin:0;padding:0 var(--docsearch-spacing);position:relative;width:100%}.DocSearch-Input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:var(--docsearch-text-color);flex:1;font:inherit;font-size:1.2em;height:100%;outline:none;padding:0 0 0 8px;width:80%}.DocSearch-Input::placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::-webkit-search-cancel-button,.DocSearch-Input::-webkit-search-decoration,.DocSearch-Input::-webkit-search-results-button,.DocSearch-Input::-webkit-search-results-decoration{display:none}.DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{margin:0;padding:0}.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator{display:none}.DocSearch-Container--Stalled .DocSearch-LoadingIndicator{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{animation:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0;stroke-width:var(--docsearch-icon-stroke-width)}}.DocSearch-Reset{animation:fade-in .1s ease-in forwards;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;padding:2px;right:0;stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.DocSearch-Cancel{display:none}.DocSearch-Dropdown{max-height:calc(var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height));min-height:var(--docsearch-spacing);overflow-y:auto;overflow-y:overlay;padding:0 var(--docsearch-spacing);scrollbar-color:var(--docsearch-muted-color) var(--docsearch-modal-background);scrollbar-width:thin}.DocSearch-Dropdown::-webkit-scrollbar{width:12px}.DocSearch-Dropdown::-webkit-scrollbar-track{background:transparent}.DocSearch-Dropdown::-webkit-scrollbar-thumb{background-color:var(--docsearch-muted-color);border:3px solid var(--docsearch-modal-background);border-radius:20px}.DocSearch-Dropdown ul{list-style:none;margin:0;padding:0}.DocSearch-Label{font-size:.75em;line-height:1.6em}.DocSearch-Help,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Help{font-size:.9em;margin:0;-webkit-user-select:none;user-select:none}.DocSearch-Title{font-size:1.2em}.DocSearch-Logo a{display:flex}.DocSearch-Logo svg{color:var(--docsearch-logo-color);margin-left:8px}.DocSearch-Hits:last-of-type{margin-bottom:24px}.DocSearch-Hits mark{background:none;color:var(--docsearch-highlight-color)}.DocSearch-HitsFooter{color:var(--docsearch-muted-color);display:flex;font-size:.85em;justify-content:center;margin-bottom:var(--docsearch-spacing);padding:var(--docsearch-spacing)}.DocSearch-HitsFooter a{border-bottom:1px solid;color:inherit}.DocSearch-Hit{border-radius:4px;display:flex;padding-bottom:4px;position:relative}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--deleting{transition:none}}.DocSearch-Hit--deleting{opacity:0;transition:all .25s linear}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--favoriting{transition:none}}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:all .25s linear;transition-delay:.25s}.DocSearch-Hit a{background:var(--docsearch-hit-background);border-radius:4px;box-shadow:var(--docsearch-hit-shadow);display:block;padding-left:var(--docsearch-spacing);width:100%}.DocSearch-Hit-source{background:var(--docsearch-modal-background);color:var(--docsearch-highlight-color);font-size:.85em;font-weight:600;line-height:32px;margin:0 -4px;padding:8px 4px 0;position:sticky;top:0;z-index:10}.DocSearch-Hit-Tree{color:var(--docsearch-muted-color);height:var(--docsearch-hit-height);opacity:.5;stroke-width:var(--docsearch-icon-stroke-width);width:24px}.DocSearch-Hit[aria-selected=true] a{background-color:var(--docsearch-highlight-color)}.DocSearch-Hit[aria-selected=true] mark{text-decoration:underline}.DocSearch-Hit-Container{align-items:center;color:var(--docsearch-hit-color);display:flex;flex-direction:row;height:var(--docsearch-hit-height);padding:0 var(--docsearch-spacing) 0 0}.DocSearch-Hit-icon{height:20px;width:20px}.DocSearch-Hit-action,.DocSearch-Hit-icon{color:var(--docsearch-muted-color);stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Hit-action{align-items:center;display:flex;height:22px;width:22px}.DocSearch-Hit-action svg{display:block;height:18px;width:18px}.DocSearch-Hit-action+.DocSearch-Hit-action{margin-left:6px}.DocSearch-Hit-action-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:inherit;cursor:pointer;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:background-color .1s ease-in}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{transition:none}}.DocSearch-Hit-action-button:focus path,.DocSearch-Hit-action-button:hover path{fill:#fff}.DocSearch-Hit-content-wrapper{display:flex;flex:1 1 auto;flex-direction:column;font-weight:500;justify-content:center;line-height:1.2em;margin:0 8px;overflow-x:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:80%}.DocSearch-Hit-title{font-size:.9em}.DocSearch-Hit-path{color:var(--docsearch-muted-color);font-size:.75em}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Tree,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-action,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-icon,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-text,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-title,.DocSearch-Hit[aria-selected=true] mark{color:var(--docsearch-hit-active-color)!important}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:none}}.DocSearch-ErrorScreen,.DocSearch-NoResults,.DocSearch-StartScreen{font-size:.9em;margin:0 auto;padding:36px 0;text-align:center;width:80%}.DocSearch-Screen-Icon{color:var(--docsearch-muted-color);padding-bottom:12px}.DocSearch-NoResults-Prefill-List{display:inline-block;padding-bottom:24px;text-align:left}.DocSearch-NoResults-Prefill-List ul{display:inline-block;padding:8px 0 0}.DocSearch-NoResults-Prefill-List li{list-style-position:inside;list-style-type:"» "}.DocSearch-Prefill{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:1em;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;font-size:1em;font-weight:700;padding:0}.DocSearch-Prefill:focus,.DocSearch-Prefill:hover{outline:none;text-decoration:underline}.DocSearch-Footer{align-items:center;background:var(--docsearch-footer-background);border-radius:0 0 8px 8px;box-shadow:var(--docsearch-footer-shadow);display:flex;flex-direction:row-reverse;flex-shrink:0;height:var(--docsearch-footer-height);justify-content:space-between;padding:0 var(--docsearch-spacing);position:relative;-webkit-user-select:none;user-select:none;width:100%;z-index:300}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li{align-items:center;display:flex}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:2px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;width:20px}.DocSearch-VisuallyHiddenForAccessibility{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}@media (max-width:768px){:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%}.DocSearch-Container{height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);position:absolute}.DocSearch-Footer{border-radius:0;bottom:0;position:absolute}.DocSearch-Hit-content-wrapper{display:flex;position:relative;width:80%}.DocSearch-Modal{border-radius:0;box-shadow:none;height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);margin:0;max-width:100%;width:100%}.DocSearch-Dropdown{max-height:calc(var(--docsearch-vh, 1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.DocSearch-Cancel{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;flex:none;font:inherit;font-size:1em;font-weight:500;margin-left:var(--docsearch-spacing);outline:none;overflow:hidden;padding:0;-webkit-user-select:none;user-select:none;white-space:nowrap}.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}[class*=DocSearch]{--docsearch-primary-color: var(--vp-c-brand-1);--docsearch-highlight-color: var(--docsearch-primary-color);--docsearch-text-color: var(--vp-c-text-1);--docsearch-muted-color: var(--vp-c-text-2);--docsearch-searchbox-shadow: none;--docsearch-searchbox-background: transparent;--docsearch-searchbox-focus-background: transparent;--docsearch-key-gradient: transparent;--docsearch-key-shadow: none;--docsearch-modal-background: var(--vp-c-bg-soft);--docsearch-footer-background: var(--vp-c-bg)}.dark [class*=DocSearch]{--docsearch-modal-shadow: none;--docsearch-footer-shadow: none;--docsearch-logo-color: var(--vp-c-text-2);--docsearch-hit-background: var(--vp-c-default-soft);--docsearch-hit-color: var(--vp-c-text-2);--docsearch-hit-shadow: none}.DocSearch-Button{display:flex;justify-content:center;align-items:center;margin:0;padding:0;width:48px;height:55px;background:transparent;transition:border-color .25s}.DocSearch-Button:hover{background:transparent}.DocSearch-Button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}.DocSearch-Button-Key--pressed{transform:none;box-shadow:none}.DocSearch-Button:focus:not(:focus-visible){outline:none!important}@media (min-width: 768px){.DocSearch-Button{justify-content:flex-start;border:1px solid transparent;border-radius:8px;padding:0 10px 0 12px;width:100%;height:40px;background-color:var(--vp-c-bg-alt)}.DocSearch-Button:hover{border-color:var(--vp-c-brand-1);background:var(--vp-c-bg-alt)}}.DocSearch-Button .DocSearch-Button-Container{display:flex;align-items:center}.DocSearch-Button .DocSearch-Search-Icon{position:relative;width:16px;height:16px;color:var(--vp-c-text-1);fill:currentColor;transition:color .5s}.DocSearch-Button:hover .DocSearch-Search-Icon{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Search-Icon{top:1px;margin-right:8px;width:14px;height:14px;color:var(--vp-c-text-2)}}.DocSearch-Button .DocSearch-Button-Placeholder{display:none;margin-top:2px;padding:0 16px 0 0;font-size:13px;font-weight:500;color:var(--vp-c-text-2);transition:color .5s}.DocSearch-Button:hover .DocSearch-Button-Placeholder{color:var(--vp-c-text-1)}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Placeholder{display:inline-block}}.DocSearch-Button .DocSearch-Button-Keys{direction:ltr;display:none;min-width:auto}@media (min-width: 768px){.DocSearch-Button .DocSearch-Button-Keys{display:flex;align-items:center}}.DocSearch-Button .DocSearch-Button-Key{display:block;margin:2px 0 0;border:1px solid var(--vp-c-divider);border-right:none;border-radius:4px 0 0 4px;padding-left:6px;min-width:0;width:auto;height:22px;line-height:22px;font-family:var(--vp-font-family-base);font-size:12px;font-weight:500;transition:color .5s,border-color .5s}.DocSearch-Button .DocSearch-Button-Key+.DocSearch-Button-Key{border-right:1px solid var(--vp-c-divider);border-left:none;border-radius:0 4px 4px 0;padding-left:2px;padding-right:6px}.DocSearch-Button .DocSearch-Button-Key:first-child{font-size:0!important}.DocSearch-Button .DocSearch-Button-Key:first-child:after{content:"Ctrl";font-size:12px;letter-spacing:normal;color:var(--docsearch-muted-color)}.mac .DocSearch-Button .DocSearch-Button-Key:first-child:after{content:"⌘"}.DocSearch-Button .DocSearch-Button-Key:first-child>*{display:none}.DocSearch-Search-Icon{--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' stroke-width='1.6' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' d='m14.386 14.386 4.088 4.088-4.088-4.088A7.533 7.533 0 1 1 3.733 3.733a7.533 7.533 0 0 1 10.653 10.653z'/%3E%3C/svg%3E")}.VPNavBarSearch{display:flex;align-items:center}@media (min-width: 768px){.VPNavBarSearch{flex-grow:1;padding-left:24px}}@media (min-width: 960px){.VPNavBarSearch{padding-left:32px}}.dark .DocSearch-Footer{border-top:1px solid var(--vp-c-divider)}.DocSearch-Form{border:1px solid var(--vp-c-brand-1);background-color:var(--vp-c-white)}.dark .DocSearch-Form{background-color:var(--vp-c-default-soft)}.DocSearch-Screen-Icon>svg{margin:auto}.VPNavBarSocialLinks[data-v-d381d9f6]{display:none}@media (min-width: 1280px){.VPNavBarSocialLinks[data-v-d381d9f6]{display:flex;align-items:center}}.title[data-v-c1a70765]{display:flex;align-items:center;border-bottom:1px solid transparent;width:100%;height:var(--vp-nav-height);font-size:16px;font-weight:600;color:var(--vp-c-text-1);transition:opacity .25s}@media (min-width: 960px){.title[data-v-c1a70765]{flex-shrink:0}.VPNavBarTitle.has-sidebar .title[data-v-c1a70765]{border-bottom-color:var(--vp-c-divider)}}[data-v-c1a70765] .logo{margin-right:8px;height:var(--vp-nav-logo-height)}.VPNavBarTranslations[data-v-7d8eba0e]{display:none}@media (min-width: 1280px){.VPNavBarTranslations[data-v-7d8eba0e]{display:flex;align-items:center}}.title[data-v-7d8eba0e]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.VPNavBar[data-v-a83c8f5d]{position:relative;height:var(--vp-nav-height);pointer-events:none;white-space:nowrap;transition:background-color .25s}.VPNavBar.screen-open[data-v-a83c8f5d]{transition:none;background-color:var(--vp-nav-bg-color);border-bottom:1px solid var(--vp-c-divider)}.VPNavBar[data-v-a83c8f5d]:not(.home){background-color:var(--vp-nav-bg-color)}@media (min-width: 960px){.VPNavBar[data-v-a83c8f5d]:not(.home){background-color:transparent}.VPNavBar[data-v-a83c8f5d]:not(.has-sidebar):not(.home.top){background-color:var(--vp-nav-bg-color)}}.wrapper[data-v-a83c8f5d]{padding:0 8px 0 24px}@media (min-width: 768px){.wrapper[data-v-a83c8f5d]{padding:0 32px}}@media (min-width: 960px){.VPNavBar.has-sidebar .wrapper[data-v-a83c8f5d]{padding:0}}.container[data-v-a83c8f5d]{display:flex;justify-content:space-between;margin:0 auto;max-width:calc(var(--vp-layout-max-width) - 64px);height:var(--vp-nav-height);pointer-events:none}.container>.title[data-v-a83c8f5d],.container>.content[data-v-a83c8f5d]{pointer-events:none}.container[data-v-a83c8f5d] *{pointer-events:auto}@media (min-width: 960px){.VPNavBar.has-sidebar .container[data-v-a83c8f5d]{max-width:100%}}.title[data-v-a83c8f5d]{flex-shrink:0;height:calc(var(--vp-nav-height) - 1px);transition:background-color .5s}@media (min-width: 960px){.VPNavBar.has-sidebar .title[data-v-a83c8f5d]{position:absolute;top:0;left:0;z-index:2;padding:0 32px;width:var(--vp-sidebar-width);height:var(--vp-nav-height);background-color:transparent}}@media (min-width: 1440px){.VPNavBar.has-sidebar .title[data-v-a83c8f5d]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}.content[data-v-a83c8f5d]{flex-grow:1}@media (min-width: 960px){.VPNavBar.has-sidebar .content[data-v-a83c8f5d]{position:relative;z-index:1;padding-right:32px;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .content[data-v-a83c8f5d]{padding-right:calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.content-body[data-v-a83c8f5d]{display:flex;justify-content:flex-end;align-items:center;height:var(--vp-nav-height);transition:background-color .5s}@media (min-width: 960px){.VPNavBar:not(.home.top) .content-body[data-v-a83c8f5d]{position:relative;background-color:var(--vp-nav-bg-color)}.VPNavBar:not(.has-sidebar):not(.home.top) .content-body[data-v-a83c8f5d]{background-color:transparent}}@media (max-width: 767px){.content-body[data-v-a83c8f5d]{column-gap:.5rem}}.menu+.translations[data-v-a83c8f5d]:before,.menu+.appearance[data-v-a83c8f5d]:before,.menu+.social-links[data-v-a83c8f5d]:before,.translations+.appearance[data-v-a83c8f5d]:before,.appearance+.social-links[data-v-a83c8f5d]:before{margin-right:8px;margin-left:8px;width:1px;height:24px;background-color:var(--vp-c-divider);content:""}.menu+.appearance[data-v-a83c8f5d]:before,.translations+.appearance[data-v-a83c8f5d]:before{margin-right:16px}.appearance+.social-links[data-v-a83c8f5d]:before{margin-left:16px}.social-links[data-v-a83c8f5d]{margin-right:-8px}.divider[data-v-a83c8f5d]{width:100%;height:1px}@media (min-width: 960px){.VPNavBar.has-sidebar .divider[data-v-a83c8f5d]{padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .divider[data-v-a83c8f5d]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.divider-line[data-v-a83c8f5d]{width:100%;height:1px;transition:background-color .5s}.VPNavBar:not(.home) .divider-line[data-v-a83c8f5d]{background-color:var(--vp-c-gutter)}@media (min-width: 960px){.VPNavBar:not(.home.top) .divider-line[data-v-a83c8f5d]{background-color:var(--vp-c-gutter)}.VPNavBar:not(.has-sidebar):not(.home.top) .divider[data-v-a83c8f5d]{background-color:var(--vp-c-gutter)}}.VPNavScreenAppearance[data-v-18596091]{display:flex;justify-content:space-between;align-items:center;border-radius:8px;padding:12px 14px 12px 16px;background-color:var(--vp-c-bg-soft)}.text[data-v-18596091]{line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.VPNavScreenMenuLink[data-v-b7b30aba]{display:block;border-bottom:1px solid var(--vp-c-divider);padding:12px 0 11px;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:border-color .25s,color .25s}.VPNavScreenMenuLink[data-v-b7b30aba]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupLink[data-v-36cdec30]{display:block;margin-left:12px;line-height:32px;font-size:14px;font-weight:400;color:var(--vp-c-text-1);transition:color .25s}.VPNavScreenMenuGroupLink[data-v-36cdec30]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupSection[data-v-95fd2ebe]{display:block}.title[data-v-95fd2ebe]{line-height:32px;font-size:13px;font-weight:700;color:var(--vp-c-text-2);transition:color .25s}.VPNavScreenMenuGroup[data-v-bbe0897e]{border-bottom:1px solid var(--vp-c-divider);height:48px;overflow:hidden;transition:border-color .5s}.VPNavScreenMenuGroup .items[data-v-bbe0897e]{visibility:hidden}.VPNavScreenMenuGroup.open .items[data-v-bbe0897e]{visibility:visible}.VPNavScreenMenuGroup.open[data-v-bbe0897e]{padding-bottom:10px;height:auto}.VPNavScreenMenuGroup.open .button[data-v-bbe0897e]{padding-bottom:6px;color:var(--vp-c-brand-1)}.VPNavScreenMenuGroup.open .button-icon[data-v-bbe0897e]{transform:rotate(45deg)}.button[data-v-bbe0897e]{display:flex;justify-content:space-between;align-items:center;padding:12px 4px 11px 0;width:100%;line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-1);transition:color .25s}.button[data-v-bbe0897e]:hover{color:var(--vp-c-brand-1)}.button-icon[data-v-bbe0897e]{transition:transform .25s}.group[data-v-bbe0897e]:first-child{padding-top:0}.group+.group[data-v-bbe0897e],.group+.item[data-v-bbe0897e]{padding-top:4px}.VPNavScreenTranslations[data-v-da5910eb]{height:24px;overflow:hidden}.VPNavScreenTranslations.open[data-v-da5910eb]{height:auto}.title[data-v-da5910eb]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-text-1)}.icon[data-v-da5910eb]{font-size:16px}.icon.lang[data-v-da5910eb]{margin-right:8px}.icon.chevron[data-v-da5910eb]{margin-left:4px}.list[data-v-da5910eb]{padding:4px 0 0 24px}.link[data-v-da5910eb]{line-height:32px;font-size:13px;color:var(--vp-c-text-1)}.VPNavScreen[data-v-42e2917f]{position:fixed;top:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px));right:0;bottom:0;left:0;padding:0 32px;width:100%;background-color:var(--vp-nav-screen-bg-color);overflow-y:auto;transition:background-color .25s;pointer-events:auto}.VPNavScreen.fade-enter-active[data-v-42e2917f],.VPNavScreen.fade-leave-active[data-v-42e2917f]{transition:opacity .25s}.VPNavScreen.fade-enter-active .container[data-v-42e2917f],.VPNavScreen.fade-leave-active .container[data-v-42e2917f]{transition:transform .25s ease}.VPNavScreen.fade-enter-from[data-v-42e2917f],.VPNavScreen.fade-leave-to[data-v-42e2917f]{opacity:0}.VPNavScreen.fade-enter-from .container[data-v-42e2917f],.VPNavScreen.fade-leave-to .container[data-v-42e2917f]{transform:translateY(-8px)}@media (min-width: 768px){.VPNavScreen[data-v-42e2917f]{display:none}}.container[data-v-42e2917f]{margin:0 auto;padding:24px 0 96px;max-width:288px}.menu+.translations[data-v-42e2917f],.menu+.appearance[data-v-42e2917f],.translations+.appearance[data-v-42e2917f]{margin-top:24px}.menu+.social-links[data-v-42e2917f]{margin-top:16px}.appearance+.social-links[data-v-42e2917f]{margin-top:16px}.VPNav[data-v-9ab70816]{position:relative;top:var(--vp-layout-top-height, 0px);left:0;z-index:var(--vp-z-index-nav);width:100%;pointer-events:none;transition:background-color .5s}@media (min-width: 960px){.VPNav[data-v-9ab70816]{position:fixed}}.VPSidebarItem.level-0[data-v-ef399d90]{padding-bottom:24px}.VPSidebarItem.collapsed.level-0[data-v-ef399d90]{padding-bottom:10px}.item[data-v-ef399d90]{position:relative;display:flex;width:100%}.VPSidebarItem.collapsible>.item[data-v-ef399d90]{cursor:pointer}.indicator[data-v-ef399d90]{position:absolute;top:6px;bottom:6px;left:-17px;width:2px;border-radius:2px;transition:background-color .25s}.VPSidebarItem.level-2.is-active>.item>.indicator[data-v-ef399d90],.VPSidebarItem.level-3.is-active>.item>.indicator[data-v-ef399d90],.VPSidebarItem.level-4.is-active>.item>.indicator[data-v-ef399d90],.VPSidebarItem.level-5.is-active>.item>.indicator[data-v-ef399d90]{background-color:var(--vp-c-brand-1)}.link[data-v-ef399d90]{display:flex;align-items:center;flex-grow:1}.text[data-v-ef399d90]{flex-grow:1;padding:4px 0;line-height:24px;font-size:14px;transition:color .25s}.VPSidebarItem.level-0 .text[data-v-ef399d90]{font-weight:700;color:var(--vp-c-text-1)}.VPSidebarItem.level-1 .text[data-v-ef399d90],.VPSidebarItem.level-2 .text[data-v-ef399d90],.VPSidebarItem.level-3 .text[data-v-ef399d90],.VPSidebarItem.level-4 .text[data-v-ef399d90],.VPSidebarItem.level-5 .text[data-v-ef399d90]{font-weight:500;color:var(--vp-c-text-2)}.VPSidebarItem.level-0.is-link>.item>.link:hover .text[data-v-ef399d90],.VPSidebarItem.level-1.is-link>.item>.link:hover .text[data-v-ef399d90],.VPSidebarItem.level-2.is-link>.item>.link:hover .text[data-v-ef399d90],.VPSidebarItem.level-3.is-link>.item>.link:hover .text[data-v-ef399d90],.VPSidebarItem.level-4.is-link>.item>.link:hover .text[data-v-ef399d90],.VPSidebarItem.level-5.is-link>.item>.link:hover .text[data-v-ef399d90]{color:var(--vp-c-brand-1)}.VPSidebarItem.level-0.has-active>.item>.text[data-v-ef399d90],.VPSidebarItem.level-1.has-active>.item>.text[data-v-ef399d90],.VPSidebarItem.level-2.has-active>.item>.text[data-v-ef399d90],.VPSidebarItem.level-3.has-active>.item>.text[data-v-ef399d90],.VPSidebarItem.level-4.has-active>.item>.text[data-v-ef399d90],.VPSidebarItem.level-5.has-active>.item>.text[data-v-ef399d90],.VPSidebarItem.level-0.has-active>.item>.link>.text[data-v-ef399d90],.VPSidebarItem.level-1.has-active>.item>.link>.text[data-v-ef399d90],.VPSidebarItem.level-2.has-active>.item>.link>.text[data-v-ef399d90],.VPSidebarItem.level-3.has-active>.item>.link>.text[data-v-ef399d90],.VPSidebarItem.level-4.has-active>.item>.link>.text[data-v-ef399d90],.VPSidebarItem.level-5.has-active>.item>.link>.text[data-v-ef399d90]{color:var(--vp-c-text-1)}.VPSidebarItem.level-0.is-active>.item .link>.text[data-v-ef399d90],.VPSidebarItem.level-1.is-active>.item .link>.text[data-v-ef399d90],.VPSidebarItem.level-2.is-active>.item .link>.text[data-v-ef399d90],.VPSidebarItem.level-3.is-active>.item .link>.text[data-v-ef399d90],.VPSidebarItem.level-4.is-active>.item .link>.text[data-v-ef399d90],.VPSidebarItem.level-5.is-active>.item .link>.text[data-v-ef399d90]{color:var(--vp-c-brand-1)}.caret[data-v-ef399d90]{display:flex;justify-content:center;align-items:center;margin-right:-7px;width:32px;height:32px;color:var(--vp-c-text-3);cursor:pointer;transition:color .25s;flex-shrink:0}.item:hover .caret[data-v-ef399d90]{color:var(--vp-c-text-2)}.item:hover .caret[data-v-ef399d90]:hover{color:var(--vp-c-text-1)}.caret-icon[data-v-ef399d90]{font-size:18px;transform:rotate(90deg);transition:transform .25s}.VPSidebarItem.collapsed .caret-icon[data-v-ef399d90]{transform:rotate(0)}.VPSidebarItem.level-1 .items[data-v-ef399d90],.VPSidebarItem.level-2 .items[data-v-ef399d90],.VPSidebarItem.level-3 .items[data-v-ef399d90],.VPSidebarItem.level-4 .items[data-v-ef399d90],.VPSidebarItem.level-5 .items[data-v-ef399d90]{border-left:1px solid var(--vp-c-divider);padding-left:16px}.VPSidebarItem.collapsed .items[data-v-ef399d90]{display:none}.no-transition[data-v-f2d94bdf] .caret-icon{transition:none}.group+.group[data-v-f2d94bdf]{border-top:1px solid var(--vp-c-divider);padding-top:10px}@media (min-width: 960px){.group[data-v-f2d94bdf]{padding-top:10px;width:calc(var(--vp-sidebar-width) - 64px)}}.VPSidebar[data-v-00f63243]{position:fixed;top:var(--vp-layout-top-height, 0px);bottom:0;left:0;z-index:var(--vp-z-index-sidebar);padding:32px 32px 96px;width:calc(100vw - 64px);max-width:320px;background-color:var(--vp-sidebar-bg-color);opacity:0;box-shadow:var(--vp-c-shadow-3);overflow-x:hidden;overflow-y:auto;transform:translate(-100%);transition:opacity .5s,transform .25s ease;overscroll-behavior:contain}.VPSidebar.open[data-v-00f63243]{opacity:1;visibility:visible;transform:translate(0);transition:opacity .25s,transform .5s cubic-bezier(.19,1,.22,1)}.dark .VPSidebar[data-v-00f63243]{box-shadow:var(--vp-shadow-1)}@media (min-width: 960px){.VPSidebar[data-v-00f63243]{padding-top:var(--vp-nav-height);width:var(--vp-sidebar-width);max-width:100%;background-color:var(--vp-sidebar-bg-color);opacity:1;visibility:visible;box-shadow:none;transform:translate(0)}}@media (min-width: 1440px){.VPSidebar[data-v-00f63243]{padding-left:max(32px,calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));width:calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px)}}@media (min-width: 960px){.curtain[data-v-00f63243]{position:sticky;top:-64px;left:0;z-index:1;margin-top:calc(var(--vp-nav-height) * -1);margin-right:-32px;margin-left:-32px;height:var(--vp-nav-height);background-color:var(--vp-sidebar-bg-color)}}.nav[data-v-00f63243]{outline:0}.VPSkipLink[data-v-bffb8ec0]{top:8px;left:8px;padding:8px 16px;z-index:999;border-radius:8px;font-size:12px;font-weight:700;text-decoration:none;color:var(--vp-c-brand-1);box-shadow:var(--vp-shadow-3);background-color:var(--vp-c-bg)}.VPSkipLink[data-v-bffb8ec0]:focus{height:auto;width:auto;clip:auto;clip-path:none}@media (min-width: 1280px){.VPSkipLink[data-v-bffb8ec0]{top:14px;left:16px}}.Layout[data-v-230dd14f]{display:flex;flex-direction:column;min-height:100vh}.VPHomeSponsors[data-v-045dab8b]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPHomeSponsors[data-v-045dab8b]{margin:96px 0}@media (min-width: 768px){.VPHomeSponsors[data-v-045dab8b]{margin:128px 0}}.VPHomeSponsors[data-v-045dab8b]{padding:0 24px}@media (min-width: 768px){.VPHomeSponsors[data-v-045dab8b]{padding:0 48px}}@media (min-width: 960px){.VPHomeSponsors[data-v-045dab8b]{padding:0 64px}}.container[data-v-045dab8b]{margin:0 auto;max-width:1152px}.love[data-v-045dab8b]{margin:0 auto;width:fit-content;font-size:28px;color:var(--vp-c-text-3)}.icon[data-v-045dab8b]{display:inline-block}.message[data-v-045dab8b]{margin:0 auto;padding-top:10px;max-width:320px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.sponsors[data-v-045dab8b]{padding-top:32px}.action[data-v-045dab8b]{padding-top:40px;text-align:center}.VPTeamPage[data-v-038dbcf8]{margin:96px 0}@media (min-width: 768px){.VPTeamPage[data-v-038dbcf8]{margin:128px 0}}.VPHome .VPTeamPageTitle[data-v-038dbcf8-s]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPTeamPageSection+.VPTeamPageSection[data-v-038dbcf8-s],.VPTeamMembers+.VPTeamPageSection[data-v-038dbcf8-s]{margin-top:64px}.VPTeamMembers+.VPTeamMembers[data-v-038dbcf8-s]{margin-top:24px}@media (min-width: 768px){.VPTeamPageTitle+.VPTeamPageSection[data-v-038dbcf8-s]{margin-top:16px}.VPTeamPageSection+.VPTeamPageSection[data-v-038dbcf8-s],.VPTeamMembers+.VPTeamPageSection[data-v-038dbcf8-s]{margin-top:96px}}.VPTeamMembers[data-v-038dbcf8-s]{padding:0 24px}@media (min-width: 768px){.VPTeamMembers[data-v-038dbcf8-s]{padding:0 48px}}@media (min-width: 960px){.VPTeamMembers[data-v-038dbcf8-s]{padding:0 64px}}.VPTeamPageTitle[data-v-da274d9c]{padding:48px 32px;text-align:center}@media (min-width: 768px){.VPTeamPageTitle[data-v-da274d9c]{padding:64px 48px 48px}}@media (min-width: 960px){.VPTeamPageTitle[data-v-da274d9c]{padding:80px 64px 48px}}.title[data-v-da274d9c]{letter-spacing:0;line-height:44px;font-size:36px;font-weight:500}@media (min-width: 768px){.title[data-v-da274d9c]{letter-spacing:-.5px;line-height:56px;font-size:48px}}.lead[data-v-da274d9c]{margin:0 auto;max-width:512px;padding-top:12px;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 768px){.lead[data-v-da274d9c]{max-width:592px;letter-spacing:.15px;line-height:28px;font-size:20px}}.VPTeamPageSection[data-v-491a5647]{padding:0 32px}@media (min-width: 768px){.VPTeamPageSection[data-v-491a5647]{padding:0 48px}}@media (min-width: 960px){.VPTeamPageSection[data-v-491a5647]{padding:0 64px}}.title[data-v-491a5647]{position:relative;margin:0 auto;max-width:1152px;text-align:center;color:var(--vp-c-text-2)}.title-line[data-v-491a5647]{position:absolute;top:16px;left:0;width:100%;height:1px;background-color:var(--vp-c-divider)}.title-text[data-v-491a5647]{position:relative;display:inline-block;padding:0 24px;letter-spacing:0;line-height:32px;font-size:20px;font-weight:500;background-color:var(--vp-c-bg)}.lead[data-v-491a5647]{margin:0 auto;max-width:480px;padding-top:12px;text-align:center;line-height:24px;font-size:16px;font-weight:500;color:var(--vp-c-text-2)}.members[data-v-491a5647]{padding-top:40px}.VPTeamMembersItem[data-v-e6e4ad42]{display:flex;flex-direction:column;gap:2px;border-radius:12px;width:100%;height:100%;overflow:hidden}.VPTeamMembersItem.small .profile[data-v-e6e4ad42]{padding:32px}.VPTeamMembersItem.small .data[data-v-e6e4ad42]{padding-top:20px}.VPTeamMembersItem.small .avatar[data-v-e6e4ad42]{width:64px;height:64px}.VPTeamMembersItem.small .name[data-v-e6e4ad42]{line-height:24px;font-size:16px}.VPTeamMembersItem.small .affiliation[data-v-e6e4ad42]{padding-top:4px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .desc[data-v-e6e4ad42]{padding-top:12px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .links[data-v-e6e4ad42]{margin:0 -16px -20px;padding:10px 0 0}.VPTeamMembersItem.medium .profile[data-v-e6e4ad42]{padding:48px 32px}.VPTeamMembersItem.medium .data[data-v-e6e4ad42]{padding-top:24px;text-align:center}.VPTeamMembersItem.medium .avatar[data-v-e6e4ad42]{width:96px;height:96px}.VPTeamMembersItem.medium .name[data-v-e6e4ad42]{letter-spacing:.15px;line-height:28px;font-size:20px}.VPTeamMembersItem.medium .affiliation[data-v-e6e4ad42]{padding-top:4px;font-size:16px}.VPTeamMembersItem.medium .desc[data-v-e6e4ad42]{padding-top:16px;max-width:288px;font-size:16px}.VPTeamMembersItem.medium .links[data-v-e6e4ad42]{margin:0 -16px -12px;padding:16px 12px 0}.profile[data-v-e6e4ad42]{flex-grow:1;background-color:var(--vp-c-bg-soft)}.data[data-v-e6e4ad42]{text-align:center}.avatar[data-v-e6e4ad42]{position:relative;flex-shrink:0;margin:0 auto;border-radius:50%;box-shadow:var(--vp-shadow-3)}.avatar-img[data-v-e6e4ad42]{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;object-fit:cover}.name[data-v-e6e4ad42]{margin:0;font-weight:600}.affiliation[data-v-e6e4ad42]{margin:0;font-weight:500;color:var(--vp-c-text-2)}.org.link[data-v-e6e4ad42]{color:var(--vp-c-text-2);transition:color .25s}.org.link[data-v-e6e4ad42]:hover{color:var(--vp-c-brand-1)}.desc[data-v-e6e4ad42]{margin:0 auto}.desc[data-v-e6e4ad42] a{font-weight:500;color:var(--vp-c-brand-1);text-decoration-style:dotted;transition:color .25s}.links[data-v-e6e4ad42]{display:flex;justify-content:center;height:56px}.sp-link[data-v-e6e4ad42]{display:flex;justify-content:center;align-items:center;text-align:center;padding:16px;font-size:14px;font-weight:500;color:var(--vp-c-sponsor);background-color:var(--vp-c-bg-soft);transition:color .25s,background-color .25s}.sp .sp-link.link[data-v-e6e4ad42]:hover,.sp .sp-link.link[data-v-e6e4ad42]:focus{outline:none;color:var(--vp-c-white);background-color:var(--vp-c-sponsor)}.sp-icon[data-v-e6e4ad42]{margin-right:8px;font-size:16px}.VPTeamMembers.small .container[data-v-c0b50e80]{grid-template-columns:repeat(auto-fit,minmax(224px,1fr))}.VPTeamMembers.small.count-1 .container[data-v-c0b50e80]{max-width:276px}.VPTeamMembers.small.count-2 .container[data-v-c0b50e80]{max-width:576px}.VPTeamMembers.small.count-3 .container[data-v-c0b50e80]{max-width:876px}.VPTeamMembers.medium .container[data-v-c0b50e80]{grid-template-columns:repeat(auto-fit,minmax(256px,1fr))}@media (min-width: 375px){.VPTeamMembers.medium .container[data-v-c0b50e80]{grid-template-columns:repeat(auto-fit,minmax(288px,1fr))}}.VPTeamMembers.medium.count-1 .container[data-v-c0b50e80]{max-width:368px}.VPTeamMembers.medium.count-2 .container[data-v-c0b50e80]{max-width:760px}.container[data-v-c0b50e80]{display:grid;gap:24px;margin:0 auto;max-width:1152px}:root{--deep-dark-blue: #4ca5ee;--deep-dark-blue-light: #40d1d6;--fresh-green: #46b983;--fresh-green-light: #28d4a3;--vivid-red: #f44336;--vivid-red-light: #e57373;--gradient-brand: linear-gradient(120deg, var(--deep-dark-blue-light) 0%, var(--fresh-green-light) 100%);--gradient-hero: linear-gradient(-45deg, var(--vivid-red) 30%, var(--deep-dark-blue) 70%);--vp-c-brand-1: var(--deep-dark-blue);--vp-c-brand-2: var(--deep-dark-blue-light);--vp-c-brand-3: var(--fresh-green);--vp-c-brand-soft: rgba(26, 35, 126, .14);--vp-c-tip-1: var(--fresh-green);--vp-c-tip-2: var(--fresh-green-light);--vp-c-tip-3: var(--deep-dark-blue);--vp-c-tip-soft: rgba(76, 175, 80, .14);--vp-c-warning-1: #ff9800;--vp-c-warning-2: #ffa726;--vp-c-warning-3: #ffb74d;--vp-c-warning-soft: rgba(255, 152, 0, .14);--vp-c-danger-1: var(--vivid-red);--vp-c-danger-2: var(--vivid-red-light);--vp-c-danger-3: #ef9a9a;--vp-c-danger-soft: rgba(244, 67, 54, .14)}:root{--vp-button-brand-border: transparent;--vp-button-brand-text: var(--vp-c-white);--vp-button-brand-bg: var(--deep-dark-blue);--vp-button-brand-hover-border: transparent;--vp-button-brand-hover-text: var(--vp-c-white);--vp-button-brand-hover-bg: var(--deep-dark-blue-light);--vp-button-brand-active-border: transparent;--vp-button-brand-active-text: var(--vp-c-white);--vp-button-brand-active-bg: var(--fresh-green)}@media (min-width: 640px){:root{--vp-home-hero-image-filter: blur(56px)}}@media (min-width: 960px){:root{--vp-home-hero-image-filter: blur(72px)}}:root{--vp-custom-block-tip-border: transparent;--vp-custom-block-tip-text: var(--vp-c-text-1);--vp-custom-block-tip-bg: var(--vp-c-brand-soft);--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft)}.DocSearch{--docsearch-primary-color: var(--deep-dark-blue) !important}.vp-doc a:hover{background:var(--gradient-brand);-webkit-background-clip:text;-webkit-text-fill-color:transparent;text-decoration:none;transition:all .3s ease}.vp-doc div[class*=language-]{border-radius:8px;box-shadow:0 4px 6px #0000001a}.vp-doc h1,.vp-doc h2,.vp-doc h3,.vp-doc h4{background:var(--gradient-brand);-webkit-background-clip:text;-webkit-text-fill-color:transparent}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--deep-dark-blue-light);border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--deep-dark-blue)}.nav-bar .nav-links .nav-item.active a{background:var(--gradient-brand);-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-weight:600}.sidebar{border-right:1px solid rgba(60,60,60,.12);background:#ffffffe6;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.vp-doc .custom-block{border-radius:8px;box-shadow:0 2px 4px #0000000d;transition:transform .2s ease,box-shadow .2s ease}.vp-doc .custom-block:hover{transform:translateY(-2px);box-shadow:0 4px 8px #0000001a}.custom-home[data-v-58648596]{padding-top:4rem;padding-bottom:4rem;width:1420px;max-width:80%;margin:0 auto}.hero[data-v-58648596]{display:flex;align-items:center;justify-content:space-between;padding:0 2rem 0 6rem;position:relative;overflow:hidden}.hero-content[data-v-58648596]{flex:1;max-width:600px;position:relative;z-index:2}.hero-title[data-v-58648596]{font-size:4rem;line-height:1.2;margin-bottom:1.5rem;font-weight:800}.gradient-text[data-v-58648596]{background:linear-gradient(120deg,var(--deep-dark-blue),var(--vivid-red));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}.hero-subtitle[data-v-58648596]{font-size:1.5rem;color:var(--vp-c-text-1);margin-bottom:1rem;line-height:1.6}.hero-tagline[data-v-58648596]{font-size:1.1rem;color:var(--vp-c-text-2);margin-bottom:2rem}.hero-buttons[data-v-58648596]{display:flex;gap:1rem}.button[data-v-58648596]{padding:.8rem 1.6rem;border-radius:8px;font-weight:600;transition:all .3s ease;text-decoration:none}.button.primary[data-v-58648596]{background:var(--deep-dark-blue);color:#fff;box-shadow:0 4px 14px #1a237e63}.button.primary[data-v-58648596]:hover{transform:translateY(-2px);box-shadow:0 6px 20px #1a237e3b}.button.secondary[data-v-58648596]{background:#e100001a;color:var(--vivid-red);border:2px solid var(--vivid-red)}.button.secondary[data-v-58648596]:hover{background:#e100003b;transform:translateY(-2px)}.hero-image[data-v-58648596]{flex:1;position:relative;height:100%;max-width:50%}@media screen and (min-width: 768px){.image-container[data-v-58648596]{position:relative;z-index:2;animation:float-58648596 6s ease-in-out infinite;margin:auto;max-width:280px}.image-container img[data-v-58648596]{max-width:100%;height:auto;border-radius:12px}.floating-elements[data-v-58648596]{position:absolute;top:0;left:0;right:0;bottom:0;z-index:1}.element[data-v-58648596]{position:absolute;border-radius:50%;opacity:.6;filter:blur(40px)}.element-1[data-v-58648596]{background:var(--deep-dark-blue);width:200px;height:200px;top:20%;left:10%;animation:float-58648596 8s ease-in-out infinite}.element-2[data-v-58648596]{background:var(--fresh-green);width:150px;height:150px;bottom:30%;right:20%;animation:float-58648596 6s ease-in-out infinite}.element-3[data-v-58648596]{background:var(--vivid-red-light);width:100px;height:100px;top:60%;left:30%;animation:float-58648596 7s ease-in-out infinite}}.features[data-v-58648596]{padding:3rem 1rem}.features-title[data-v-58648596]{text-align:center;font-size:2.5rem;margin-bottom:4rem}.features-grid[data-v-58648596]{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:2rem;max-width:1200px;margin:0 auto}.feature-card[data-v-58648596]{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);padding:2rem;border-radius:12px;box-shadow:0 4px 6px #0000000d;transition:all .3s ease}.feature-card[data-v-58648596]:hover{transform:translateY(-5px);box-shadow:0 8px 15px #0000001a}.feature-icon[data-v-58648596]{width:60px;height:60px;border-radius:12px;font-size:2rem;background:var(--gradient-brand);display:flex;align-items:center;justify-content:center;margin-bottom:1.5rem}.feature-icon i[data-v-58648596]{font-size:24px;color:#fff}@keyframes float-58648596{0%,to{transform:translateY(0)}50%{transform:translateY(-20px)}}@media (max-width: 960px){.hero[data-v-58648596]{flex-direction:column;text-align:center;padding-top:2rem}.hero-content[data-v-58648596]{max-width:100%;margin-bottom:3rem}.hero-buttons[data-v-58648596]{justify-content:center}.hero-image[data-v-58648596]{max-width:80%}.features-grid[data-v-58648596]{grid-template-columns:1fr}}@media (max-width: 640px){.hero-title[data-v-58648596]{font-size:3rem}.hero-subtitle[data-v-58648596]{font-size:1.25rem}.button[data-v-58648596]{padding:.6rem 1.2rem}}.carousel-section[data-v-58648596]{padding:2rem 2rem 0;margin:0 auto}.carousel-container[data-v-58648596]{position:relative;width:100%;max-width:800px;margin:0 auto;overflow:hidden;border-radius:12px;box-shadow:0 4px 6px #0000001a}.carousel-track[data-v-58648596]{display:flex;transition:transform .5s ease-in-out}.carousel-slide[data-v-58648596]{min-width:100%;height:400px}.carousel-slide img[data-v-58648596]{width:100%;height:100%;object-fit:cover}.carousel-button[data-v-58648596]{position:absolute;top:50%;transform:translateY(-50%);background:#00000080;color:#fff;border:none;width:40px;height:40px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:1.5rem;transition:all .3s ease;z-index:2}.carousel-button[data-v-58648596]:hover:not(:disabled){background:#000c}.carousel-button[data-v-58648596]:disabled{opacity:.5;cursor:not-allowed}.carousel-button.prev[data-v-58648596]{left:1rem}.carousel-button.next[data-v-58648596]{right:1rem}.carousel-dots[data-v-58648596]{position:absolute;bottom:1rem;left:50%;transform:translate(-50%);display:flex;gap:.5rem;z-index:2}.carousel-dot[data-v-58648596]{width:10px;height:10px;border-radius:50%;border:none;background:#ffffff80;cursor:pointer;transition:all .3s ease;padding:0}.carousel-dot.active[data-v-58648596]{background:#fff;transform:scale(1.2)}@media (max-width: 768px){.carousel-slide[data-v-58648596]{height:300px}.carousel-button[data-v-58648596]{width:30px;height:30px;font-size:1rem}}
diff --git a/basic/config.html b/basic/config.html
new file mode 100644
index 00000000..e312df8f
--- /dev/null
+++ b/basic/config.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+ 配置详解 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/basic/index.html b/basic/index.html
new file mode 100644
index 00000000..5ddbf46d
--- /dev/null
+++ b/basic/index.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+ 简介 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/basic/modules.html b/basic/modules.html
new file mode 100644
index 00000000..85fab2eb
--- /dev/null
+++ b/basic/modules.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+ 模块安装 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/basic/start.html b/basic/start.html
new file mode 100644
index 00000000..545e89c8
--- /dev/null
+++ b/basic/start.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+ 快速开始 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/basic/usage.html b/basic/usage.html
new file mode 100644
index 00000000..c2e175da
--- /dev/null
+++ b/basic/usage.html
@@ -0,0 +1,72 @@
+
+
+
+
+
+ 立即使用 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/data.json b/data.json
new file mode 100644
index 00000000..3873aae1
--- /dev/null
+++ b/data.json
@@ -0,0 +1,167 @@
+{
+ "meta": {
+ "registry": "https://registry.npmjs.org/"
+ },
+ "list": [
+ {
+ "name": "@kotori-bot/adapter-discord",
+ "description": "Discord 适配器"
+ },
+ {
+ "name": "@kotori-bot/adapter-mail",
+ "description": "电子邮箱适配器"
+ },
+ {
+ "name": "@kotori-bot/adapter-slack",
+ "description": "Slack 适配器"
+ },
+ {
+ "name": "@kotori-bot/adapter-telegram",
+ "description": "Telegram 适配器"
+ },
+ {
+ "name": "@kotori-bot/adapter-onebot",
+ "description": "基于 OneBot11 标准的适配器"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-access",
+ "description": "权限插件,用于设置多个机器人管理员"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-adapter-cmd",
+ "description": "基于控制台的适配器"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-adapter-onebot",
+ "description": "基于 OneBot11 标准的适配器"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-adapter-qq",
+ "description": "基于 Tencent 官方 API 的适配器"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-adapter-sandbox",
+ "description": "模块测试环境,虚拟沙盒适配器"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-alias",
+ "description": "用户级别名设置"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-core",
+ "description": "核心插件"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-filter",
+ "description": "模块加载过滤器"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-helper",
+ "description": "查看指令帮助"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-status",
+ "description": "查看服务器运行状态"
+ },
+ {
+ "name": "@kotori-bot/kotori-plugin-webui",
+ "description": "网页控制台"
+ },
+ {
+ "name": "kotori-plugin-adapter-minecraft",
+ "description": "Minecraft 基岩版原生 WebSocket 适配器"
+ },
+ {
+ "name": "kotori-plugin-bangumi",
+ "description": "番组计划插件"
+ },
+ {
+ "name": "kotori-plugin-bangumi",
+ "description": "番组计划插件"
+ },
+ {
+ "name": "kotori-plugin-better-join",
+ "description": "趣味的进群欢迎提示"
+ },
+ {
+ "name": "kotori-plugin-bilibili",
+ "description": "哔哩哔哩插件"
+ },
+ {
+ "name": "kotori-plugin-color",
+ "description": "随机颜色图片、日本传统色、中国传统色"
+ },
+ {
+ "name": "kotori-plugin-drift-bottle",
+ "description": "漂流瓶插件"
+ },
+ {
+ "name": "kotori-plugin-github",
+ "description": "GitHub 仓库搜索"
+ },
+ {
+ "name": "kotori-plugin-goodnight",
+ "description": "早晚安插件"
+ },
+ {
+ "name": "kotori-plugin-grouper",
+ "description": "群聊活跃排行与小游戏"
+ },
+ {
+ "name": "kotori-plugin-hitokotos",
+ "description": "随机一言,十七种不同语录"
+ },
+ {
+ "name": "kotori-plugin-manger",
+ "description": "简易群管插件"
+ },
+ {
+ "name": "kotori-plugin-mediawiki",
+ "description": "维基搜索,支持自定义多个维基"
+ },
+ {
+ "name": "kotori-plugin-minecraft",
+ "description": "Minecraft JE/BE 服务器,账号皮肤,最新版本查询"
+ },
+ {
+ "name": "kotori-plugin-music",
+ "description": "网易云音乐搜索"
+ },
+ {
+ "name": "kotori-plugin-nmsl",
+ "description": "孙笑川在线帮你翻译成抽象话"
+ },
+ {
+ "name": "kotori-plugin-penis",
+ "description": "查看今日牛牛的长度和排行"
+ },
+ {
+ "name": "kotori-plugin-rand",
+ "description": "数学计算、随机操作、投骰子、扔硬币!"
+ },
+ {
+ "name": "kotori-plugin-randomimg",
+ "description": "Pixiv 等多种随机图片"
+ },
+ {
+ "name": "kotori-plugin-requester",
+ "description": "给主人报告的插件"
+ },
+ {
+ "name": "kotori-plugin-run-code",
+ "description": "在线运行 JavaScript 和 Lua 代码"
+ },
+ {
+ "name": "kotori-plugin-sed",
+ "description": "社工查询插件"
+ },
+ {
+ "name": "kotori-plugin-testing",
+ "description": "测试用的插件,提供了 eval 和 echo 指令"
+ },
+ {
+ "name": "kotori-plugin-weather",
+ "description": "天气查询"
+ }
+ ]
+}
diff --git a/favicon.svg b/favicon.svg
new file mode 100644
index 00000000..02f64fa5
--- /dev/null
+++ b/favicon.svg
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/fluoro.png b/fluoro.png
new file mode 100644
index 00000000..698e851b
Binary files /dev/null and b/fluoro.png differ
diff --git a/guide/base/command.html b/guide/base/command.html
new file mode 100644
index 00000000..dd45a929
--- /dev/null
+++ b/guide/base/command.html
@@ -0,0 +1,231 @@
+
+
+
+
+
+ 指令注册 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/base/events.html b/guide/base/events.html
new file mode 100644
index 00000000..ee06fa40
--- /dev/null
+++ b/guide/base/events.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+ 事件系统 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/base/middleware.html b/guide/base/middleware.html
new file mode 100644
index 00000000..bd782a43
--- /dev/null
+++ b/guide/base/middleware.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+ 中间件 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/base/regexp.html b/guide/base/regexp.html
new file mode 100644
index 00000000..b5c03249
--- /dev/null
+++ b/guide/base/regexp.html
@@ -0,0 +1,75 @@
+
+
+
+
+
+ 正则匹配 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/components/adapter.html b/guide/components/adapter.html
new file mode 100644
index 00000000..71011f1d
--- /dev/null
+++ b/guide/components/adapter.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ 实现适配器类 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/components/api.html b/guide/components/api.html
new file mode 100644
index 00000000..62a45808
--- /dev/null
+++ b/guide/components/api.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ 实现接口类 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/components/custom.html b/guide/components/custom.html
new file mode 100644
index 00000000..ef3c8b38
--- /dev/null
+++ b/guide/components/custom.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ 自定义服务 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/components/elements.html b/guide/components/elements.html
new file mode 100644
index 00000000..ee960097
--- /dev/null
+++ b/guide/components/elements.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ 实现元素类 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/extend/tools.html b/guide/extend/tools.html
new file mode 100644
index 00000000..899ea798
--- /dev/null
+++ b/guide/extend/tools.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ 工具函数 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/index.html b/guide/index.html
new file mode 100644
index 00000000..b2158896
--- /dev/null
+++ b/guide/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ 前言 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/modules/context.html b/guide/modules/context.html
new file mode 100644
index 00000000..10f4ddb5
--- /dev/null
+++ b/guide/modules/context.html
@@ -0,0 +1,227 @@
+
+
+
+
+
+ 上下文 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/modules/decorator.html b/guide/modules/decorator.html
new file mode 100644
index 00000000..134e7af4
--- /dev/null
+++ b/guide/modules/decorator.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ 装饰器 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/modules/filter.html b/guide/modules/filter.html
new file mode 100644
index 00000000..9b19202a
--- /dev/null
+++ b/guide/modules/filter.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ 滤器 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/modules/i18n.html b/guide/modules/i18n.html
new file mode 100644
index 00000000..a0fa477a
--- /dev/null
+++ b/guide/modules/i18n.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ 国际化 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/modules/plugin.html b/guide/modules/plugin.html
new file mode 100644
index 00000000..c1b317f9
--- /dev/null
+++ b/guide/modules/plugin.html
@@ -0,0 +1,203 @@
+
+
+
+
+
+ 模块与插件 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/modules/rescript.html b/guide/modules/rescript.html
new file mode 100644
index 00000000..a8ff8f98
--- /dev/null
+++ b/guide/modules/rescript.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ 使用 ReScript 开发 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/modules/schema.html b/guide/modules/schema.html
new file mode 100644
index 00000000..8badcfb2
--- /dev/null
+++ b/guide/modules/schema.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+ 配置检测 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/modules/service.html b/guide/modules/service.html
new file mode 100644
index 00000000..b8010f75
--- /dev/null
+++ b/guide/modules/service.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ 依赖与服务 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/start/environment.html b/guide/start/environment.html
new file mode 100644
index 00000000..a2f97974
--- /dev/null
+++ b/guide/start/environment.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ 环境搭建 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/start/publish.html b/guide/start/publish.html
new file mode 100644
index 00000000..a6eac71e
--- /dev/null
+++ b/guide/start/publish.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+ 模块发布 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/guide/start/setup.html b/guide/start/setup.html
new file mode 100644
index 00000000..dda18dca
--- /dev/null
+++ b/guide/start/setup.html
@@ -0,0 +1,171 @@
+
+
+
+
+
+ 项目构建 | Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hashmap.json b/hashmap.json
new file mode 100644
index 00000000..48656819
--- /dev/null
+++ b/hashmap.json
@@ -0,0 +1 @@
+{"advanced_architecture.md":"ESjdj-B2","advanced_browser.md":"BtfITGHb","advanced_contributing.md":"20whP1zM","advanced_develop.md":"CA0vNCVO","advanced_history.md":"C7WGhqv4","advanced_index.md":"DSFMyyrZ","advanced_testing.md":"Cnt7EvXQ","advanced_thanks.md":"qE2Dsx_W","api_index.md":"DIh_xnM0","basic_config.md":"DjQQ98nJ","basic_index.md":"DDCEHizn","basic_modules.md":"DhYW5BJ1","basic_start.md":"WiJIbtoH","basic_usage.md":"BoyQM9Wi","guide_base_command.md":"B8T0uA6j","guide_base_events.md":"BNxI8SOa","guide_base_middleware.md":"BSX_ratG","guide_base_regexp.md":"Bh1AF6U4","guide_components_adapter.md":"CuUX_HEj","guide_components_api.md":"BnaAxIB8","guide_components_custom.md":"DujlTsCC","guide_components_elements.md":"DDywsrdv","guide_extend_tools.md":"BELaqOF4","guide_index.md":"CbUYeeBD","guide_modules_context.md":"HAyvRJ52","guide_modules_decorator.md":"C_mpKbTA","guide_modules_filter.md":"BIGnsrTf","guide_modules_i18n.md":"BETTBqEz","guide_modules_plugin.md":"BW9Me5-D","guide_modules_rescript.md":"zh3sC6a0","guide_modules_schema.md":"B5KeilZE","guide_modules_service.md":"DxB9k1Hz","guide_start_environment.md":"DbqmpXqr","guide_start_publish.md":"XXQZNH0i","guide_start_setup.md":"DUpgviI7","index.md":"Dr7K_nS5","modules_index.md":"A8sDrAic"}
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..fde3778c
--- /dev/null
+++ b/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Skip to content 小鳥 · KotoriBot
基于 Node.js + TypeScript 的跨平台聊天机器人框架
🚀
跨平台 得益于模块化支持,通过编写各种模块实现不同的功能与聊天平台接入
🧩
解耦合 基于控制反转与面向切面编程思想,减少代码冗余与复杂度
🛠️
现代化 使用现代化的 ECMAScript 语法规范与强大的 TypeScript 类型支持
+
+
+
+
\ No newline at end of file
diff --git a/logo.png b/logo.png
new file mode 100644
index 00000000..2bc3c132
Binary files /dev/null and b/logo.png differ
diff --git a/modules/index.html b/modules/index.html
new file mode 100644
index 00000000..0863fa23
--- /dev/null
+++ b/modules/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ Kotori
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/schema/1.3.0.json b/schema/1.3.0.json
new file mode 100644
index 00000000..fd5bf8ee
--- /dev/null
+++ b/schema/1.3.0.json
@@ -0,0 +1,74 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://kotori.js.org/schema/schema-1.3.0.json",
+ "title": "Kotori",
+ "description": "Config file of kotori",
+ "type": "object",
+ "properties": {
+ "global": {
+ "type": "object",
+ "properties": {
+ "port": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65525
+ },
+ "lang": {
+ "enum": ["en_US", "ja_JP", "zh_TW", "zh_CN"]
+ },
+ "commandPrefix": {
+ "type": "string"
+ },
+ "dirs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "adapter": {
+ "type": "object",
+ "patternProperties": {
+ ".": {
+ "type": "object",
+ "properties": {
+ "extends": {
+ "type": "string"
+ },
+ "master": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "lang": {
+ "enum": ["en_US", "ja_JP", "zh_TW", "zh_CN"]
+ },
+ "commandPrefix": {
+ "type": "string"
+ }
+ },
+ "required": ["extends", "master"]
+ }
+ }
+ },
+ "plugin": {
+ "type": "object",
+ "patternProperties": {
+ ".": {
+ "type": "object",
+ "properties": {
+ "filter": {
+ "type": "object"
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/vp-icons.css b/vp-icons.css
new file mode 100644
index 00000000..ddc5bd8e
--- /dev/null
+++ b/vp-icons.css
@@ -0,0 +1 @@
+.vpi-social-github{--icon:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")}
\ No newline at end of file