diff --git a/404.html b/404.html new file mode 100644 index 00000000..75ecb418 --- /dev/null +++ b/404.html @@ -0,0 +1,25 @@ + + + + + + 404 | Kotori + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..b433c451 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +https://kotori.js.org \ No newline at end of file diff --git a/advanced/architecture.html b/advanced/architecture.html new file mode 100644 index 00000000..59ed3665 --- /dev/null +++ b/advanced/architecture.html @@ -0,0 +1,29 @@ + + + + + + Architecture | Kotori + + + + + + + + + + + + + + + + + + +
Skip to content

Architecture

Kotori use workspace developing (monorepo) by pnpm and submodules management by git, and linter and formatter by Biome (fucks eslint and prettier).

Packages

fluoro (submodule)

fluoro

Fluoro is universal meta-framework,provides Context Modules and Events. it is the base of whole Kotori,more details view Fluoro.

@kotori-bot/core

@kotori-bot/core

It is the core of Kotori and provides many important features of kotori (encapsulates fluoro).It has fully the support for browser and other not node.js (such as quick.js or service-worker),more details view Use in browser.

@kotori-bot/loader

@kotori-bot/loader

It encapsulates @kotori-bot/loader and provides some interface and features need server environments (Node.js).

@kotori-bot/logger

@kotori-bot/logger

It is fully functional logger base on adapters thinking and supports browser.

@kotori-bot/i18n

@kotori-bot/i18n

It is a i18n module,provides i18n for @kotori-bot/core.

@kotori-bot/tools

@kotori-bot/tools

It is a tools module,provides some useful tools for @kotori-bot/core and @kotori-bot/loader.

tsukiko

tsukiko

It used to parse and check data type at running time.

Process image

architecture

+ + + + \ No newline at end of file diff --git a/advanced/browser.html b/advanced/browser.html new file mode 100644 index 00000000..5a8831d9 --- /dev/null +++ b/advanced/browser.html @@ -0,0 +1,95 @@ + + + + + + Use in browser | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

Use in browser

To wait better supports...

bash
pnpm install @kotori-bot/core

The @kotori-bot/core package is the core package of the bot,it only used ecmascript standard api, so you can use it in any the environments which support ecmascript >= 2020.

typescript
import { Adapter, Api, Core, Elements, type Message, Messages, MessageScope } from '@kotori-bot/core'
+
+const core = new Core({
+  global: {
+    commandPrefix: '/'
+  }
+})
+
+function MyPlugin(ctx: Core) {
+  ctx.command('echo <msg>').action(({ args: [msg] }, session) => {
+    alert(`You said: ${msg}`)
+    console.log(session)
+  })
+}
+
+core.load(MyPlugin)
+
+core.start()
+
+class BrowserAdapter extends Adapter {
+  public platform = 'browser'
+
+  public constructor(ctx: Core) {
+    super(ctx, { commandPrefix: '/', extends: 'browser', master: '1', lang: 'zh_CN' }, 'browser')
+  }
+
+  public api = new (class extends Api {
+    public getSupportedEvents(): ReturnType<Api['getSupportedEvents']> {
+      return ['on_message']
+    }
+  })(this) as Api
+
+  public elements = new (class extends Elements {
+    getSupportsElements(): ReturnType<Elements['getSupportsElements']> {
+      return []
+    }
+
+    decode(message: Message): string {
+      return message.toString()
+    }
+
+    encode(raw: string): Message {
+      return Messages(raw)
+    }
+  })(this) as Elements
+
+  public handle = this.session.bind(this)
+
+  public start() {}
+  public stop() {}
+  public send() {}
+}
+
+const bot = new BrowserAdapter(core)
+
+const result = prompt('input:')
+
+bot.handle('on_message', {
+  type: MessageScope.PRIVATE,
+  message: result ?? '',
+  messageAlt: 'alt',
+  messageId: '1',
+  time: Date.now(),
+  userId: '1',
+  sender: {
+    nickname: 'my-browser'
+  }
+})
+ + + + \ No newline at end of file diff --git a/advanced/contributing.html b/advanced/contributing.html new file mode 100644 index 00000000..27e0882b --- /dev/null +++ b/advanced/contributing.html @@ -0,0 +1,28 @@ + + + + + + Contributing | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

Contributing

TIP

Here need to improved.

This project is open source and we welcome your contributions!

Steps

  1. Fork it!

  2. Create your feature branch: git checkout -b my-new-feature

  3. Commit your changes: git commit -am 'Add some feature'

  4. Push to the branch: git push origin my-new-feature

  5. Submit a pull request 😄

More details please refer to CONTRIBUTING.md

Docs standards

It's used for documentation purposes and styles (such as language and formatting).

+ + + + \ No newline at end of file diff --git a/advanced/develop.html b/advanced/develop.html new file mode 100644 index 00000000..e5517506 --- /dev/null +++ b/advanced/develop.html @@ -0,0 +1,32 @@ + + + + + + Develop | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

Develop

As dependence

bash
pnpm install kotori-bot

Of course, you can also install @kotori-bot/core or @kotori-bot/loader by according your needs, about the difference and modifications between them, see architecture.

typescript
import { Loader } from 'kotori-bot'
+
+const loader = new Loader()
+
+loader.run(true)

Secondary development

1.Clone the repository

bash
git clone https://github.com/kotorijs/kotori

2.Install dependencies

bash
pnpm install

3.Run

bash
pnpm dev

Other scripts:

  • build Build all packages at the workspace
  • build:action Build all packages at the workspace base safe mode.
  • dev:only Only start else nodemon
  • pub Publish all packages at the workspace base public access
  • test Run all unit tests at the workspace
  • version Generate CHANGELOG.md
+ + + + \ No newline at end of file diff --git a/advanced/history.html b/advanced/history.html new file mode 100644 index 00000000..7f14c6a1 --- /dev/null +++ b/advanced/history.html @@ -0,0 +1,28 @@ + + + + + + History | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

History

TIP

Here need to improved.

On May In 2023 years,AI chat models(mainly referred to Claude and OpenAi) rose,I was interested in them. Many people use python to build QQ chatbot and connect to the AI chat models,but as a JavaScript developer, I decided to use JavaScript to do the same thing (Just for fun and to learn).Of course,at that time I didn't know that the community had already the chatbot framework base on Node.js.

At first,I only intended to implement QQ platform's access and the project's name is ISLABot.The name could be followed 2022 years and more early,at that time I was develop another chatbot framework's plugins by chinese language(be like shit),I named the plugin ISLABot(was from the anime character).I forgot the time when started use typescript as developing language for the project.The first commit to github is at 14th on June and the project had already renamed Kotori Bot,at the some time I fell in love with using romaji to named the project.

During the 0.x ~ 1.0 version,I had been adapted to QQ platform(base on go-cqhttp), During that time,I referred to many similar projects's api interface designs and thinkings.Finally,I released v1.0 at 29th on Dec In 2023 and moved repository from my own account biyuehu/kotori-bot to organization account kotorijs/kotori.

Waiting update...

+ + + + \ No newline at end of file diff --git a/advanced/index.html b/advanced/index.html new file mode 100644 index 00000000..e1a0db3f --- /dev/null +++ b/advanced/index.html @@ -0,0 +1,28 @@ + + + + + + Fluoro | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

Fluoro

TIP

Here need to improved.

logo

Fluoro

⚡ A modern and universal Meta-Framework to construct other frameworks. ⚡

It refers to thoughts which are Aspect-Oriented Programming, Inversion of Control and Dependency Injection. Kotori's core is based it.

Why is Fluoro?

Fluoro, its original word is Fluorine (F₂), it is the strongest monatomic oxidant in nature, except for some inert gases, it can react with almost all elements, and its compounds are extremely rich and diverse and have stability. Take this name, hoping Fluoro has strong ability, thus build various diversified frameworks and provide strong underlying support.

Other applications

Misakura is a galgame(Visual novel games) made framework based on tauri, PIXI.js (solid.js) and Fluoro. It used Fluoro to implement scripts(lines command) parser.

MoeHub is a anime and galgame characters showing system, its backend used Fluoro to implement easy to manage database by console interaction.

License

GPL-3.0 license.

+ + + + \ No newline at end of file diff --git a/advanced/testing.html b/advanced/testing.html new file mode 100644 index 00000000..848739ca --- /dev/null +++ b/advanced/testing.html @@ -0,0 +1,28 @@ + + + + + + Testing | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

Testing

TIP

Here need to improved.

Kotori used Jest to implement unit tests.

Running tests

To run the tests, run:

bash
pnpm test
+ + + + \ No newline at end of file diff --git a/advanced/thanks.html b/advanced/thanks.html new file mode 100644 index 00000000..d2ab16a4 --- /dev/null +++ b/advanced/thanks.html @@ -0,0 +1,28 @@ + + + + + + Thanks | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

Thanks

Open source is a great thing,the developing process of every open-source projects need other projects' support and reference to help self improvement and walking farther.

List

Thanks, referred more and less them at kotori's whole developing process (the ranking is not in order):

License

GPL-3.0 license.

+ + + + \ No newline at end of file diff --git a/api/index.html b/api/index.html new file mode 100644 index 00000000..20fea4c1 --- /dev/null +++ b/api/index.html @@ -0,0 +1,28 @@ + + + + + + Api references | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

Api references

This page is currently being written.For the time being, you can only view the TSDoc comments in the source code for details.Italics indicate that the documentation for this content needs to be improved.

Context

Tools

Components

Service

+ + + + \ No newline at end of file diff --git a/architecture.png b/architecture.png new file mode 100644 index 00000000..bff4cfd0 Binary files /dev/null and b/architecture.png differ diff --git a/architecture.svg b/architecture.svg new file mode 100644 index 00000000..4a50b6fc --- /dev/null +++ b/architecture.svg @@ -0,0 +1 @@ +
Loader
Loader
Modules
Modules
Context
Context
Modules
Modules
Events
Events
Service
Service
Core
Core
Fluoro
Fluoro
Message
Message
Session
Session
Command
Command
Events
Events
Regexp
Regexp
Middwares
Middwares
Task
Task
Filter
Filter
Messages
Messages
Adapter
Adapter
Elements
Elements
Api
Api
Plugins
Plugins
Config
Config
Command
Command
Messages
Messages
Session
Session
Events
Events
Regexp
Regexp
Middwares
Middwares
@kotori-bot/core
@kotori-bot/core
Decorators
Decorators
Http
Http
Colors
Colors
Random
Random
Other functions
Other functions
@kotori-bot/tools
@kotori-bot/tools
Cache
Cache
I18n
I18n
@kotori-bot/i18n
@kotori-bot/i18n
Logger
Logger
Transports
Transports
Loader
Loader
Adapters
Adapters
Server
Server
File
File
Database
Database
@kotori-bot/logger
@kotori-bot/logger
Console
Transport
Console...
@kotori-bot/loader
@kotori-bot/loader
Parsers
Parsers
Tsukiko
Tsukiko

Running process 

Running process 
Cli
Cli
Gui
Gui
Dameon
Dameon
kotori-bot
kotori-bot
Run
Run
Cli
Cli
CONFIG FILE
CONFIG FILE
DEFAULT VALUE
DEFAULT VALUE
environment variables
environment variables
CLI PARAMETERS
CLI PARAMETERS
Daemon
Daemon
External Modules
External Modules
Create
Create
Check and merge config 
Check and merge config 
Adapter
Adapter
Plugins
Plugins
Load modules
Load modules
Core
Core
Create bots
Create bots
Error catcher
Error catcher
Check update
Check update
Register contents
Register contents
Context
Context
Emit session events
Emit session events
Listen events
Listen events
Events
Events
Modules
Modules
Messages
Messages
Config
Config
Create core instance
Create core instance
Emit system events
Emit system events
Listen on_message event
Listen on_message event
Emit ready event
Emit ready event
Decorators
Decorators
Set global container
Set global container
Listen system events
Listen system events
Middlewares
Middlewares
Command
Command
Filter
Filter
Create session
Create session
Session
Session
Messages
Messages
Pass
Pass
Emit on_message event
Emit on_message event
Pass
Pass
Regexp
Regexp
task
task
Service
Service
Service
Service
Inject services
Inject services
Inject services
Inject services
Provide services
Provide services
Webui
Webui
Provide services
Provide services
Api
Api
Other
Other
Kams
Kams
Static
Static
Pages
Pages
Sandbox
Sandbox
Belong
Belong
Confirm
Confirm
Send
Send
Prompt
Prompt
Version: 1.6 Last update: 2024.8.10
Version: 1.6 Last update...
Kotori Bot Architecture
Kotori Bot Architecture
Copyright © 2023 - 2024 Arimura Sena All rights reserved
Copyright © 2023 - 20...
https://kotori.js.org
https://kotori.j...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/assets/advanced_architecture.md.Pljt76DU.js b/assets/advanced_architecture.md.Pljt76DU.js new file mode 100644 index 00000000..447f814e --- /dev/null +++ b/assets/advanced_architecture.md.Pljt76DU.js @@ -0,0 +1 @@ +import{N as t}from"./chunks/NpmBadge.BObfAVH-.js";import{c as i,a0 as l,G as a,j as o,a as r,o as s}from"./chunks/framework.P9qPzDnn.js";const n="/architecture.svg",m=JSON.parse('{"title":"Architecture","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/architecture.md","filePath":"advanced/architecture.md","lastUpdated":1723295344000}'),d={name:"advanced/architecture.md"},f=Object.assign(d,{setup(u){return(k,e)=>(s(),i("div",null,[e[0]||(e[0]=l('

Architecture

Kotori use workspace developing (monorepo) by pnpm and submodules management by git, and linter and formatter by Biome (fucks eslint and prettier).

Packages

fluoro (submodule)

',4)),a(t,{package:"fluoro"}),e[1]||(e[1]=o("p",null,[r("Fluoro is universal meta-framework,provides "),o("code",null,"Context"),r(),o("code",null,"Modules"),r(" and "),o("code",null,"Events"),r(". it is the base of whole Kotori,more details view "),o("a",{href:"./"},"Fluoro"),r(".")],-1)),e[2]||(e[2]=o("h3",{id:"kotori-bot-core",tabindex:"-1"},[r("@kotori-bot/core "),o("a",{class:"header-anchor",href:"#kotori-bot-core","aria-label":'Permalink to "@kotori-bot/core"'},"​")],-1)),a(t,{package:"@kotori-bot/core"}),e[3]||(e[3]=o("p",null,[r("It is the core of Kotori and provides many important features of kotori (encapsulates "),o("code",null,"fluoro"),r(").It has fully the support for browser and other not node.js (such as "),o("a",{href:"https://bellard.org/quickjs/",target:"_blank",rel:"noreferrer"},"quick.js"),r(" or service-worker),more details view "),o("a",{href:"./browser.html"},"Use in browser"),r(".")],-1)),e[4]||(e[4]=o("h3",{id:"kotori-bot-loader",tabindex:"-1"},[r("@kotori-bot/loader "),o("a",{class:"header-anchor",href:"#kotori-bot-loader","aria-label":'Permalink to "@kotori-bot/loader"'},"​")],-1)),a(t,{package:"@kotori-bot/loader"}),e[5]||(e[5]=o("p",null,[r("It encapsulates "),o("code",null,"@kotori-bot/loader"),r(" and provides some interface and features need server environments (Node.js).")],-1)),e[6]||(e[6]=o("h3",{id:"kotori-bot-logger",tabindex:"-1"},[r("@kotori-bot/logger "),o("a",{class:"header-anchor",href:"#kotori-bot-logger","aria-label":'Permalink to "@kotori-bot/logger"'},"​")],-1)),a(t,{package:"@kotori-bot/logger"}),e[7]||(e[7]=o("p",null,"It is fully functional logger base on adapters thinking and supports browser.",-1)),e[8]||(e[8]=o("h3",{id:"kotori-bot-i18n",tabindex:"-1"},[r("@kotori-bot/i18n "),o("a",{class:"header-anchor",href:"#kotori-bot-i18n","aria-label":'Permalink to "@kotori-bot/i18n"'},"​")],-1)),a(t,{package:"@kotori-bot/i18n"}),e[9]||(e[9]=o("p",null,[r("It is a i18n module,provides "),o("code",null,"i18n"),r(" for "),o("code",null,"@kotori-bot/core"),r(".")],-1)),e[10]||(e[10]=o("h3",{id:"kotori-bot-tools",tabindex:"-1"},[r("@kotori-bot/tools "),o("a",{class:"header-anchor",href:"#kotori-bot-tools","aria-label":'Permalink to "@kotori-bot/tools"'},"​")],-1)),a(t,{package:"@kotori-bot/tools"}),e[11]||(e[11]=o("p",null,[r("It is a tools module,provides some useful tools for "),o("code",null,"@kotori-bot/core"),r(" and "),o("code",null,"@kotori-bot/loader"),r(".")],-1)),e[12]||(e[12]=o("h3",{id:"tsukiko",tabindex:"-1"},[r("tsukiko "),o("a",{class:"header-anchor",href:"#tsukiko","aria-label":'Permalink to "tsukiko"'},"​")],-1)),a(t,{package:"tsukiko"}),e[13]||(e[13]=o("p",null,"It used to parse and check data type at running time.",-1)),e[14]||(e[14]=o("h2",{id:"process-image",tabindex:"-1"},[r("Process image "),o("a",{class:"header-anchor",href:"#process-image","aria-label":'Permalink to "Process image"'},"​")],-1)),e[15]||(e[15]=o("p",null,[o("img",{src:n,alt:"architecture"})],-1))]))}});export{m as __pageData,f as default}; diff --git a/assets/advanced_architecture.md.Pljt76DU.lean.js b/assets/advanced_architecture.md.Pljt76DU.lean.js new file mode 100644 index 00000000..447f814e --- /dev/null +++ b/assets/advanced_architecture.md.Pljt76DU.lean.js @@ -0,0 +1 @@ +import{N as t}from"./chunks/NpmBadge.BObfAVH-.js";import{c as i,a0 as l,G as a,j as o,a as r,o as s}from"./chunks/framework.P9qPzDnn.js";const n="/architecture.svg",m=JSON.parse('{"title":"Architecture","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/architecture.md","filePath":"advanced/architecture.md","lastUpdated":1723295344000}'),d={name:"advanced/architecture.md"},f=Object.assign(d,{setup(u){return(k,e)=>(s(),i("div",null,[e[0]||(e[0]=l('

Architecture

Kotori use workspace developing (monorepo) by pnpm and submodules management by git, and linter and formatter by Biome (fucks eslint and prettier).

Packages

fluoro (submodule)

',4)),a(t,{package:"fluoro"}),e[1]||(e[1]=o("p",null,[r("Fluoro is universal meta-framework,provides "),o("code",null,"Context"),r(),o("code",null,"Modules"),r(" and "),o("code",null,"Events"),r(". it is the base of whole Kotori,more details view "),o("a",{href:"./"},"Fluoro"),r(".")],-1)),e[2]||(e[2]=o("h3",{id:"kotori-bot-core",tabindex:"-1"},[r("@kotori-bot/core "),o("a",{class:"header-anchor",href:"#kotori-bot-core","aria-label":'Permalink to "@kotori-bot/core"'},"​")],-1)),a(t,{package:"@kotori-bot/core"}),e[3]||(e[3]=o("p",null,[r("It is the core of Kotori and provides many important features of kotori (encapsulates "),o("code",null,"fluoro"),r(").It has fully the support for browser and other not node.js (such as "),o("a",{href:"https://bellard.org/quickjs/",target:"_blank",rel:"noreferrer"},"quick.js"),r(" or service-worker),more details view "),o("a",{href:"./browser.html"},"Use in browser"),r(".")],-1)),e[4]||(e[4]=o("h3",{id:"kotori-bot-loader",tabindex:"-1"},[r("@kotori-bot/loader "),o("a",{class:"header-anchor",href:"#kotori-bot-loader","aria-label":'Permalink to "@kotori-bot/loader"'},"​")],-1)),a(t,{package:"@kotori-bot/loader"}),e[5]||(e[5]=o("p",null,[r("It encapsulates "),o("code",null,"@kotori-bot/loader"),r(" and provides some interface and features need server environments (Node.js).")],-1)),e[6]||(e[6]=o("h3",{id:"kotori-bot-logger",tabindex:"-1"},[r("@kotori-bot/logger "),o("a",{class:"header-anchor",href:"#kotori-bot-logger","aria-label":'Permalink to "@kotori-bot/logger"'},"​")],-1)),a(t,{package:"@kotori-bot/logger"}),e[7]||(e[7]=o("p",null,"It is fully functional logger base on adapters thinking and supports browser.",-1)),e[8]||(e[8]=o("h3",{id:"kotori-bot-i18n",tabindex:"-1"},[r("@kotori-bot/i18n "),o("a",{class:"header-anchor",href:"#kotori-bot-i18n","aria-label":'Permalink to "@kotori-bot/i18n"'},"​")],-1)),a(t,{package:"@kotori-bot/i18n"}),e[9]||(e[9]=o("p",null,[r("It is a i18n module,provides "),o("code",null,"i18n"),r(" for "),o("code",null,"@kotori-bot/core"),r(".")],-1)),e[10]||(e[10]=o("h3",{id:"kotori-bot-tools",tabindex:"-1"},[r("@kotori-bot/tools "),o("a",{class:"header-anchor",href:"#kotori-bot-tools","aria-label":'Permalink to "@kotori-bot/tools"'},"​")],-1)),a(t,{package:"@kotori-bot/tools"}),e[11]||(e[11]=o("p",null,[r("It is a tools module,provides some useful tools for "),o("code",null,"@kotori-bot/core"),r(" and "),o("code",null,"@kotori-bot/loader"),r(".")],-1)),e[12]||(e[12]=o("h3",{id:"tsukiko",tabindex:"-1"},[r("tsukiko "),o("a",{class:"header-anchor",href:"#tsukiko","aria-label":'Permalink to "tsukiko"'},"​")],-1)),a(t,{package:"tsukiko"}),e[13]||(e[13]=o("p",null,"It used to parse and check data type at running time.",-1)),e[14]||(e[14]=o("h2",{id:"process-image",tabindex:"-1"},[r("Process image "),o("a",{class:"header-anchor",href:"#process-image","aria-label":'Permalink to "Process image"'},"​")],-1)),e[15]||(e[15]=o("p",null,[o("img",{src:n,alt:"architecture"})],-1))]))}});export{m as __pageData,f as default}; diff --git a/assets/advanced_browser.md.CURKrjcr.js b/assets/advanced_browser.md.CURKrjcr.js new file mode 100644 index 00000000..4e9e9d2b --- /dev/null +++ b/assets/advanced_browser.md.CURKrjcr.js @@ -0,0 +1,68 @@ +import{_ as i,c as a,a0 as n,o as h}from"./chunks/framework.P9qPzDnn.js";const y=JSON.parse('{"title":"Use in browser","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/browser.md","filePath":"advanced/browser.md","lastUpdated":1723293723000}'),k={name:"advanced/browser.md"};function p(l,s,t,e,r,d){return h(),a("div",null,s[0]||(s[0]=[n(`

Use in browser

To wait better supports...

bash
pnpm install @kotori-bot/core

The @kotori-bot/core package is the core package of the bot,it only used ecmascript standard api, so you can use it in any the environments which support ecmascript >= 2020.

typescript
import { Adapter, Api, Core, Elements, type Message, Messages, MessageScope } from '@kotori-bot/core'
+
+const core = new Core({
+  global: {
+    commandPrefix: '/'
+  }
+})
+
+function MyPlugin(ctx: Core) {
+  ctx.command('echo <msg>').action(({ args: [msg] }, session) => {
+    alert(\`You said: \${msg}\`)
+    console.log(session)
+  })
+}
+
+core.load(MyPlugin)
+
+core.start()
+
+class BrowserAdapter extends Adapter {
+  public platform = 'browser'
+
+  public constructor(ctx: Core) {
+    super(ctx, { commandPrefix: '/', extends: 'browser', master: '1', lang: 'zh_CN' }, 'browser')
+  }
+
+  public api = new (class extends Api {
+    public getSupportedEvents(): ReturnType<Api['getSupportedEvents']> {
+      return ['on_message']
+    }
+  })(this) as Api
+
+  public elements = new (class extends Elements {
+    getSupportsElements(): ReturnType<Elements['getSupportsElements']> {
+      return []
+    }
+
+    decode(message: Message): string {
+      return message.toString()
+    }
+
+    encode(raw: string): Message {
+      return Messages(raw)
+    }
+  })(this) as Elements
+
+  public handle = this.session.bind(this)
+
+  public start() {}
+  public stop() {}
+  public send() {}
+}
+
+const bot = new BrowserAdapter(core)
+
+const result = prompt('input:')
+
+bot.handle('on_message', {
+  type: MessageScope.PRIVATE,
+  message: result ?? '',
+  messageAlt: 'alt',
+  messageId: '1',
+  time: Date.now(),
+  userId: '1',
+  sender: {
+    nickname: 'my-browser'
+  }
+})
`,5)]))}const A=i(k,[["render",p]]);export{y as __pageData,A as default}; diff --git a/assets/advanced_browser.md.CURKrjcr.lean.js b/assets/advanced_browser.md.CURKrjcr.lean.js new file mode 100644 index 00000000..4e9e9d2b --- /dev/null +++ b/assets/advanced_browser.md.CURKrjcr.lean.js @@ -0,0 +1,68 @@ +import{_ as i,c as a,a0 as n,o as h}from"./chunks/framework.P9qPzDnn.js";const y=JSON.parse('{"title":"Use in browser","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/browser.md","filePath":"advanced/browser.md","lastUpdated":1723293723000}'),k={name:"advanced/browser.md"};function p(l,s,t,e,r,d){return h(),a("div",null,s[0]||(s[0]=[n(`

Use in browser

To wait better supports...

bash
pnpm install @kotori-bot/core

The @kotori-bot/core package is the core package of the bot,it only used ecmascript standard api, so you can use it in any the environments which support ecmascript >= 2020.

typescript
import { Adapter, Api, Core, Elements, type Message, Messages, MessageScope } from '@kotori-bot/core'
+
+const core = new Core({
+  global: {
+    commandPrefix: '/'
+  }
+})
+
+function MyPlugin(ctx: Core) {
+  ctx.command('echo <msg>').action(({ args: [msg] }, session) => {
+    alert(\`You said: \${msg}\`)
+    console.log(session)
+  })
+}
+
+core.load(MyPlugin)
+
+core.start()
+
+class BrowserAdapter extends Adapter {
+  public platform = 'browser'
+
+  public constructor(ctx: Core) {
+    super(ctx, { commandPrefix: '/', extends: 'browser', master: '1', lang: 'zh_CN' }, 'browser')
+  }
+
+  public api = new (class extends Api {
+    public getSupportedEvents(): ReturnType<Api['getSupportedEvents']> {
+      return ['on_message']
+    }
+  })(this) as Api
+
+  public elements = new (class extends Elements {
+    getSupportsElements(): ReturnType<Elements['getSupportsElements']> {
+      return []
+    }
+
+    decode(message: Message): string {
+      return message.toString()
+    }
+
+    encode(raw: string): Message {
+      return Messages(raw)
+    }
+  })(this) as Elements
+
+  public handle = this.session.bind(this)
+
+  public start() {}
+  public stop() {}
+  public send() {}
+}
+
+const bot = new BrowserAdapter(core)
+
+const result = prompt('input:')
+
+bot.handle('on_message', {
+  type: MessageScope.PRIVATE,
+  message: result ?? '',
+  messageAlt: 'alt',
+  messageId: '1',
+  time: Date.now(),
+  userId: '1',
+  sender: {
+    nickname: 'my-browser'
+  }
+})
`,5)]))}const A=i(k,[["render",p]]);export{y as __pageData,A as default}; diff --git a/assets/advanced_contributing.md.CcXEnDf1.js b/assets/advanced_contributing.md.CcXEnDf1.js new file mode 100644 index 00000000..8fd1b5c0 --- /dev/null +++ b/assets/advanced_contributing.md.CcXEnDf1.js @@ -0,0 +1 @@ +import{_ as e,c as a,a0 as o,o as r}from"./chunks/framework.P9qPzDnn.js";const m=JSON.parse('{"title":"Contributing","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/contributing.md","filePath":"advanced/contributing.md","lastUpdated":1723293723000}'),i={name:"advanced/contributing.md"};function s(n,t,c,d,l,p){return r(),a("div",null,t[0]||(t[0]=[o('

Contributing

TIP

Here need to improved.

This project is open source and we welcome your contributions!

Steps

  1. Fork it!

  2. Create your feature branch: git checkout -b my-new-feature

  3. Commit your changes: git commit -am 'Add some feature'

  4. Push to the branch: git push origin my-new-feature

  5. Submit a pull request 😄

More details please refer to CONTRIBUTING.md

Docs standards

It's used for documentation purposes and styles (such as language and formatting).

',8)]))}const h=e(i,[["render",s]]);export{m as __pageData,h as default}; diff --git a/assets/advanced_contributing.md.CcXEnDf1.lean.js b/assets/advanced_contributing.md.CcXEnDf1.lean.js new file mode 100644 index 00000000..8fd1b5c0 --- /dev/null +++ b/assets/advanced_contributing.md.CcXEnDf1.lean.js @@ -0,0 +1 @@ +import{_ as e,c as a,a0 as o,o as r}from"./chunks/framework.P9qPzDnn.js";const m=JSON.parse('{"title":"Contributing","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/contributing.md","filePath":"advanced/contributing.md","lastUpdated":1723293723000}'),i={name:"advanced/contributing.md"};function s(n,t,c,d,l,p){return r(),a("div",null,t[0]||(t[0]=[o('

Contributing

TIP

Here need to improved.

This project is open source and we welcome your contributions!

Steps

  1. Fork it!

  2. Create your feature branch: git checkout -b my-new-feature

  3. Commit your changes: git commit -am 'Add some feature'

  4. Push to the branch: git push origin my-new-feature

  5. Submit a pull request 😄

More details please refer to CONTRIBUTING.md

Docs standards

It's used for documentation purposes and styles (such as language and formatting).

',8)]))}const h=e(i,[["render",s]]);export{m as __pageData,h as default}; diff --git a/assets/advanced_develop.md.CCQONjp6.js b/assets/advanced_develop.md.CCQONjp6.js new file mode 100644 index 00000000..cca1abcf --- /dev/null +++ b/assets/advanced_develop.md.CCQONjp6.js @@ -0,0 +1,5 @@ +import{_ as e,c as a,a0 as i,o as t}from"./chunks/framework.P9qPzDnn.js";const r=JSON.parse('{"title":"Develop","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/develop.md","filePath":"advanced/develop.md","lastUpdated":1723293723000}'),n={name:"advanced/develop.md"};function l(p,s,d,o,h,c){return t(),a("div",null,s[0]||(s[0]=[i(`

Develop

As dependence

bash
pnpm install kotori-bot

Of course, you can also install @kotori-bot/core or @kotori-bot/loader by according your needs, about the difference and modifications between them, see architecture.

typescript
import { Loader } from 'kotori-bot'
+
+const loader = new Loader()
+
+loader.run(true)

Secondary development

1.Clone the repository

bash
git clone https://github.com/kotorijs/kotori

2.Install dependencies

bash
pnpm install

3.Run

bash
pnpm dev

Other scripts:

`,14)]))}const g=e(n,[["render",l]]);export{r as __pageData,g as default}; diff --git a/assets/advanced_develop.md.CCQONjp6.lean.js b/assets/advanced_develop.md.CCQONjp6.lean.js new file mode 100644 index 00000000..cca1abcf --- /dev/null +++ b/assets/advanced_develop.md.CCQONjp6.lean.js @@ -0,0 +1,5 @@ +import{_ as e,c as a,a0 as i,o as t}from"./chunks/framework.P9qPzDnn.js";const r=JSON.parse('{"title":"Develop","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/develop.md","filePath":"advanced/develop.md","lastUpdated":1723293723000}'),n={name:"advanced/develop.md"};function l(p,s,d,o,h,c){return t(),a("div",null,s[0]||(s[0]=[i(`

Develop

As dependence

bash
pnpm install kotori-bot

Of course, you can also install @kotori-bot/core or @kotori-bot/loader by according your needs, about the difference and modifications between them, see architecture.

typescript
import { Loader } from 'kotori-bot'
+
+const loader = new Loader()
+
+loader.run(true)

Secondary development

1.Clone the repository

bash
git clone https://github.com/kotorijs/kotori

2.Install dependencies

bash
pnpm install

3.Run

bash
pnpm dev

Other scripts:

`,14)]))}const g=e(n,[["render",l]]);export{r as __pageData,g as default}; diff --git a/assets/advanced_history.md.FDb9Wd5O.js b/assets/advanced_history.md.FDb9Wd5O.js new file mode 100644 index 00000000..b88ddf11 --- /dev/null +++ b/assets/advanced_history.md.FDb9Wd5O.js @@ -0,0 +1 @@ +import{_ as e,c as a,a0 as o,o as r}from"./chunks/framework.P9qPzDnn.js";const m=JSON.parse('{"title":"History","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/history.md","filePath":"advanced/history.md","lastUpdated":1723293723000}'),n={name:"advanced/history.md"};function i(s,t,d,h,c,l){return r(),a("div",null,t[0]||(t[0]=[o('

History

TIP

Here need to improved.

On May In 2023 years,AI chat models(mainly referred to Claude and OpenAi) rose,I was interested in them. Many people use python to build QQ chatbot and connect to the AI chat models,but as a JavaScript developer, I decided to use JavaScript to do the same thing (Just for fun and to learn).Of course,at that time I didn't know that the community had already the chatbot framework base on Node.js.

At first,I only intended to implement QQ platform's access and the project's name is ISLABot.The name could be followed 2022 years and more early,at that time I was develop another chatbot framework's plugins by chinese language(be like shit),I named the plugin ISLABot(was from the anime character).I forgot the time when started use typescript as developing language for the project.The first commit to github is at 14th on June and the project had already renamed Kotori Bot,at the some time I fell in love with using romaji to named the project.

During the 0.x ~ 1.0 version,I had been adapted to QQ platform(base on go-cqhttp), During that time,I referred to many similar projects's api interface designs and thinkings.Finally,I released v1.0 at 29th on Dec In 2023 and moved repository from my own account biyuehu/kotori-bot to organization account kotorijs/kotori.

Waiting update...

',6)]))}const u=e(n,[["render",i]]);export{m as __pageData,u as default}; diff --git a/assets/advanced_history.md.FDb9Wd5O.lean.js b/assets/advanced_history.md.FDb9Wd5O.lean.js new file mode 100644 index 00000000..b88ddf11 --- /dev/null +++ b/assets/advanced_history.md.FDb9Wd5O.lean.js @@ -0,0 +1 @@ +import{_ as e,c as a,a0 as o,o as r}from"./chunks/framework.P9qPzDnn.js";const m=JSON.parse('{"title":"History","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/history.md","filePath":"advanced/history.md","lastUpdated":1723293723000}'),n={name:"advanced/history.md"};function i(s,t,d,h,c,l){return r(),a("div",null,t[0]||(t[0]=[o('

History

TIP

Here need to improved.

On May In 2023 years,AI chat models(mainly referred to Claude and OpenAi) rose,I was interested in them. Many people use python to build QQ chatbot and connect to the AI chat models,but as a JavaScript developer, I decided to use JavaScript to do the same thing (Just for fun and to learn).Of course,at that time I didn't know that the community had already the chatbot framework base on Node.js.

At first,I only intended to implement QQ platform's access and the project's name is ISLABot.The name could be followed 2022 years and more early,at that time I was develop another chatbot framework's plugins by chinese language(be like shit),I named the plugin ISLABot(was from the anime character).I forgot the time when started use typescript as developing language for the project.The first commit to github is at 14th on June and the project had already renamed Kotori Bot,at the some time I fell in love with using romaji to named the project.

During the 0.x ~ 1.0 version,I had been adapted to QQ platform(base on go-cqhttp), During that time,I referred to many similar projects's api interface designs and thinkings.Finally,I released v1.0 at 29th on Dec In 2023 and moved repository from my own account biyuehu/kotori-bot to organization account kotorijs/kotori.

Waiting update...

',6)]))}const u=e(n,[["render",i]]);export{m as __pageData,u as default}; diff --git a/assets/advanced_index.md.epBIxi7X.js b/assets/advanced_index.md.epBIxi7X.js new file mode 100644 index 00000000..64ffa8dc --- /dev/null +++ b/assets/advanced_index.md.epBIxi7X.js @@ -0,0 +1 @@ +import{_ as a,c as o,a0 as r,o as t}from"./chunks/framework.P9qPzDnn.js";const i="/fluoro.png",m=JSON.parse('{"title":"Fluoro","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/index.md","filePath":"advanced/index.md","lastUpdated":1725520614000}'),s={name:"advanced/index.md"};function n(l,e,d,c,u,h){return t(),o("div",null,e[0]||(e[0]=[r('

Fluoro

TIP

Here need to improved.

logo

Fluoro

⚡ A modern and universal Meta-Framework to construct other frameworks. ⚡

It refers to thoughts which are Aspect-Oriented Programming, Inversion of Control and Dependency Injection. Kotori's core is based it.

Why is Fluoro?

Fluoro, its original word is Fluorine (F₂), it is the strongest monatomic oxidant in nature, except for some inert gases, it can react with almost all elements, and its compounds are extremely rich and diverse and have stability. Take this name, hoping Fluoro has strong ability, thus build various diversified frameworks and provide strong underlying support.

Other applications

Misakura is a galgame(Visual novel games) made framework based on tauri, PIXI.js (solid.js) and Fluoro. It used Fluoro to implement scripts(lines command) parser.

MoeHub is a anime and galgame characters showing system, its backend used Fluoro to implement easy to manage database by console interaction.

License

GPL-3.0 license.

',13)]))}const g=a(s,[["render",n]]);export{m as __pageData,g as default}; diff --git a/assets/advanced_index.md.epBIxi7X.lean.js b/assets/advanced_index.md.epBIxi7X.lean.js new file mode 100644 index 00000000..64ffa8dc --- /dev/null +++ b/assets/advanced_index.md.epBIxi7X.lean.js @@ -0,0 +1 @@ +import{_ as a,c as o,a0 as r,o as t}from"./chunks/framework.P9qPzDnn.js";const i="/fluoro.png",m=JSON.parse('{"title":"Fluoro","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/index.md","filePath":"advanced/index.md","lastUpdated":1725520614000}'),s={name:"advanced/index.md"};function n(l,e,d,c,u,h){return t(),o("div",null,e[0]||(e[0]=[r('

Fluoro

TIP

Here need to improved.

logo

Fluoro

⚡ A modern and universal Meta-Framework to construct other frameworks. ⚡

It refers to thoughts which are Aspect-Oriented Programming, Inversion of Control and Dependency Injection. Kotori's core is based it.

Why is Fluoro?

Fluoro, its original word is Fluorine (F₂), it is the strongest monatomic oxidant in nature, except for some inert gases, it can react with almost all elements, and its compounds are extremely rich and diverse and have stability. Take this name, hoping Fluoro has strong ability, thus build various diversified frameworks and provide strong underlying support.

Other applications

Misakura is a galgame(Visual novel games) made framework based on tauri, PIXI.js (solid.js) and Fluoro. It used Fluoro to implement scripts(lines command) parser.

MoeHub is a anime and galgame characters showing system, its backend used Fluoro to implement easy to manage database by console interaction.

License

GPL-3.0 license.

',13)]))}const g=a(s,[["render",n]]);export{m as __pageData,g as default}; diff --git a/assets/advanced_testing.md.BIJx2vTA.js b/assets/advanced_testing.md.BIJx2vTA.js new file mode 100644 index 00000000..aacf6b7d --- /dev/null +++ b/assets/advanced_testing.md.BIJx2vTA.js @@ -0,0 +1 @@ +import{_ as e,c as s,a0 as a,o as n}from"./chunks/framework.P9qPzDnn.js";const u=JSON.parse('{"title":"Testing","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/testing.md","filePath":"advanced/testing.md","lastUpdated":1723293723000}'),i={name:"advanced/testing.md"};function r(o,t,d,l,p,c){return n(),s("div",null,t[0]||(t[0]=[a('

Testing

TIP

Here need to improved.

Kotori used Jest to implement unit tests.

Running tests

To run the tests, run:

bash
pnpm test
',6)]))}const g=e(i,[["render",r]]);export{u as __pageData,g as default}; diff --git a/assets/advanced_testing.md.BIJx2vTA.lean.js b/assets/advanced_testing.md.BIJx2vTA.lean.js new file mode 100644 index 00000000..aacf6b7d --- /dev/null +++ b/assets/advanced_testing.md.BIJx2vTA.lean.js @@ -0,0 +1 @@ +import{_ as e,c as s,a0 as a,o as n}from"./chunks/framework.P9qPzDnn.js";const u=JSON.parse('{"title":"Testing","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/testing.md","filePath":"advanced/testing.md","lastUpdated":1723293723000}'),i={name:"advanced/testing.md"};function r(o,t,d,l,p,c){return n(),s("div",null,t[0]||(t[0]=[a('

Testing

TIP

Here need to improved.

Kotori used Jest to implement unit tests.

Running tests

To run the tests, run:

bash
pnpm test
',6)]))}const g=e(i,[["render",r]]);export{u as __pageData,g as default}; diff --git a/assets/advanced_thanks.md.CK7Fjmcb.js b/assets/advanced_thanks.md.CK7Fjmcb.js new file mode 100644 index 00000000..8f84f7f1 --- /dev/null +++ b/assets/advanced_thanks.md.CK7Fjmcb.js @@ -0,0 +1 @@ +import{_ as a,c as t,a0 as r,o as n}from"./chunks/framework.P9qPzDnn.js";const f=JSON.parse('{"title":"Thanks","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/thanks.md","filePath":"advanced/thanks.md","lastUpdated":1723293723000}'),o={name:"advanced/thanks.md"};function i(s,e,l,h,c,d){return n(),t("div",null,e[0]||(e[0]=[r('

Thanks

Open source is a great thing,the developing process of every open-source projects need other projects' support and reference to help self improvement and walking farther.

List

Thanks, referred more and less them at kotori's whole developing process (the ranking is not in order):

License

GPL-3.0 license.

',7)]))}const k=a(o,[["render",i]]);export{f as __pageData,k as default}; diff --git a/assets/advanced_thanks.md.CK7Fjmcb.lean.js b/assets/advanced_thanks.md.CK7Fjmcb.lean.js new file mode 100644 index 00000000..8f84f7f1 --- /dev/null +++ b/assets/advanced_thanks.md.CK7Fjmcb.lean.js @@ -0,0 +1 @@ +import{_ as a,c as t,a0 as r,o as n}from"./chunks/framework.P9qPzDnn.js";const f=JSON.parse('{"title":"Thanks","description":"","frontmatter":{},"headers":[],"relativePath":"advanced/thanks.md","filePath":"advanced/thanks.md","lastUpdated":1723293723000}'),o={name:"advanced/thanks.md"};function i(s,e,l,h,c,d){return n(),t("div",null,e[0]||(e[0]=[r('

Thanks

Open source is a great thing,the developing process of every open-source projects need other projects' support and reference to help self improvement and walking farther.

List

Thanks, referred more and less them at kotori's whole developing process (the ranking is not in order):

License

GPL-3.0 license.

',7)]))}const k=a(o,[["render",i]]);export{f as __pageData,k as default}; diff --git a/assets/api_index.md.B1SyjzrW.js b/assets/api_index.md.B1SyjzrW.js new file mode 100644 index 00000000..149adaca --- /dev/null +++ b/assets/api_index.md.B1SyjzrW.js @@ -0,0 +1 @@ +import{_ as r,c as t,a0 as o,o as a}from"./chunks/framework.P9qPzDnn.js";const k=JSON.parse('{"title":"Api references","description":"","frontmatter":{},"headers":[],"relativePath":"api/index.md","filePath":"api/index.md","lastUpdated":1723293723000}'),s={name:"api/index.md"};function i(l,e,c,n,m,h){return a(),t("div",null,e[0]||(e[0]=[o('

Api references

This page is currently being written.For the time being, you can only view the TSDoc comments in the source code for details.Italics indicate that the documentation for this content needs to be improved.

Context

Tools

Components

Service

',10)]))}const p=r(s,[["render",i]]);export{k as __pageData,p as default}; diff --git a/assets/api_index.md.B1SyjzrW.lean.js b/assets/api_index.md.B1SyjzrW.lean.js new file mode 100644 index 00000000..149adaca --- /dev/null +++ b/assets/api_index.md.B1SyjzrW.lean.js @@ -0,0 +1 @@ +import{_ as r,c as t,a0 as o,o as a}from"./chunks/framework.P9qPzDnn.js";const k=JSON.parse('{"title":"Api references","description":"","frontmatter":{},"headers":[],"relativePath":"api/index.md","filePath":"api/index.md","lastUpdated":1723293723000}'),s={name:"api/index.md"};function i(l,e,c,n,m,h){return a(),t("div",null,e[0]||(e[0]=[o('

Api references

This page is currently being written.For the time being, you can only view the TSDoc comments in the source code for details.Italics indicate that the documentation for this content needs to be improved.

Context

Tools

Components

Service

',10)]))}const p=r(s,[["render",i]]);export{k as __pageData,p as default}; diff --git a/assets/app.CnTrt7QD.js b/assets/app.CnTrt7QD.js new file mode 100644 index 00000000..7b7f9f63 --- /dev/null +++ b/assets/app.CnTrt7QD.js @@ -0,0 +1 @@ +import{t as i}from"./chunks/theme.DXZpxO3K.js";import{R as o,a1 as u,a2 as c,a3 as l,a4 as f,a5 as d,a6 as m,a7 as h,a8 as g,a9 as A,aa as v,d as P,u as y,v as C,s as b,ab as w,ac as R,ad as E,ae as S}from"./chunks/framework.P9qPzDnn.js";function p(e){if(e.extends){const a=p(e.extends);return{...a,...e,async enhanceApp(t){a.enhanceApp&&await a.enhanceApp(t),e.enhanceApp&&await e.enhanceApp(t)}}}return e}const s=p(i),T=P({name:"VitePressApp",setup(){const{site:e,lang:a,dir:t}=y();return C(()=>{b(()=>{document.documentElement.lang=a.value,document.documentElement.dir=t.value})}),e.value.router.prefetchLinks&&w(),R(),E(),s.setup&&s.setup(),()=>S(s.Layout)}});async function D(){globalThis.__VITEPRESS__=!0;const e=j(),a=_();a.provide(c,e);const t=l(e.route);return a.provide(f,t),a.component("Content",d),a.component("ClientOnly",m),Object.defineProperties(a.config.globalProperties,{$frontmatter:{get(){return t.frontmatter.value}},$params:{get(){return t.page.value.params}}}),s.enhanceApp&&await s.enhanceApp({app:a,router:e,siteData:h}),{app:a,router:e,data:t}}function _(){return g(T)}function j(){let e=o,a;return A(t=>{let n=v(t),r=null;return n&&(e&&(a=n),(e||a===n)&&(n=n.replace(/\.js$/,".lean.js")),r=import(n)),o&&(e=!1),r},s.NotFound)}o&&D().then(({app:e,router:a,data:t})=>{a.go().then(()=>{u(a.route,t.site),e.mount("#app")})});export{D as createApp}; diff --git a/assets/basic_config.md.CIBSN9d7.js b/assets/basic_config.md.CIBSN9d7.js new file mode 100644 index 00000000..36683470 --- /dev/null +++ b/assets/basic_config.md.CIBSN9d7.js @@ -0,0 +1,55 @@ +import{_ as i,c as a,a0 as l,o as n}from"./chunks/framework.P9qPzDnn.js";const g=JSON.parse('{"title":"配置详解","description":"","frontmatter":{},"headers":[],"relativePath":"basic/config.md","filePath":"basic/config.md","lastUpdated":1723345558000}'),h={name:"basic/config.md"};function t(e,s,p,k,d,r){return n(),a("div",null,s[0]||(s[0]=[l(`

配置详解

前面一节已对 kotori.toml 有了大概认识,本节内容将更为全面的介绍它。kotori.toml 是一个 Kotori 程序的核心配置文件,它一般位于 Kotori 根目录,与 package.json 文件同级,使用 TOML 格式。

文件格式

虽然默认使用的是 TOML 格式,但 Kotori 也支持 YAML 格式(.yaml.yml)与 JSON 格式,其它格式使用方法请参考下文

配置项

以下是将先前的配置片段集中在一起的例子(仅作参考请勿直接复制):

toml
[global]
+port = 720
+dbPrefix = "romiChan"
+lang = "en_US"
+commandPrefix = "/"
+noColor = false
+level = 25
+dirs = [
+  "./node_modules/@custom-scope/",
+  "./test_modules"
+]
+
+[adapter.cmd-test]
+extends = "cmd"
+master = 2_333
+nickname = "Kotarou"
+age = 18
+sex = "male"
+self-id = 720
+
+[adapter.kisaki]
+extends = "qq"
+appid = "xxxx"
+secret = "xxxxx"
+master = 2_333
+retry = 10
+
+[plugin.menu]
+alias = "cd"
+keywords = [ "菜单", "功能", "帮助" ]
+content = "菜单 | 小鳥%break%/menu - 查看BOT菜单%break%/hitokoto - 获取一条一言%break%ByHotaru"

global.lang

定义全局使用的语言,目前仅支持英语、日语、台湾语、中文四门语言。

typescript
type LocaleType = 'en_US' | 'ja_JP' | 'zh_TW' | 'zh_CN';

global.commandPrefix

定义全局使用的命令前缀。

global.dirs

定义需要加载的模块目录。

global.port

定义 Kotori 使用的端口(Http 服务器与 WebSocket 服务器)。

global.dbPrefix

定义 Kotori 使用的数据库前缀。

global.noColor

定义是否禁用彩色输出。

global.level

定义日志输出级别。

typescript
export enum LoggerLevel {
+  TRACE = 10,
+  DEBUG = 20,
+  RECORD = 25,
+  INFO = 30,
+  WARN = 40,
+  ERROR = 50,
+  FATAL = 60,
+  SILENT = 70
+}

adapter

定义 Bot。

typescript
interface AdapterConfig {
+  extends: string;
+  master?: number;
+  lang?: langType;
+  commandPrefix?: string;
+  [propName: string]?: unknown;
+}

AdapterConfig.extends

定义该 Bot 使用的适配器。

AdapterConfig.master

定义该 Bot 的最高管理员 id(即该用户在平台的 id)。

AdapterConfig.lang

定义该 Bot 使用的语言。

AdapterConfig.commandPrefix

定义该 Bot 使用的命令前缀。

AdapterConfig[propName]

除去以上由 Kotori 内部定义的配置项,extends 中指定的适配器一般会额外定义配置项用于 Bot 内部,这些配置项也可能不存在或为可选,具体请参考该模块的详情页。

plugin

定义插件的配置项。

typescript
interface PluginConfig {
+  filter?: {};
+  [propName: string]?: unknown;
+}

PluginConfig.filter

定义该插件使用的滤器。

关于滤器使用请参考 滤器

PluginConfig[propName]

类似于 AdapterConfig 中的 [propName],该插件也可能会定义一些配置项用于插件内部,具体请参考该模块的详情页。

CLI 参数

Kotori 提供了一些 CLI 参数,用于在启动时修改配置文件中的配置项。

环境变量

Kotori 提供了一些环境变量,用于在启动时修改配置文件中的配置项。

设置环境变量只需在运行根目录下创建 .env 文件,并以 KEY=VALUE 的形式写入即可,例如:

ini
NODE_ENV=build
+CONFIG=config.toml
+PORT=720
+DB_PREFIX=romiChan
+LEVEL=25
+NO_COLOR=off
+DAEMON=on

NOTE

在环境变量文件中,对于 boolean 值,使用 on 表示 true,使用 off 或其它任何值表示 false

优先级

一般地,CLI 参数 > 环境变量 > 配置文件 > 默认值

`,69)]))}const c=i(h,[["render",t]]);export{g as __pageData,c as default}; diff --git a/assets/basic_config.md.CIBSN9d7.lean.js b/assets/basic_config.md.CIBSN9d7.lean.js new file mode 100644 index 00000000..36683470 --- /dev/null +++ b/assets/basic_config.md.CIBSN9d7.lean.js @@ -0,0 +1,55 @@ +import{_ as i,c as a,a0 as l,o as n}from"./chunks/framework.P9qPzDnn.js";const g=JSON.parse('{"title":"配置详解","description":"","frontmatter":{},"headers":[],"relativePath":"basic/config.md","filePath":"basic/config.md","lastUpdated":1723345558000}'),h={name:"basic/config.md"};function t(e,s,p,k,d,r){return n(),a("div",null,s[0]||(s[0]=[l(`

配置详解

前面一节已对 kotori.toml 有了大概认识,本节内容将更为全面的介绍它。kotori.toml 是一个 Kotori 程序的核心配置文件,它一般位于 Kotori 根目录,与 package.json 文件同级,使用 TOML 格式。

文件格式

虽然默认使用的是 TOML 格式,但 Kotori 也支持 YAML 格式(.yaml.yml)与 JSON 格式,其它格式使用方法请参考下文

配置项

以下是将先前的配置片段集中在一起的例子(仅作参考请勿直接复制):

toml
[global]
+port = 720
+dbPrefix = "romiChan"
+lang = "en_US"
+commandPrefix = "/"
+noColor = false
+level = 25
+dirs = [
+  "./node_modules/@custom-scope/",
+  "./test_modules"
+]
+
+[adapter.cmd-test]
+extends = "cmd"
+master = 2_333
+nickname = "Kotarou"
+age = 18
+sex = "male"
+self-id = 720
+
+[adapter.kisaki]
+extends = "qq"
+appid = "xxxx"
+secret = "xxxxx"
+master = 2_333
+retry = 10
+
+[plugin.menu]
+alias = "cd"
+keywords = [ "菜单", "功能", "帮助" ]
+content = "菜单 | 小鳥%break%/menu - 查看BOT菜单%break%/hitokoto - 获取一条一言%break%ByHotaru"

global.lang

定义全局使用的语言,目前仅支持英语、日语、台湾语、中文四门语言。

typescript
type LocaleType = 'en_US' | 'ja_JP' | 'zh_TW' | 'zh_CN';

global.commandPrefix

定义全局使用的命令前缀。

global.dirs

定义需要加载的模块目录。

global.port

定义 Kotori 使用的端口(Http 服务器与 WebSocket 服务器)。

global.dbPrefix

定义 Kotori 使用的数据库前缀。

global.noColor

定义是否禁用彩色输出。

global.level

定义日志输出级别。

typescript
export enum LoggerLevel {
+  TRACE = 10,
+  DEBUG = 20,
+  RECORD = 25,
+  INFO = 30,
+  WARN = 40,
+  ERROR = 50,
+  FATAL = 60,
+  SILENT = 70
+}

adapter

定义 Bot。

typescript
interface AdapterConfig {
+  extends: string;
+  master?: number;
+  lang?: langType;
+  commandPrefix?: string;
+  [propName: string]?: unknown;
+}

AdapterConfig.extends

定义该 Bot 使用的适配器。

AdapterConfig.master

定义该 Bot 的最高管理员 id(即该用户在平台的 id)。

AdapterConfig.lang

定义该 Bot 使用的语言。

AdapterConfig.commandPrefix

定义该 Bot 使用的命令前缀。

AdapterConfig[propName]

除去以上由 Kotori 内部定义的配置项,extends 中指定的适配器一般会额外定义配置项用于 Bot 内部,这些配置项也可能不存在或为可选,具体请参考该模块的详情页。

plugin

定义插件的配置项。

typescript
interface PluginConfig {
+  filter?: {};
+  [propName: string]?: unknown;
+}

PluginConfig.filter

定义该插件使用的滤器。

关于滤器使用请参考 滤器

PluginConfig[propName]

类似于 AdapterConfig 中的 [propName],该插件也可能会定义一些配置项用于插件内部,具体请参考该模块的详情页。

CLI 参数

Kotori 提供了一些 CLI 参数,用于在启动时修改配置文件中的配置项。

环境变量

Kotori 提供了一些环境变量,用于在启动时修改配置文件中的配置项。

设置环境变量只需在运行根目录下创建 .env 文件,并以 KEY=VALUE 的形式写入即可,例如:

ini
NODE_ENV=build
+CONFIG=config.toml
+PORT=720
+DB_PREFIX=romiChan
+LEVEL=25
+NO_COLOR=off
+DAEMON=on

NOTE

在环境变量文件中,对于 boolean 值,使用 on 表示 true,使用 off 或其它任何值表示 false

优先级

一般地,CLI 参数 > 环境变量 > 配置文件 > 默认值

`,69)]))}const c=i(h,[["render",t]]);export{g as __pageData,c as default}; diff --git a/assets/basic_index.md.BT38T7GH.js b/assets/basic_index.md.BT38T7GH.js new file mode 100644 index 00000000..70a3f32d --- /dev/null +++ b/assets/basic_index.md.BT38T7GH.js @@ -0,0 +1 @@ +import{d as p,o,c as n,j as r,a as t,G as i,a0 as d}from"./chunks/framework.P9qPzDnn.js";import{N as u}from"./chunks/NpmBadge.BObfAVH-.js";const b=p({__name:"Voice",setup(s){function l(){new Audio("/assets/kotori.mp3").play()}return(e,a)=>(o(),n("span",null,[r("input",{type:"image",src:"https://cn.vitejs.dev/voice.svg#voice",onClick:a[0]||(a[0]=g=>l()),class:"voice",style:{"background-color":"rgb(243, 244, 245)",border:"none",padding:"3px","border-radius":"4px","vertical-align":"bottom",height:"1.5em",width:"1.5em"}})]))}}),k=JSON.parse('{"title":"简介","description":"","frontmatter":{},"headers":[],"relativePath":"basic/index.md","filePath":"basic/index.md","lastUpdated":1723293723000}'),h={name:"basic/index.md"},v=Object.assign(h,{setup(s){return(l,e)=>(o(),n("div",null,[e[10]||(e[10]=r("h1",{id:"简介",tabindex:"-1"},[t("简介 "),r("a",{class:"header-anchor",href:"#简介","aria-label":'Permalink to "简介"'},"​")],-1)),i(u,{package:"kotori-bot"}),e[11]||(e[11]=r("hr",null,null,-1)),e[12]||(e[12]=r("p",null,[t("kotori 是一个"),r("strong",null,"跨平台、解耦合、现代化"),t("于一体的聊天机器人框架,运行于 Node.js 环境,使用 TypeScript 语言开发。")],-1)),e[13]||(e[13]=r("h2",{id:"概述",tabindex:"-1"},[t("概述 "),r("a",{class:"header-anchor",href:"#概述","aria-label":'Permalink to "概述"'},"​")],-1)),r("p",null,[e[0]||(e[0]=t("「Kotori」是一个罗马字,在日语中是「ことり」(小鳥)的意思,发音为 ")),e[1]||(e[1]=r("code",null,"/kotolɪ/",-1)),e[2]||(e[2]=t()),i(b),e[3]||(e[3]=t(",该名字取自于 ")),e[4]||(e[4]=r("a",{href:"http://key.visualarts.gr.jp/",target:"_blank",rel:"noreferrer"},"Key 公式",-1)),e[5]||(e[5]=t(" 的游戏 ")),e[6]||(e[6]=r("a",{href:"https://bgm.tv/subject/4022",target:"_blank",rel:"noreferrer"},"《Rewrite》",-1)),e[7]||(e[7]=t(" 中主要女性角色之一:")),e[8]||(e[8]=r("a",{href:"https://bgm.tv/character/12063",target:"_blank",rel:"noreferrer"},"神户小鸟",-1)),e[9]||(e[9]=t(" (神戸(かんべ) 小鳥(ことり))。 借助 Kotori,可快速搭建一个多平台、功能强大的聊天机器人应用,通过安装不同模块为 Kotori 扩展功能、玩法和个性化配置等。同时,Kotori 为开发者提供了现成的 Cli 用于模块开发与 Kotori 二次开发。"))]),e[14]||(e[14]=d('

特点

扩展支持

平台

即将支持:

数据

Kotori 使用极为轻量的 LevelDb 作为数据存储。

你是?

',12))]))}});export{k as __pageData,v as default}; diff --git a/assets/basic_index.md.BT38T7GH.lean.js b/assets/basic_index.md.BT38T7GH.lean.js new file mode 100644 index 00000000..70a3f32d --- /dev/null +++ b/assets/basic_index.md.BT38T7GH.lean.js @@ -0,0 +1 @@ +import{d as p,o,c as n,j as r,a as t,G as i,a0 as d}from"./chunks/framework.P9qPzDnn.js";import{N as u}from"./chunks/NpmBadge.BObfAVH-.js";const b=p({__name:"Voice",setup(s){function l(){new Audio("/assets/kotori.mp3").play()}return(e,a)=>(o(),n("span",null,[r("input",{type:"image",src:"https://cn.vitejs.dev/voice.svg#voice",onClick:a[0]||(a[0]=g=>l()),class:"voice",style:{"background-color":"rgb(243, 244, 245)",border:"none",padding:"3px","border-radius":"4px","vertical-align":"bottom",height:"1.5em",width:"1.5em"}})]))}}),k=JSON.parse('{"title":"简介","description":"","frontmatter":{},"headers":[],"relativePath":"basic/index.md","filePath":"basic/index.md","lastUpdated":1723293723000}'),h={name:"basic/index.md"},v=Object.assign(h,{setup(s){return(l,e)=>(o(),n("div",null,[e[10]||(e[10]=r("h1",{id:"简介",tabindex:"-1"},[t("简介 "),r("a",{class:"header-anchor",href:"#简介","aria-label":'Permalink to "简介"'},"​")],-1)),i(u,{package:"kotori-bot"}),e[11]||(e[11]=r("hr",null,null,-1)),e[12]||(e[12]=r("p",null,[t("kotori 是一个"),r("strong",null,"跨平台、解耦合、现代化"),t("于一体的聊天机器人框架,运行于 Node.js 环境,使用 TypeScript 语言开发。")],-1)),e[13]||(e[13]=r("h2",{id:"概述",tabindex:"-1"},[t("概述 "),r("a",{class:"header-anchor",href:"#概述","aria-label":'Permalink to "概述"'},"​")],-1)),r("p",null,[e[0]||(e[0]=t("「Kotori」是一个罗马字,在日语中是「ことり」(小鳥)的意思,发音为 ")),e[1]||(e[1]=r("code",null,"/kotolɪ/",-1)),e[2]||(e[2]=t()),i(b),e[3]||(e[3]=t(",该名字取自于 ")),e[4]||(e[4]=r("a",{href:"http://key.visualarts.gr.jp/",target:"_blank",rel:"noreferrer"},"Key 公式",-1)),e[5]||(e[5]=t(" 的游戏 ")),e[6]||(e[6]=r("a",{href:"https://bgm.tv/subject/4022",target:"_blank",rel:"noreferrer"},"《Rewrite》",-1)),e[7]||(e[7]=t(" 中主要女性角色之一:")),e[8]||(e[8]=r("a",{href:"https://bgm.tv/character/12063",target:"_blank",rel:"noreferrer"},"神户小鸟",-1)),e[9]||(e[9]=t(" (神戸(かんべ) 小鳥(ことり))。 借助 Kotori,可快速搭建一个多平台、功能强大的聊天机器人应用,通过安装不同模块为 Kotori 扩展功能、玩法和个性化配置等。同时,Kotori 为开发者提供了现成的 Cli 用于模块开发与 Kotori 二次开发。"))]),e[14]||(e[14]=d('

特点

扩展支持

平台

即将支持:

数据

Kotori 使用极为轻量的 LevelDb 作为数据存储。

你是?

',12))]))}});export{k as __pageData,v as default}; diff --git a/assets/basic_modules.md.R6u-LxoQ.js b/assets/basic_modules.md.R6u-LxoQ.js new file mode 100644 index 00000000..685e1566 --- /dev/null +++ b/assets/basic_modules.md.R6u-LxoQ.js @@ -0,0 +1,32 @@ +import{_ as i,c as a,a0 as t,o as n}from"./chunks/framework.P9qPzDnn.js";const g=JSON.parse('{"title":"模块安装","description":"","frontmatter":{},"headers":[],"relativePath":"basic/modules.md","filePath":"basic/modules.md","lastUpdated":1723293723000}'),l={name:"basic/modules.md"};function h(e,s,k,p,o,d){return n(),a("div",null,s[0]||(s[0]=[t(`

模块安装

介绍

模块(Modules) 是 Kotori 的重要组成部分之一,通过使用模块以扩展各式各样的功能。

模块根据功能与应用范围不同,主要分为以下三大类型:

寻找模块

Kotori 模块中心 内收录了大部分 Kotori 模块。选择所需模块,在详情页中会有插件的基础信息、介绍、使用说明、配置说明等。

下载与安装

此处以「QQ 适配器服务模块」(@kotori-bot/kotori-plugin-adapter-qq)为例。

模块的包名除去 @xxx/ 的部分(如果有),会有一段相似的开始字段,将其称之为「模块前缀」。通过模块前缀可判断模块类型,如「kotori-plugin-adapter-xxx」表示适配器,「kotori-plugin-database-xxx」表示数据库服务,「kotori-plugin-xxx」表示插件自定义服务,详细内容请参考 开发文档 - 插件范式

使用包管理工具

复制模块详情页里中的安装指令,或手动输入对应模块的 npm 包名,在 Kotori 根目录运行:

bash
npm install @kotori-bot/kotori-plugin-adapter-qq

手动下载安装

WARNING

该方法目前仅建议插件开发者在工作区下可适当使用。

在模块详情页里跳转至对应的 npm 地址或 GitHub 地址,下载模块的构建产物。 解压压缩包并移动至 Kotori 根目录下的 ./modules/ 内。

GitHub 仓库中存有模块的源码,在当前阶段,你应下载并使用模块的构建产物而非源码

务必确保解压后的模块文件夹仅有一层文件夹而非多层,否则将无法识别与加载模块。

添加加载目录

模块安装在 ./modules 目录内请忽略该步骤

通过包管理工具安装的模块一般会安装在 Kotori 根目录下的 ./node_modules/ 内,如若插件包名带有 @xxx/ 的前缀,表示为包的命名空间,上述示例模块中的「@kotori-bot/」为 Kotori 官方包的命名空间,表示官方模块,其余的命名空间或无命名空间的模块为社区模块

所有未安装在 ./modules/ 都应配置 kotori.ymlglobal.dirs 项以设置额外的加载根目录,但对于 ./node_modules/@kotori-bot/ 命名空间已经存在于 Kotori.yml 默认配置中,因此无需担心。

对于其它安装目录或命名空间则需手动添加到 Kotori.yml 中,如:

对应配置为:

toml
[global]:
+dirs = [
+  "./node_modules/",
+  "./node_modules/@kotori-bot/",
+  # 上面为默认配置的加载目录
+  "./node_modules/@custom-scope/",
+  "- ./test_modules"
+]

配置模块

根据安装的模块类型不同,配置策略也将不同。

插件

插件配置数据应写在 kotori.ymlplugin.<plugin-name> 项下,其中 <plugin-name> 为插件名字,不应含有包的命名空间与模块前缀,值必须是一个对象。插件的配置项由插件本身提供与指定,并非所有插件本身都会提供配置项。一般地,有提供配置项的插件内都会有一套默认配置,因此不配置也可以正常运行插件。插件的配置和说明可参考该插件的详情页,此处以 「菜单插件」(@kotori-bot/kotori-plugin-menu)为例,在详情页查看配置说明后在 kotori.yml 中配置相关内容:

toml
[plugin.menu]
+alias = "cd"
+keywords = [ "菜单", "功能", "帮助" ]
+content = "菜单 | 小鳥%break%/menu - 查看BOT菜单%break%/hitokoto - 获取一条一言%break%ByHotaru"

适配器

适配器配置数据应写在 kotori.ymladapter[instanceName] 项下,其中 instanceName 为适配器实例(以下简称「Bot」)名字应由小写英语字母、数字、连字符([a-z0-9])组成,值必须是一个对象。适配器的配置数据不会作用于适配器模块,Kotori 会根据配置数据创建对应 Bot。对于适配器的配置,必须提供一些必要配置项才能确保实例的正常运行,其中有部分配置项由 Kotori 内部定义,如:

toml
[adapter.cmd-test]
+extends = "cmd"
+master = 2_333

「cmd-test」是该 Bot 的名字也是在 kotori 程序运行中的唯一标识符,不可重复。extends 用于指定该实例使用的适配器,值为适配器模块的包名除去命名空间与适配器服务前缀的字符串,如:使用「@kotori-bot/kotori-plugin-adapter-qq」适配器,则应填入「qq」。master 用于指定该实例的最高管理员(Admin),值类型可为数字或字符串,非必填。 除去由 Kotori 内部定义的配置项以外,一般还需要填入该适配器要求传入的必要配置项。

toml
[adapter.cmd-test]
+extends = "cmd"
+master = 2_333
+nickname = "Kotarou"
+age = 18
+sex = "male"
+self-id = 720

cmd 适配器即「@kotori-bot/kotori-plugin-adapter-cmd」,属于 kotori 预装模块之一,为 kotori 程序当前所在控制台提供聊天交互功能,也是最方便的测试机器人的场所(但并不推荐,因为只支持文字交互,模块开发有更好的测试场所选择,详细内容请参考 开发文档 - 项目构建

不过此处使用的 cmd 适配器定义的配置项均有默认值因此为可选。接着使用「@kotori-bot/kotori-plugin-adapter-qq」适配器再创建一个 Bot:

toml
[adapter.cmd-test]
+extends = "cmd"
+master = 2_333
+nickname = "Kotarou"
+age = 18
+sex = "male"
+self-id = 720
+
+[adapter.kisaki]
+extends = "qq"
+appid = "xxxx"
+secret = "xxxxx"
+master = 2_333
+retry = 10

查看 QQ 适配器的详情页面可知,appidsecret 为其定义的必要配置项,retry 为其定义的可选配置项,关于 QQ 适配器的具体使用与配置项含义请查看其插件详情页。

关于配置文件的详细介绍请参考 配置详解

`,41)]))}const c=i(l,[["render",h]]);export{g as __pageData,c as default}; diff --git a/assets/basic_modules.md.R6u-LxoQ.lean.js b/assets/basic_modules.md.R6u-LxoQ.lean.js new file mode 100644 index 00000000..685e1566 --- /dev/null +++ b/assets/basic_modules.md.R6u-LxoQ.lean.js @@ -0,0 +1,32 @@ +import{_ as i,c as a,a0 as t,o as n}from"./chunks/framework.P9qPzDnn.js";const g=JSON.parse('{"title":"模块安装","description":"","frontmatter":{},"headers":[],"relativePath":"basic/modules.md","filePath":"basic/modules.md","lastUpdated":1723293723000}'),l={name:"basic/modules.md"};function h(e,s,k,p,o,d){return n(),a("div",null,s[0]||(s[0]=[t(`

模块安装

介绍

模块(Modules) 是 Kotori 的重要组成部分之一,通过使用模块以扩展各式各样的功能。

模块根据功能与应用范围不同,主要分为以下三大类型:

寻找模块

Kotori 模块中心 内收录了大部分 Kotori 模块。选择所需模块,在详情页中会有插件的基础信息、介绍、使用说明、配置说明等。

下载与安装

此处以「QQ 适配器服务模块」(@kotori-bot/kotori-plugin-adapter-qq)为例。

模块的包名除去 @xxx/ 的部分(如果有),会有一段相似的开始字段,将其称之为「模块前缀」。通过模块前缀可判断模块类型,如「kotori-plugin-adapter-xxx」表示适配器,「kotori-plugin-database-xxx」表示数据库服务,「kotori-plugin-xxx」表示插件自定义服务,详细内容请参考 开发文档 - 插件范式

使用包管理工具

复制模块详情页里中的安装指令,或手动输入对应模块的 npm 包名,在 Kotori 根目录运行:

bash
npm install @kotori-bot/kotori-plugin-adapter-qq

手动下载安装

WARNING

该方法目前仅建议插件开发者在工作区下可适当使用。

在模块详情页里跳转至对应的 npm 地址或 GitHub 地址,下载模块的构建产物。 解压压缩包并移动至 Kotori 根目录下的 ./modules/ 内。

GitHub 仓库中存有模块的源码,在当前阶段,你应下载并使用模块的构建产物而非源码

务必确保解压后的模块文件夹仅有一层文件夹而非多层,否则将无法识别与加载模块。

添加加载目录

模块安装在 ./modules 目录内请忽略该步骤

通过包管理工具安装的模块一般会安装在 Kotori 根目录下的 ./node_modules/ 内,如若插件包名带有 @xxx/ 的前缀,表示为包的命名空间,上述示例模块中的「@kotori-bot/」为 Kotori 官方包的命名空间,表示官方模块,其余的命名空间或无命名空间的模块为社区模块

所有未安装在 ./modules/ 都应配置 kotori.ymlglobal.dirs 项以设置额外的加载根目录,但对于 ./node_modules/@kotori-bot/ 命名空间已经存在于 Kotori.yml 默认配置中,因此无需担心。

对于其它安装目录或命名空间则需手动添加到 Kotori.yml 中,如:

对应配置为:

toml
[global]:
+dirs = [
+  "./node_modules/",
+  "./node_modules/@kotori-bot/",
+  # 上面为默认配置的加载目录
+  "./node_modules/@custom-scope/",
+  "- ./test_modules"
+]

配置模块

根据安装的模块类型不同,配置策略也将不同。

插件

插件配置数据应写在 kotori.ymlplugin.<plugin-name> 项下,其中 <plugin-name> 为插件名字,不应含有包的命名空间与模块前缀,值必须是一个对象。插件的配置项由插件本身提供与指定,并非所有插件本身都会提供配置项。一般地,有提供配置项的插件内都会有一套默认配置,因此不配置也可以正常运行插件。插件的配置和说明可参考该插件的详情页,此处以 「菜单插件」(@kotori-bot/kotori-plugin-menu)为例,在详情页查看配置说明后在 kotori.yml 中配置相关内容:

toml
[plugin.menu]
+alias = "cd"
+keywords = [ "菜单", "功能", "帮助" ]
+content = "菜单 | 小鳥%break%/menu - 查看BOT菜单%break%/hitokoto - 获取一条一言%break%ByHotaru"

适配器

适配器配置数据应写在 kotori.ymladapter[instanceName] 项下,其中 instanceName 为适配器实例(以下简称「Bot」)名字应由小写英语字母、数字、连字符([a-z0-9])组成,值必须是一个对象。适配器的配置数据不会作用于适配器模块,Kotori 会根据配置数据创建对应 Bot。对于适配器的配置,必须提供一些必要配置项才能确保实例的正常运行,其中有部分配置项由 Kotori 内部定义,如:

toml
[adapter.cmd-test]
+extends = "cmd"
+master = 2_333

「cmd-test」是该 Bot 的名字也是在 kotori 程序运行中的唯一标识符,不可重复。extends 用于指定该实例使用的适配器,值为适配器模块的包名除去命名空间与适配器服务前缀的字符串,如:使用「@kotori-bot/kotori-plugin-adapter-qq」适配器,则应填入「qq」。master 用于指定该实例的最高管理员(Admin),值类型可为数字或字符串,非必填。 除去由 Kotori 内部定义的配置项以外,一般还需要填入该适配器要求传入的必要配置项。

toml
[adapter.cmd-test]
+extends = "cmd"
+master = 2_333
+nickname = "Kotarou"
+age = 18
+sex = "male"
+self-id = 720

cmd 适配器即「@kotori-bot/kotori-plugin-adapter-cmd」,属于 kotori 预装模块之一,为 kotori 程序当前所在控制台提供聊天交互功能,也是最方便的测试机器人的场所(但并不推荐,因为只支持文字交互,模块开发有更好的测试场所选择,详细内容请参考 开发文档 - 项目构建

不过此处使用的 cmd 适配器定义的配置项均有默认值因此为可选。接着使用「@kotori-bot/kotori-plugin-adapter-qq」适配器再创建一个 Bot:

toml
[adapter.cmd-test]
+extends = "cmd"
+master = 2_333
+nickname = "Kotarou"
+age = 18
+sex = "male"
+self-id = 720
+
+[adapter.kisaki]
+extends = "qq"
+appid = "xxxx"
+secret = "xxxxx"
+master = 2_333
+retry = 10

查看 QQ 适配器的详情页面可知,appidsecret 为其定义的必要配置项,retry 为其定义的可选配置项,关于 QQ 适配器的具体使用与配置项含义请查看其插件详情页。

关于配置文件的详细介绍请参考 配置详解

`,41)]))}const c=i(l,[["render",h]]);export{g as __pageData,c as default}; diff --git a/assets/basic_start.md.UB_T-Yyu.js b/assets/basic_start.md.UB_T-Yyu.js new file mode 100644 index 00000000..65ccf67b --- /dev/null +++ b/assets/basic_start.md.UB_T-Yyu.js @@ -0,0 +1,17 @@ +import{_ as i,c as a,a0 as t,o as e}from"./chunks/framework.P9qPzDnn.js";const c=JSON.parse('{"title":"快速开始","description":"","frontmatter":{},"headers":[],"relativePath":"basic/start.md","filePath":"basic/start.md","lastUpdated":1723345558000}'),h={name:"basic/start.md"};function n(l,s,p,k,d,r){return e(),a("div",null,s[0]||(s[0]=[t(`

快速开始

一键使用仓库

尽管 Kotori 的安装几乎已可以说是开箱即用的地步,只需要简单动用一下包管理器同时写入配置文件即可完成安装,但鉴于这仅仅安装了本体,并未附带一些基础且必要的模块,因此这里有一份来自于社区的 Kotori 手把手安装教程,该仓库提供了一个包含基础模块的 package.json 与 Kotori 配置文件以及启动脚本。

NOTE

如若视频加载不出来请考虑使用 VPN,当然这并不重要。

下载该仓库

bash
git clone https://github.com/kotorijs/kotori-app.git

下载地址

安装依赖

1.安装 Node.js

2.安装项目依赖

支持 npmyarn 主流包管理器安装,pnpm 安装后可能存在启动问题,cnpmdenobun 未经测试,但理论仍可以进行安装并运行。先进入仓库根目录,输入以下任一命令安装:

bash
npm install

如若出现安装失败问题则强制安装:

bash
npm install --force

强制更新所有包:

bash
npm update --force

配置 kotori.toml

关于 kotori.toml 的详细介绍请参考 配置详解

正式启动

在仓库根目录打开命令行输入:

bash
npm exec kotori

当然,也可以直接使用仓库根目录下提供的两个启动文件:

启动完成后,在控制台内输入 /status 查看,输入 /help 查看帮助内容

进入 Webui

启动 Kotori 后,会在控制台看到以下类似信息:

log
7/19 23:59:59 LOG (1372799) : Http server started at http://127.0.0.1:720
+7/20 0:0:0 LOG (1372799) : WebSocket server started at ws://127.0.0.1:720
+KotoriO > 当前未设置 Webui 账号与密码,请输入 /webui 指令以进行初始化

此处的 http://127.0.0.1:720 即为 Kotori 网页控制台,第一次启动会提示设置用户名与密码,通过输入 /webui 命令进行设置:

bash
/webui
+KotoriO > 请输入 Webui 用户名:
+Admin
+KotoriO > 请输入 Webui 密码:
+kotori666
+8/11 10:37:5 LOG (1435470) [cmd/cmd-test]: User 2333 exec command webui successfully
+KotoriO > 配置成功!请再次输入指令以查看运行状态
+/webui
+8/11 10:47:30 LOG (1435470) [cmd/cmd-test]: User 2333 exec command webui successfully
+KotoriO > Webui 服务已启动!运行端口:720

当想重置用户名或密码时也可以输入以下指令:

bash
/webui -R
+8/11 10:35:51 LOG (1372799) [cmd/cmd-test]: User 2333 exec command webui -R successfully
+KotoriO > Webui 账户数据已重置

安装适配器与插件

此处收录了大部分的 Kotori 模块,选择自己喜欢的模块打开详情页查看说明,复制包名使用你的包管理器进行安装。以下指令会为你安装一些重要的基础插件:

更新 Kotori 与模块

一般地,在需要更新时使用以下命令即可进行更新:

bash
npm update -f

此外,当变动较大时可重新重复上述安装步骤。

使用 GUI

Kotori 内置了一个简易的交互式命令行功能,用于执行简单的操作,在根目录下输入以下命令:

bash
npm exec kotori ui

ui1

gui2

启用守护进程

程序运行中可能总会发生一些不会如用户所愿的错误和异常,为了避免这种情况,Kotori 提供了守护进程功能,当程序崩溃时,守护进程会自动重启程序,而在默认情况下(即程序处于 build 模式),守护进程是会自动启用的,想改变这一策略可更改环境变量或 CLI 参数,具体参见 配置详解

bash
24/8/11 10:35:56 INFO (1372340) : [Daemon] Starting...

当 Kotori 启动时出现以上消息则表明守护进程成功启用。同时在启用守护进程后,也可以通过指令 /restart(来自「@kotori-bot/kotori-plugin-core」) 手动进行重启:

bash
/restart
+KotoriO > Kotori 正在重启中...
+8/11 10:35:56 LOG (1372799) [cmd/cmd-test]: User 2333 exec command restart successfully
+24/8/11 10:35:56 WARN (1372340) : [Daemon] Restarting...

当然,守护进程在程序崩溃时也有一套判定规则,如若程序在短时间内多次崩溃,则视为存在重大异常问题,此时便会停止自动重启需要人工进行排查原因。。

从源码构建

请参考 作为依赖与二次开发

其它方式

N.A

`,57)]))}const g=i(h,[["render",n]]);export{c as __pageData,g as default}; diff --git a/assets/basic_start.md.UB_T-Yyu.lean.js b/assets/basic_start.md.UB_T-Yyu.lean.js new file mode 100644 index 00000000..65ccf67b --- /dev/null +++ b/assets/basic_start.md.UB_T-Yyu.lean.js @@ -0,0 +1,17 @@ +import{_ as i,c as a,a0 as t,o as e}from"./chunks/framework.P9qPzDnn.js";const c=JSON.parse('{"title":"快速开始","description":"","frontmatter":{},"headers":[],"relativePath":"basic/start.md","filePath":"basic/start.md","lastUpdated":1723345558000}'),h={name:"basic/start.md"};function n(l,s,p,k,d,r){return e(),a("div",null,s[0]||(s[0]=[t(`

快速开始

一键使用仓库

尽管 Kotori 的安装几乎已可以说是开箱即用的地步,只需要简单动用一下包管理器同时写入配置文件即可完成安装,但鉴于这仅仅安装了本体,并未附带一些基础且必要的模块,因此这里有一份来自于社区的 Kotori 手把手安装教程,该仓库提供了一个包含基础模块的 package.json 与 Kotori 配置文件以及启动脚本。

NOTE

如若视频加载不出来请考虑使用 VPN,当然这并不重要。

下载该仓库

bash
git clone https://github.com/kotorijs/kotori-app.git

下载地址

安装依赖

1.安装 Node.js

2.安装项目依赖

支持 npmyarn 主流包管理器安装,pnpm 安装后可能存在启动问题,cnpmdenobun 未经测试,但理论仍可以进行安装并运行。先进入仓库根目录,输入以下任一命令安装:

bash
npm install

如若出现安装失败问题则强制安装:

bash
npm install --force

强制更新所有包:

bash
npm update --force

配置 kotori.toml

关于 kotori.toml 的详细介绍请参考 配置详解

正式启动

在仓库根目录打开命令行输入:

bash
npm exec kotori

当然,也可以直接使用仓库根目录下提供的两个启动文件:

启动完成后,在控制台内输入 /status 查看,输入 /help 查看帮助内容

进入 Webui

启动 Kotori 后,会在控制台看到以下类似信息:

log
7/19 23:59:59 LOG (1372799) : Http server started at http://127.0.0.1:720
+7/20 0:0:0 LOG (1372799) : WebSocket server started at ws://127.0.0.1:720
+KotoriO > 当前未设置 Webui 账号与密码,请输入 /webui 指令以进行初始化

此处的 http://127.0.0.1:720 即为 Kotori 网页控制台,第一次启动会提示设置用户名与密码,通过输入 /webui 命令进行设置:

bash
/webui
+KotoriO > 请输入 Webui 用户名:
+Admin
+KotoriO > 请输入 Webui 密码:
+kotori666
+8/11 10:37:5 LOG (1435470) [cmd/cmd-test]: User 2333 exec command webui successfully
+KotoriO > 配置成功!请再次输入指令以查看运行状态
+/webui
+8/11 10:47:30 LOG (1435470) [cmd/cmd-test]: User 2333 exec command webui successfully
+KotoriO > Webui 服务已启动!运行端口:720

当想重置用户名或密码时也可以输入以下指令:

bash
/webui -R
+8/11 10:35:51 LOG (1372799) [cmd/cmd-test]: User 2333 exec command webui -R successfully
+KotoriO > Webui 账户数据已重置

安装适配器与插件

此处收录了大部分的 Kotori 模块,选择自己喜欢的模块打开详情页查看说明,复制包名使用你的包管理器进行安装。以下指令会为你安装一些重要的基础插件:

更新 Kotori 与模块

一般地,在需要更新时使用以下命令即可进行更新:

bash
npm update -f

此外,当变动较大时可重新重复上述安装步骤。

使用 GUI

Kotori 内置了一个简易的交互式命令行功能,用于执行简单的操作,在根目录下输入以下命令:

bash
npm exec kotori ui

ui1

gui2

启用守护进程

程序运行中可能总会发生一些不会如用户所愿的错误和异常,为了避免这种情况,Kotori 提供了守护进程功能,当程序崩溃时,守护进程会自动重启程序,而在默认情况下(即程序处于 build 模式),守护进程是会自动启用的,想改变这一策略可更改环境变量或 CLI 参数,具体参见 配置详解

bash
24/8/11 10:35:56 INFO (1372340) : [Daemon] Starting...

当 Kotori 启动时出现以上消息则表明守护进程成功启用。同时在启用守护进程后,也可以通过指令 /restart(来自「@kotori-bot/kotori-plugin-core」) 手动进行重启:

bash
/restart
+KotoriO > Kotori 正在重启中...
+8/11 10:35:56 LOG (1372799) [cmd/cmd-test]: User 2333 exec command restart successfully
+24/8/11 10:35:56 WARN (1372340) : [Daemon] Restarting...

当然,守护进程在程序崩溃时也有一套判定规则,如若程序在短时间内多次崩溃,则视为存在重大异常问题,此时便会停止自动重启需要人工进行排查原因。。

从源码构建

请参考 作为依赖与二次开发

其它方式

N.A

`,57)]))}const g=i(h,[["render",n]]);export{c as __pageData,g as default}; diff --git a/assets/basic_usage.md.ggWDU75P.js b/assets/basic_usage.md.ggWDU75P.js new file mode 100644 index 00000000..0d117ffa --- /dev/null +++ b/assets/basic_usage.md.ggWDU75P.js @@ -0,0 +1,45 @@ +import{_ as s,c as a,a0 as l,o as e}from"./chunks/framework.P9qPzDnn.js";const c=JSON.parse('{"title":"立即使用","description":"","frontmatter":{},"headers":[],"relativePath":"basic/usage.md","filePath":"basic/usage.md","lastUpdated":1723293723000}'),t={name:"basic/usage.md"};function n(o,i,h,p,k,r){return e(),a("div",null,i[0]||(i[0]=[l(`

立即使用

TIP

本篇适用于想立即体验基于 Kotori 搭建的 Bot 实际效果、或单纯想使用由官方提供的各平台 Bot 服务的平台用户。


服务平台

目前共提供 2 个平台、四个 Bot:

QQ

Kanbe Kotori(小鳥一号)

小鳥二号

基于 OneBot11 标准的第三方 QQ 机器人(@Kotori-bot/kotori-plugin-adapter-onebot)。

立即使用:Kotori 交流群

小鳥三号

基于 Tencent 官方 API(@Kotori-bot/kotori-plugin-adapter-qq)。相比于第三方 QQ 机器人,限制较多(不可发送主动消息、URL 需备案等),其余功能与「小鳥二号」基本一致。

立即使用:Kotori 交流群

Misakura

基于 OneBot11 标准的第三方 QQ 机器人。

立即使用:Kotori 交流群

Telegram

Sena

基于 @kotori-bot/adapter-telegram 的 Telegram 机器人。

立即使用:@Sena0620Bot

Discord

Slack

功能一览

WARNING

以下内容已严重过期

以下展示并非全部功能。

@kotori-bot/kotori-plugin-core

@kotori-bot/kotori-plugin-help

@kotori-bot/kotori-plugin-menu

/menu 查看 BOT 菜单 别名:cd

kotori-plugin-random-img

kotori-plugin-bangumi

bash
/bgm 素晴日
+> 原名:素晴らしき日々~不連続存在~公式ビジュアルアーカイヴ
+中文名:素晴之日 不连续的存在 Official Visual Archive
+介绍:人気アダルトゲームブランド「ケロQ」から、実に6年ぶりに発売された新作『素晴らしき日々 ~不連続存在~』。その魅力をギュッと閉じ込めたファン必携の一冊。描き下ろしイラスト&原作を担当したSCA-自(すかぢ)氏の新作書き下ろしテキスト満載でお届け。
+标签:素晴らしき日々 设定集 电波 神作 素晴日 公式书 2010 百合 FanBook 悬疑 画集 画集・設定資料集 推理 VFB
+详情:https://bgm.tv/subject/8318
+[image]

kotori-plugin-bilibili

kotori-plugin-hitokoto

bash
/hitokoto
+> 如果人们不相信数学简单,那是因为他们没有意识到人生有多复杂。——冯诺依曼
+类型:俗语

kotori-plugin-mediawiki

bash
/wiki 月社妃
+> 标题:月社妃
+内容:月社妃(日语:月社(つきやしろ) 妃(きさき))是由ウグイスカグラ所制作的18禁galgame《纸上的魔法使》及其衍生作品的登场角色。是主人公四条琉璃的同胞妹妹。
+https://mzh.moegirl.org.cn/.php?curid=384932
+来源:萌娘百科

kotori-plugin-github

bash
/github kotorijs/kotori
+> 地址:kotorijs/kotori
+描述:Cross platform, decoupled, and modernized ChatBot framework base on NodeJS
+语言:TypeScript
+所有者:kotorijs
+创建时间:
+2023-06-14T11:45:16Z
+最后更新时间:2023-12-31T15:28:10Z
+最后推送时间:2024-01-14T09:48:13Z
+开源协议:GNU General Public License v3.0

kotori-plugin-music

bash
/music 夢水の調べ
+> 歌曲ID:2077744375
+歌曲标题:夢水の調べ
+歌曲作者:おはる
+歌曲下载:http://music.163.com/song/media/outer/url?id=2077744375.mp3
+歌曲封面:[image]

kotori-plugin-weather

bash
/weather 北京
+> 城市:北京市
+日期:周四
+温度:-6~4℃
+天气:晴
+风度:南风-2级
+空气质量:良
+
+日期:周五
+温度:-5~0℃
+天气:阴
+风度:东风-1级
+空气质量:良
+
+日期:周六
+温度:-8~0℃
+天气:阴
+风度:东北风-1级
+空气质量:良
`,53)]))}const g=s(t,[["render",n]]);export{c as __pageData,g as default}; diff --git a/assets/basic_usage.md.ggWDU75P.lean.js b/assets/basic_usage.md.ggWDU75P.lean.js new file mode 100644 index 00000000..0d117ffa --- /dev/null +++ b/assets/basic_usage.md.ggWDU75P.lean.js @@ -0,0 +1,45 @@ +import{_ as s,c as a,a0 as l,o as e}from"./chunks/framework.P9qPzDnn.js";const c=JSON.parse('{"title":"立即使用","description":"","frontmatter":{},"headers":[],"relativePath":"basic/usage.md","filePath":"basic/usage.md","lastUpdated":1723293723000}'),t={name:"basic/usage.md"};function n(o,i,h,p,k,r){return e(),a("div",null,i[0]||(i[0]=[l(`

立即使用

TIP

本篇适用于想立即体验基于 Kotori 搭建的 Bot 实际效果、或单纯想使用由官方提供的各平台 Bot 服务的平台用户。


服务平台

目前共提供 2 个平台、四个 Bot:

QQ

Kanbe Kotori(小鳥一号)

小鳥二号

基于 OneBot11 标准的第三方 QQ 机器人(@Kotori-bot/kotori-plugin-adapter-onebot)。

立即使用:Kotori 交流群

小鳥三号

基于 Tencent 官方 API(@Kotori-bot/kotori-plugin-adapter-qq)。相比于第三方 QQ 机器人,限制较多(不可发送主动消息、URL 需备案等),其余功能与「小鳥二号」基本一致。

立即使用:Kotori 交流群

Misakura

基于 OneBot11 标准的第三方 QQ 机器人。

立即使用:Kotori 交流群

Telegram

Sena

基于 @kotori-bot/adapter-telegram 的 Telegram 机器人。

立即使用:@Sena0620Bot

Discord

Slack

功能一览

WARNING

以下内容已严重过期

以下展示并非全部功能。

@kotori-bot/kotori-plugin-core

@kotori-bot/kotori-plugin-help

@kotori-bot/kotori-plugin-menu

/menu 查看 BOT 菜单 别名:cd

kotori-plugin-random-img

kotori-plugin-bangumi

bash
/bgm 素晴日
+> 原名:素晴らしき日々~不連続存在~公式ビジュアルアーカイヴ
+中文名:素晴之日 不连续的存在 Official Visual Archive
+介绍:人気アダルトゲームブランド「ケロQ」から、実に6年ぶりに発売された新作『素晴らしき日々 ~不連続存在~』。その魅力をギュッと閉じ込めたファン必携の一冊。描き下ろしイラスト&原作を担当したSCA-自(すかぢ)氏の新作書き下ろしテキスト満載でお届け。
+标签:素晴らしき日々 设定集 电波 神作 素晴日 公式书 2010 百合 FanBook 悬疑 画集 画集・設定資料集 推理 VFB
+详情:https://bgm.tv/subject/8318
+[image]

kotori-plugin-bilibili

kotori-plugin-hitokoto

bash
/hitokoto
+> 如果人们不相信数学简单,那是因为他们没有意识到人生有多复杂。——冯诺依曼
+类型:俗语

kotori-plugin-mediawiki

bash
/wiki 月社妃
+> 标题:月社妃
+内容:月社妃(日语:月社(つきやしろ) 妃(きさき))是由ウグイスカグラ所制作的18禁galgame《纸上的魔法使》及其衍生作品的登场角色。是主人公四条琉璃的同胞妹妹。
+https://mzh.moegirl.org.cn/.php?curid=384932
+来源:萌娘百科

kotori-plugin-github

bash
/github kotorijs/kotori
+> 地址:kotorijs/kotori
+描述:Cross platform, decoupled, and modernized ChatBot framework base on NodeJS
+语言:TypeScript
+所有者:kotorijs
+创建时间:
+2023-06-14T11:45:16Z
+最后更新时间:2023-12-31T15:28:10Z
+最后推送时间:2024-01-14T09:48:13Z
+开源协议:GNU General Public License v3.0

kotori-plugin-music

bash
/music 夢水の調べ
+> 歌曲ID:2077744375
+歌曲标题:夢水の調べ
+歌曲作者:おはる
+歌曲下载:http://music.163.com/song/media/outer/url?id=2077744375.mp3
+歌曲封面:[image]

kotori-plugin-weather

bash
/weather 北京
+> 城市:北京市
+日期:周四
+温度:-6~4℃
+天气:晴
+风度:南风-2级
+空气质量:良
+
+日期:周五
+温度:-5~0℃
+天气:阴
+风度:东风-1级
+空气质量:良
+
+日期:周六
+温度:-8~0℃
+天气:阴
+风度:东北风-1级
+空气质量:良
`,53)]))}const g=s(t,[["render",n]]);export{c as __pageData,g as default}; diff --git a/assets/chunks/NpmBadge.BObfAVH-.js b/assets/chunks/NpmBadge.BObfAVH-.js new file mode 100644 index 00000000..b75fac00 --- /dev/null +++ b/assets/chunks/NpmBadge.BObfAVH-.js @@ -0,0 +1 @@ +import{d as r,h as t,o,c as p,j as d,_ as g}from"./framework.P9qPzDnn.js";const l=["href","title"],i=["src","alt"],m=r({__name:"NpmBadge",props:{package:{type:String,required:!0},distTag:{type:String,required:!1,default:"latest"}},setup(a){const e=a,n=t(()=>`https://www.npmjs.com/package/${e.package}`),s=t(()=>e.distTag?`${e.package}@${e.distTag}`:e.package),c=t(()=>`https://badgen.net/npm/v/${e.package}/${e.distTag}?label=${encodeURIComponent(s.value)}`);return(u,k)=>(o(),p("a",{class:"npm-badge",href:n.value,title:a.package,target:"_blank",rel:"noopener noreferrer"},[d("img",{src:c.value,alt:a.package},null,8,i)],8,l))}}),f=g(m,[["__scopeId","data-v-8bf061d3"]]);export{f as N}; diff --git a/assets/chunks/framework.P9qPzDnn.js b/assets/chunks/framework.P9qPzDnn.js new file mode 100644 index 00000000..307ad28a --- /dev/null +++ b/assets/chunks/framework.P9qPzDnn.js @@ -0,0 +1,18 @@ +/** +* @vue/shared v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**//*! #__NO_SIDE_EFFECTS__ */function Es(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const te={},St=[],Ue=()=>{},wo=()=>!1,Xt=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Ts=e=>e.startsWith("onUpdate:"),ce=Object.assign,Cs=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},So=Object.prototype.hasOwnProperty,z=(e,t)=>So.call(e,t),B=Array.isArray,xt=e=>En(e)==="[object Map]",Hr=e=>En(e)==="[object Set]",q=e=>typeof e=="function",re=e=>typeof e=="string",Ge=e=>typeof e=="symbol",ne=e=>e!==null&&typeof e=="object",$r=e=>(ne(e)||q(e))&&q(e.then)&&q(e.catch),Dr=Object.prototype.toString,En=e=>Dr.call(e),xo=e=>En(e).slice(8,-1),jr=e=>En(e)==="[object Object]",As=e=>re(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Et=Es(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Tn=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},Eo=/-(\w)/g,Le=Tn(e=>e.replace(Eo,(t,n)=>n?n.toUpperCase():"")),To=/\B([A-Z])/g,st=Tn(e=>e.replace(To,"-$1").toLowerCase()),Cn=Tn(e=>e.charAt(0).toUpperCase()+e.slice(1)),dn=Tn(e=>e?`on${Cn(e)}`:""),et=(e,t)=>!Object.is(e,t),kn=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},Co=e=>{const t=parseFloat(e);return isNaN(t)?e:t},Ao=e=>{const t=re(e)?Number(e):NaN;return isNaN(t)?e:t};let Gs;const An=()=>Gs||(Gs=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Rs(e){if(B(e)){const t={};for(let n=0;n{if(n){const s=n.split(Oo);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function Os(e){let t="";if(re(e))t=e;else if(B(e))for(let n=0;n!!(e&&e.__v_isRef===!0),No=e=>re(e)?e:e==null?"":B(e)||ne(e)&&(e.toString===Dr||!q(e.toString))?Br(e)?No(e.value):JSON.stringify(e,kr,2):String(e),kr=(e,t)=>Br(t)?kr(e,t.value):xt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],i)=>(n[Wn(s,i)+" =>"]=r,n),{})}:Hr(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>Wn(n))}:Ge(t)?Wn(t):ne(t)&&!B(t)&&!jr(t)?String(t):t,Wn=(e,t="")=>{var n;return Ge(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let be;class Fo{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=be,!t&&be&&(this.index=(be.scopes||(be.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t0)return;if(Ht){let t=Ht;for(Ht=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;Ft;){let t=Ft;for(Ft=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function Yr(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Xr(e){let t,n=e.depsTail,s=n;for(;s;){const r=s.prevDep;s.version===-1?(s===n&&(n=r),Ls(s),$o(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=r}e.deps=t,e.depsTail=n}function fs(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Jr(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Jr(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Vt))return;e.globalVersion=Vt;const t=e.dep;if(e.flags|=2,t.version>0&&!e.isSSR&&e.deps&&!fs(e)){e.flags&=-3;return}const n=ee,s=Ne;ee=e,Ne=!0;try{Yr(e);const r=e.fn(e._value);(t.version===0||et(r,e._value))&&(e._value=r,t.version++)}catch(r){throw t.version++,r}finally{ee=n,Ne=s,Xr(e),e.flags&=-3}}function Ls(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let i=n.computed.deps;i;i=i.nextDep)Ls(i,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function $o(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let Ne=!0;const zr=[];function rt(){zr.push(Ne),Ne=!1}function it(){const e=zr.pop();Ne=e===void 0?!0:e}function Ys(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=ee;ee=void 0;try{t()}finally{ee=n}}}let Vt=0;class Do{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Rn{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0}track(t){if(!ee||!Ne||ee===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==ee)n=this.activeLink=new Do(ee,this),ee.deps?(n.prevDep=ee.depsTail,ee.depsTail.nextDep=n,ee.depsTail=n):ee.deps=ee.depsTail=n,Qr(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=ee.depsTail,n.nextDep=void 0,ee.depsTail.nextDep=n,ee.depsTail=n,ee.deps===n&&(ee.deps=s)}return n}trigger(t){this.version++,Vt++,this.notify(t)}notify(t){Ms();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{Is()}}}function Qr(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)Qr(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const yn=new WeakMap,dt=Symbol(""),us=Symbol(""),Ut=Symbol("");function ge(e,t,n){if(Ne&&ee){let s=yn.get(e);s||yn.set(e,s=new Map);let r=s.get(n);r||(s.set(n,r=new Rn),r.map=s,r.key=n),r.track()}}function Ke(e,t,n,s,r,i){const o=yn.get(e);if(!o){Vt++;return}const l=c=>{c&&c.trigger()};if(Ms(),t==="clear")o.forEach(l);else{const c=B(e),u=c&&As(n);if(c&&n==="length"){const a=Number(s);o.forEach((h,v)=>{(v==="length"||v===Ut||!Ge(v)&&v>=a)&&l(h)})}else switch((n!==void 0||o.has(void 0))&&l(o.get(n)),u&&l(o.get(Ut)),t){case"add":c?u&&l(o.get("length")):(l(o.get(dt)),xt(e)&&l(o.get(us)));break;case"delete":c||(l(o.get(dt)),xt(e)&&l(o.get(us)));break;case"set":xt(e)&&l(o.get(dt));break}}Is()}function jo(e,t){const n=yn.get(e);return n&&n.get(t)}function _t(e){const t=J(e);return t===e?t:(ge(t,"iterate",Ut),Ie(e)?t:t.map(me))}function On(e){return ge(e=J(e),"iterate",Ut),e}const Vo={__proto__:null,[Symbol.iterator](){return qn(this,Symbol.iterator,me)},concat(...e){return _t(this).concat(...e.map(t=>B(t)?_t(t):t))},entries(){return qn(this,"entries",e=>(e[1]=me(e[1]),e))},every(e,t){return Be(this,"every",e,t,void 0,arguments)},filter(e,t){return Be(this,"filter",e,t,n=>n.map(me),arguments)},find(e,t){return Be(this,"find",e,t,me,arguments)},findIndex(e,t){return Be(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Be(this,"findLast",e,t,me,arguments)},findLastIndex(e,t){return Be(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Be(this,"forEach",e,t,void 0,arguments)},includes(...e){return Gn(this,"includes",e)},indexOf(...e){return Gn(this,"indexOf",e)},join(e){return _t(this).join(e)},lastIndexOf(...e){return Gn(this,"lastIndexOf",e)},map(e,t){return Be(this,"map",e,t,void 0,arguments)},pop(){return Lt(this,"pop")},push(...e){return Lt(this,"push",e)},reduce(e,...t){return Xs(this,"reduce",e,t)},reduceRight(e,...t){return Xs(this,"reduceRight",e,t)},shift(){return Lt(this,"shift")},some(e,t){return Be(this,"some",e,t,void 0,arguments)},splice(...e){return Lt(this,"splice",e)},toReversed(){return _t(this).toReversed()},toSorted(e){return _t(this).toSorted(e)},toSpliced(...e){return _t(this).toSpliced(...e)},unshift(...e){return Lt(this,"unshift",e)},values(){return qn(this,"values",me)}};function qn(e,t,n){const s=On(e),r=s[t]();return s!==e&&!Ie(e)&&(r._next=r.next,r.next=()=>{const i=r._next();return i.value&&(i.value=n(i.value)),i}),r}const Uo=Array.prototype;function Be(e,t,n,s,r,i){const o=On(e),l=o!==e&&!Ie(e),c=o[t];if(c!==Uo[t]){const h=c.apply(e,i);return l?me(h):h}let u=n;o!==e&&(l?u=function(h,v){return n.call(this,me(h),v,e)}:n.length>2&&(u=function(h,v){return n.call(this,h,v,e)}));const a=c.call(o,u,s);return l&&r?r(a):a}function Xs(e,t,n,s){const r=On(e);let i=n;return r!==e&&(Ie(e)?n.length>3&&(i=function(o,l,c){return n.call(this,o,l,c,e)}):i=function(o,l,c){return n.call(this,o,me(l),c,e)}),r[t](i,...s)}function Gn(e,t,n){const s=J(e);ge(s,"iterate",Ut);const r=s[t](...n);return(r===-1||r===!1)&&Fs(n[0])?(n[0]=J(n[0]),s[t](...n)):r}function Lt(e,t,n=[]){rt(),Ms();const s=J(e)[t].apply(e,n);return Is(),it(),s}const Bo=Es("__proto__,__v_isRef,__isVue"),Zr=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Ge));function ko(e){Ge(e)||(e=String(e));const t=J(this);return ge(t,"has",e),t.hasOwnProperty(e)}class ei{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,i=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return i;if(n==="__v_raw")return s===(r?i?Zo:ri:i?si:ni).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const o=B(t);if(!r){let c;if(o&&(c=Vo[n]))return c;if(n==="hasOwnProperty")return ko}const l=Reflect.get(t,n,le(t)?t:s);return(Ge(n)?Zr.has(n):Bo(n))||(r||ge(t,"get",n),i)?l:le(l)?o&&As(n)?l:l.value:ne(l)?r?In(l):Mn(l):l}}class ti extends ei{constructor(t=!1){super(!1,t)}set(t,n,s,r){let i=t[n];if(!this._isShallow){const c=yt(i);if(!Ie(s)&&!yt(s)&&(i=J(i),s=J(s)),!B(t)&&le(i)&&!le(s))return c?!1:(i.value=s,!0)}const o=B(t)&&As(n)?Number(n)e,en=e=>Reflect.getPrototypeOf(e);function Yo(e,t,n){return function(...s){const r=this.__v_raw,i=J(r),o=xt(i),l=e==="entries"||e===Symbol.iterator&&o,c=e==="keys"&&o,u=r[e](...s),a=n?ds:t?hs:me;return!t&&ge(i,"iterate",c?us:dt),{next(){const{value:h,done:v}=u.next();return v?{value:h,done:v}:{value:l?[a(h[0]),a(h[1])]:a(h),done:v}},[Symbol.iterator](){return this}}}}function tn(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Xo(e,t){const n={get(r){const i=this.__v_raw,o=J(i),l=J(r);e||(et(r,l)&&ge(o,"get",r),ge(o,"get",l));const{has:c}=en(o),u=t?ds:e?hs:me;if(c.call(o,r))return u(i.get(r));if(c.call(o,l))return u(i.get(l));i!==o&&i.get(r)},get size(){const r=this.__v_raw;return!e&&ge(J(r),"iterate",dt),Reflect.get(r,"size",r)},has(r){const i=this.__v_raw,o=J(i),l=J(r);return e||(et(r,l)&&ge(o,"has",r),ge(o,"has",l)),r===l?i.has(r):i.has(r)||i.has(l)},forEach(r,i){const o=this,l=o.__v_raw,c=J(l),u=t?ds:e?hs:me;return!e&&ge(c,"iterate",dt),l.forEach((a,h)=>r.call(i,u(a),u(h),o))}};return ce(n,e?{add:tn("add"),set:tn("set"),delete:tn("delete"),clear:tn("clear")}:{add(r){!t&&!Ie(r)&&!yt(r)&&(r=J(r));const i=J(this);return en(i).has.call(i,r)||(i.add(r),Ke(i,"add",r,r)),this},set(r,i){!t&&!Ie(i)&&!yt(i)&&(i=J(i));const o=J(this),{has:l,get:c}=en(o);let u=l.call(o,r);u||(r=J(r),u=l.call(o,r));const a=c.call(o,r);return o.set(r,i),u?et(i,a)&&Ke(o,"set",r,i):Ke(o,"add",r,i),this},delete(r){const i=J(this),{has:o,get:l}=en(i);let c=o.call(i,r);c||(r=J(r),c=o.call(i,r)),l&&l.call(i,r);const u=i.delete(r);return c&&Ke(i,"delete",r,void 0),u},clear(){const r=J(this),i=r.size!==0,o=r.clear();return i&&Ke(r,"clear",void 0,void 0),o}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=Yo(r,e,t)}),n}function Ps(e,t){const n=Xo(e,t);return(s,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(z(n,r)&&r in s?n:s,r,i)}const Jo={get:Ps(!1,!1)},zo={get:Ps(!1,!0)},Qo={get:Ps(!0,!1)};const ni=new WeakMap,si=new WeakMap,ri=new WeakMap,Zo=new WeakMap;function el(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function tl(e){return e.__v_skip||!Object.isExtensible(e)?0:el(xo(e))}function Mn(e){return yt(e)?e:Ns(e,!1,Ko,Jo,ni)}function nl(e){return Ns(e,!1,Go,zo,si)}function In(e){return Ns(e,!0,qo,Qo,ri)}function Ns(e,t,n,s,r){if(!ne(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=r.get(e);if(i)return i;const o=tl(e);if(o===0)return e;const l=new Proxy(e,o===2?s:n);return r.set(e,l),l}function ht(e){return yt(e)?ht(e.__v_raw):!!(e&&e.__v_isReactive)}function yt(e){return!!(e&&e.__v_isReadonly)}function Ie(e){return!!(e&&e.__v_isShallow)}function Fs(e){return e?!!e.__v_raw:!1}function J(e){const t=e&&e.__v_raw;return t?J(t):e}function hn(e){return!z(e,"__v_skip")&&Object.isExtensible(e)&&Vr(e,"__v_skip",!0),e}const me=e=>ne(e)?Mn(e):e,hs=e=>ne(e)?In(e):e;function le(e){return e?e.__v_isRef===!0:!1}function ue(e){return oi(e,!1)}function ii(e){return oi(e,!0)}function oi(e,t){return le(e)?e:new sl(e,t)}class sl{constructor(t,n){this.dep=new Rn,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:J(t),this._value=n?t:me(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||Ie(t)||yt(t);t=s?t:J(t),et(t,n)&&(this._rawValue=t,this._value=s?t:me(t),this.dep.trigger())}}function li(e){return le(e)?e.value:e}const rl={get:(e,t,n)=>t==="__v_raw"?e:li(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return le(r)&&!le(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function ci(e){return ht(e)?e:new Proxy(e,rl)}class il{constructor(t){this.__v_isRef=!0,this._value=void 0;const n=this.dep=new Rn,{get:s,set:r}=t(n.track.bind(n),n.trigger.bind(n));this._get=s,this._set=r}get value(){return this._value=this._get()}set value(t){this._set(t)}}function ol(e){return new il(e)}class ll{constructor(t,n,s){this._object=t,this._key=n,this._defaultValue=s,this.__v_isRef=!0,this._value=void 0}get value(){const t=this._object[this._key];return this._value=t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return jo(J(this._object),this._key)}}class cl{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function al(e,t,n){return le(e)?e:q(e)?new cl(e):ne(e)&&arguments.length>1?fl(e,t,n):ue(e)}function fl(e,t,n){const s=e[t];return le(s)?s:new ll(e,t,n)}class ul{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new Rn(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Vt-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&ee!==this)return Gr(this,!0),!0}get value(){const t=this.dep.track();return Jr(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function dl(e,t,n=!1){let s,r;return q(e)?s=e:(s=e.get,r=e.set),new ul(s,r,n)}const nn={},vn=new WeakMap;let ft;function hl(e,t=!1,n=ft){if(n){let s=vn.get(n);s||vn.set(n,s=[]),s.push(e)}}function pl(e,t,n=te){const{immediate:s,deep:r,once:i,scheduler:o,augmentJob:l,call:c}=n,u=p=>r?p:Ie(p)||r===!1||r===0?Ze(p,1):Ze(p);let a,h,v,S,L=!1,O=!1;if(le(e)?(h=()=>e.value,L=Ie(e)):ht(e)?(h=()=>u(e),L=!0):B(e)?(O=!0,L=e.some(p=>ht(p)||Ie(p)),h=()=>e.map(p=>{if(le(p))return p.value;if(ht(p))return u(p);if(q(p))return c?c(p,2):p()})):q(e)?t?h=c?()=>c(e,2):e:h=()=>{if(v){rt();try{v()}finally{it()}}const p=ft;ft=a;try{return c?c(e,3,[S]):e(S)}finally{ft=p}}:h=Ue,t&&r){const p=h,A=r===!0?1/0:r;h=()=>Ze(p(),A)}const G=Wr(),U=()=>{a.stop(),G&&G.active&&Cs(G.effects,a)};if(i&&t){const p=t;t=(...A)=>{p(...A),U()}}let W=O?new Array(e.length).fill(nn):nn;const g=p=>{if(!(!(a.flags&1)||!a.dirty&&!p))if(t){const A=a.run();if(r||L||(O?A.some((D,j)=>et(D,W[j])):et(A,W))){v&&v();const D=ft;ft=a;try{const j=[A,W===nn?void 0:O&&W[0]===nn?[]:W,S];c?c(t,3,j):t(...j),W=A}finally{ft=D}}}else a.run()};return l&&l(g),a=new Kr(h),a.scheduler=o?()=>o(g,!1):g,S=p=>hl(p,!1,a),v=a.onStop=()=>{const p=vn.get(a);if(p){if(c)c(p,4);else for(const A of p)A();vn.delete(a)}},t?s?g(!0):W=a.run():o?o(g.bind(null,!0),!0):a.run(),U.pause=a.pause.bind(a),U.resume=a.resume.bind(a),U.stop=U,U}function Ze(e,t=1/0,n){if(t<=0||!ne(e)||e.__v_skip||(n=n||new Set,n.has(e)))return e;if(n.add(e),t--,le(e))Ze(e.value,t,n);else if(B(e))for(let s=0;s{Ze(s,t,n)});else if(jr(e)){for(const s in e)Ze(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&Ze(e[s],t,n)}return e}/** +* @vue/runtime-core v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Jt(e,t,n,s){try{return s?e(...s):e()}catch(r){Ln(r,t,n)}}function He(e,t,n,s){if(q(e)){const r=Jt(e,t,n,s);return r&&$r(r)&&r.catch(i=>{Ln(i,t,n)}),r}if(B(e)){const r=[];for(let i=0;i>>1,r=we[s],i=Bt(r);i=Bt(n)?we.push(e):we.splice(ml(t),0,e),e.flags|=1,fi()}}function fi(){_n||(_n=ai.then(ui))}function yl(e){B(e)?Tt.push(...e):Je&&e.id===-1?Je.splice(wt+1,0,e):e.flags&1||(Tt.push(e),e.flags|=1),fi()}function Js(e,t,n=je+1){for(;nBt(n)-Bt(s));if(Tt.length=0,Je){Je.push(...t);return}for(Je=t,wt=0;wte.id==null?e.flags&2?-1:1/0:e.id;function ui(e){try{for(je=0;je{s._d&&cr(-1);const i=wn(t);let o;try{o=e(...r)}finally{wn(i),s._d&&cr(1)}return o};return s._n=!0,s._c=!0,s._d=!0,s}function Ve(e,t,n,s){const r=e.dirs,i=t&&t.dirs;for(let o=0;oe.__isTeleport,ze=Symbol("_leaveCb"),sn=Symbol("_enterCb");function bl(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return Ot(()=>{e.isMounted=!0}),Si(()=>{e.isUnmounting=!0}),e}const Re=[Function,Array],pi={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Re,onEnter:Re,onAfterEnter:Re,onEnterCancelled:Re,onBeforeLeave:Re,onLeave:Re,onAfterLeave:Re,onLeaveCancelled:Re,onBeforeAppear:Re,onAppear:Re,onAfterAppear:Re,onAppearCancelled:Re},gi=e=>{const t=e.subTree;return t.component?gi(t.component):t},wl={name:"BaseTransition",props:pi,setup(e,{slots:t}){const n=jn(),s=bl();return()=>{const r=t.default&&vi(t.default(),!0);if(!r||!r.length)return;const i=mi(r),o=J(e),{mode:l}=o;if(s.isLeaving)return Yn(i);const c=zs(i);if(!c)return Yn(i);let u=ps(c,o,s,n,h=>u=h);c.type!==ye&&kt(c,u);let a=n.subTree&&zs(n.subTree);if(a&&a.type!==ye&&!ut(c,a)&&gi(n).type!==ye){let h=ps(a,o,s,n);if(kt(a,h),l==="out-in"&&c.type!==ye)return s.isLeaving=!0,h.afterLeave=()=>{s.isLeaving=!1,n.job.flags&8||n.update(),delete h.afterLeave,a=void 0},Yn(i);l==="in-out"&&c.type!==ye?h.delayLeave=(v,S,L)=>{const O=yi(s,a);O[String(a.key)]=a,v[ze]=()=>{S(),v[ze]=void 0,delete u.delayedLeave,a=void 0},u.delayedLeave=()=>{L(),delete u.delayedLeave,a=void 0}}:a=void 0}else a&&(a=void 0);return i}}};function mi(e){let t=e[0];if(e.length>1){for(const n of e)if(n.type!==ye){t=n;break}}return t}const Sl=wl;function yi(e,t){const{leavingVNodes:n}=e;let s=n.get(t.type);return s||(s=Object.create(null),n.set(t.type,s)),s}function ps(e,t,n,s,r){const{appear:i,mode:o,persisted:l=!1,onBeforeEnter:c,onEnter:u,onAfterEnter:a,onEnterCancelled:h,onBeforeLeave:v,onLeave:S,onAfterLeave:L,onLeaveCancelled:O,onBeforeAppear:G,onAppear:U,onAfterAppear:W,onAppearCancelled:g}=t,p=String(e.key),A=yi(n,e),D=(I,_)=>{I&&He(I,s,9,_)},j=(I,_)=>{const P=_[1];D(I,_),B(I)?I.every(b=>b.length<=1)&&P():I.length<=1&&P()},K={mode:o,persisted:l,beforeEnter(I){let _=c;if(!n.isMounted)if(i)_=G||c;else return;I[ze]&&I[ze](!0);const P=A[p];P&&ut(e,P)&&P.el[ze]&&P.el[ze](),D(_,[I])},enter(I){let _=u,P=a,b=h;if(!n.isMounted)if(i)_=U||u,P=W||a,b=g||h;else return;let V=!1;const se=I[sn]=ie=>{V||(V=!0,ie?D(b,[I]):D(P,[I]),K.delayedLeave&&K.delayedLeave(),I[sn]=void 0)};_?j(_,[I,se]):se()},leave(I,_){const P=String(e.key);if(I[sn]&&I[sn](!0),n.isUnmounting)return _();D(v,[I]);let b=!1;const V=I[ze]=se=>{b||(b=!0,_(),se?D(O,[I]):D(L,[I]),I[ze]=void 0,A[P]===e&&delete A[P])};A[P]=e,S?j(S,[I,V]):V()},clone(I){const _=ps(I,t,n,s,r);return r&&r(_),_}};return K}function Yn(e){if(Nn(e))return e=nt(e),e.children=null,e}function zs(e){if(!Nn(e))return hi(e.type)&&e.children?mi(e.children):e;const{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&q(n.default))return n.default()}}function kt(e,t){e.shapeFlag&6&&e.component?(e.transition=t,kt(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function vi(e,t=!1,n){let s=[],r=0;for(let i=0;i1)for(let i=0;iWt(L,t&&(B(t)?t[O]:t),n,s,r));return}if(pt(s)&&!r){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&Wt(e,t,n,s.component.subTree);return}const i=s.shapeFlag&4?js(s.component):s.el,o=r?null:i,{i:l,r:c}=e,u=t&&t.r,a=l.refs===te?l.refs={}:l.refs,h=l.setupState,v=J(h),S=h===te?()=>!1:L=>z(v,L);if(u!=null&&u!==c&&(re(u)?(a[u]=null,S(u)&&(h[u]=null)):le(u)&&(u.value=null)),q(c))Jt(c,l,12,[o,a]);else{const L=re(c),O=le(c);if(L||O){const G=()=>{if(e.f){const U=L?S(c)?h[c]:a[c]:c.value;r?B(U)&&Cs(U,i):B(U)?U.includes(i)||U.push(i):L?(a[c]=[i],S(c)&&(h[c]=a[c])):(c.value=[i],e.k&&(a[e.k]=c.value))}else L?(a[c]=o,S(c)&&(h[c]=o)):O&&(c.value=o,e.k&&(a[e.k]=o))};o?(G.id=-1,Ce(G,n)):G()}}}let Qs=!1;const bt=()=>{Qs||(console.error("Hydration completed but contains mismatches."),Qs=!0)},xl=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",El=e=>e.namespaceURI.includes("MathML"),rn=e=>{if(e.nodeType===1){if(xl(e))return"svg";if(El(e))return"mathml"}},on=e=>e.nodeType===8;function Tl(e){const{mt:t,p:n,o:{patchProp:s,createText:r,nextSibling:i,parentNode:o,remove:l,insert:c,createComment:u}}=e,a=(g,p)=>{if(!p.hasChildNodes()){n(null,g,p),bn(),p._vnode=g;return}h(p.firstChild,g,null,null,null),bn(),p._vnode=g},h=(g,p,A,D,j,K=!1)=>{K=K||!!p.dynamicChildren;const I=on(g)&&g.data==="[",_=()=>O(g,p,A,D,j,I),{type:P,ref:b,shapeFlag:V,patchFlag:se}=p;let ie=g.nodeType;p.el=g,se===-2&&(K=!1,p.dynamicChildren=null);let H=null;switch(P){case gt:ie!==3?p.children===""?(c(p.el=r(""),o(g),g),H=g):H=_():(g.data!==p.children&&(bt(),g.data=p.children),H=i(g));break;case ye:W(g)?(H=i(g),U(p.el=g.content.firstChild,g,A)):ie!==8||I?H=_():H=i(g);break;case Dt:if(I&&(g=i(g),ie=g.nodeType),ie===1||ie===3){H=g;const Y=!p.children.length;for(let F=0;F{K=K||!!p.dynamicChildren;const{type:I,props:_,patchFlag:P,shapeFlag:b,dirs:V,transition:se}=p,ie=I==="input"||I==="option";if(ie||P!==-1){V&&Ve(p,null,A,"created");let H=!1;if(W(g)){H=ji(null,se)&&A&&A.vnode.props&&A.vnode.props.appear;const F=g.content.firstChild;H&&se.beforeEnter(F),U(F,g,A),p.el=g=F}if(b&16&&!(_&&(_.innerHTML||_.textContent))){let F=S(g.firstChild,p,g,A,D,j,K);for(;F;){ln(g,1)||bt();const ae=F;F=F.nextSibling,l(ae)}}else if(b&8){let F=p.children;F[0]===` +`&&(g.tagName==="PRE"||g.tagName==="TEXTAREA")&&(F=F.slice(1)),g.textContent!==F&&(ln(g,0)||bt(),g.textContent=p.children)}if(_){if(ie||!K||P&48){const F=g.tagName.includes("-");for(const ae in _)(ie&&(ae.endsWith("value")||ae==="indeterminate")||Xt(ae)&&!Et(ae)||ae[0]==="."||F)&&s(g,ae,null,_[ae],void 0,A)}else if(_.onClick)s(g,"onClick",null,_.onClick,void 0,A);else if(P&4&&ht(_.style))for(const F in _.style)_.style[F]}let Y;(Y=_&&_.onVnodeBeforeMount)&&Oe(Y,A,p),V&&Ve(p,null,A,"beforeMount"),((Y=_&&_.onVnodeMounted)||V||H)&&Gi(()=>{Y&&Oe(Y,A,p),H&&se.enter(g),V&&Ve(p,null,A,"mounted")},D)}return g.nextSibling},S=(g,p,A,D,j,K,I)=>{I=I||!!p.dynamicChildren;const _=p.children,P=_.length;for(let b=0;b{const{slotScopeIds:I}=p;I&&(j=j?j.concat(I):I);const _=o(g),P=S(i(g),p,_,A,D,j,K);return P&&on(P)&&P.data==="]"?i(p.anchor=P):(bt(),c(p.anchor=u("]"),_,P),P)},O=(g,p,A,D,j,K)=>{if(ln(g.parentElement,1)||bt(),p.el=null,K){const P=G(g);for(;;){const b=i(g);if(b&&b!==P)l(b);else break}}const I=i(g),_=o(g);return l(g),n(null,p,_,I,A,D,rn(_),j),A&&(A.vnode.el=p.el,Ki(A,p.el)),I},G=(g,p="[",A="]")=>{let D=0;for(;g;)if(g=i(g),g&&on(g)&&(g.data===p&&D++,g.data===A)){if(D===0)return i(g);D--}return g},U=(g,p,A)=>{const D=p.parentNode;D&&D.replaceChild(g,p);let j=A;for(;j;)j.vnode.el===p&&(j.vnode.el=j.subTree.el=g),j=j.parent},W=g=>g.nodeType===1&&g.tagName==="TEMPLATE";return[a,h]}const Zs="data-allow-mismatch",Cl={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function ln(e,t){if(t===0||t===1)for(;e&&!e.hasAttribute(Zs);)e=e.parentElement;const n=e&&e.getAttribute(Zs);if(n==null)return!1;if(n==="")return!0;{const s=n.split(",");return t===0&&s.includes("children")?!0:n.split(",").includes(Cl[t])}}An().requestIdleCallback;An().cancelIdleCallback;const pt=e=>!!e.type.__asyncLoader,Nn=e=>e.type.__isKeepAlive;function Al(e,t){wi(e,"a",t)}function Rl(e,t){wi(e,"da",t)}function wi(e,t,n=fe){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(Fn(t,s,n),n){let r=n.parent;for(;r&&r.parent;)Nn(r.parent.vnode)&&Ol(s,t,n,r),r=r.parent}}function Ol(e,t,n,s){const r=Fn(t,e,s,!0);Hn(()=>{Cs(s[t],r)},n)}function Fn(e,t,n=fe,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...o)=>{rt();const l=zt(n),c=He(t,n,e,o);return l(),it(),c});return s?r.unshift(i):r.push(i),i}}const Ye=e=>(t,n=fe)=>{(!Gt||e==="sp")&&Fn(e,(...s)=>t(...s),n)},Ml=Ye("bm"),Ot=Ye("m"),Il=Ye("bu"),Ll=Ye("u"),Si=Ye("bum"),Hn=Ye("um"),Pl=Ye("sp"),Nl=Ye("rtg"),Fl=Ye("rtc");function Hl(e,t=fe){Fn("ec",e,t)}const xi="components";function Xa(e,t){return Ti(xi,e,!0,t)||e}const Ei=Symbol.for("v-ndc");function Ja(e){return re(e)?Ti(xi,e,!1)||e:e||Ei}function Ti(e,t,n=!0,s=!1){const r=ve||fe;if(r){const i=r.type;{const l=bc(i,!1);if(l&&(l===t||l===Le(t)||l===Cn(Le(t))))return i}const o=er(r[e]||i[e],t)||er(r.appContext[e],t);return!o&&s?i:o}}function er(e,t){return e&&(e[t]||e[Le(t)]||e[Cn(Le(t))])}function za(e,t,n,s){let r;const i=n,o=B(e);if(o||re(e)){const l=o&&ht(e);let c=!1;l&&(c=!Ie(e),e=On(e)),r=new Array(e.length);for(let u=0,a=e.length;ut(l,c,void 0,i));else{const l=Object.keys(e);r=new Array(l.length);for(let c=0,u=l.length;cqt(t)?!(t.type===ye||t.type===Se&&!Ci(t.children)):!0)?e:null}function Za(e,t){const n={};for(const s in e)n[/[A-Z]/.test(s)?`on:${s}`:dn(s)]=e[s];return n}const gs=e=>e?Qi(e)?js(e):gs(e.parent):null,$t=ce(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>gs(e.parent),$root:e=>gs(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Ri(e),$forceUpdate:e=>e.f||(e.f=()=>{Hs(e.update)}),$nextTick:e=>e.n||(e.n=Pn.bind(e.proxy)),$watch:e=>rc.bind(e)}),Xn=(e,t)=>e!==te&&!e.__isScriptSetup&&z(e,t),$l={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:r,props:i,accessCache:o,type:l,appContext:c}=e;let u;if(t[0]!=="$"){const S=o[t];if(S!==void 0)switch(S){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(Xn(s,t))return o[t]=1,s[t];if(r!==te&&z(r,t))return o[t]=2,r[t];if((u=e.propsOptions[0])&&z(u,t))return o[t]=3,i[t];if(n!==te&&z(n,t))return o[t]=4,n[t];ms&&(o[t]=0)}}const a=$t[t];let h,v;if(a)return t==="$attrs"&&ge(e.attrs,"get",""),a(e);if((h=l.__cssModules)&&(h=h[t]))return h;if(n!==te&&z(n,t))return o[t]=4,n[t];if(v=c.config.globalProperties,z(v,t))return v[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:i}=e;return Xn(r,t)?(r[t]=n,!0):s!==te&&z(s,t)?(s[t]=n,!0):z(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,propsOptions:i}},o){let l;return!!n[o]||e!==te&&z(e,o)||Xn(t,o)||(l=i[0])&&z(l,o)||z(s,o)||z($t,o)||z(r.config.globalProperties,o)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:z(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function ef(){return Dl().slots}function Dl(){const e=jn();return e.setupContext||(e.setupContext=eo(e))}function tr(e){return B(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let ms=!0;function jl(e){const t=Ri(e),n=e.proxy,s=e.ctx;ms=!1,t.beforeCreate&&nr(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:l,provide:c,inject:u,created:a,beforeMount:h,mounted:v,beforeUpdate:S,updated:L,activated:O,deactivated:G,beforeDestroy:U,beforeUnmount:W,destroyed:g,unmounted:p,render:A,renderTracked:D,renderTriggered:j,errorCaptured:K,serverPrefetch:I,expose:_,inheritAttrs:P,components:b,directives:V,filters:se}=t;if(u&&Vl(u,s,null),o)for(const Y in o){const F=o[Y];q(F)&&(s[Y]=F.bind(n))}if(r){const Y=r.call(n,n);ne(Y)&&(e.data=Mn(Y))}if(ms=!0,i)for(const Y in i){const F=i[Y],ae=q(F)?F.bind(n,n):q(F.get)?F.get.bind(n,n):Ue,Qt=!q(F)&&q(F.set)?F.set.bind(n):Ue,ot=oe({get:ae,set:Qt});Object.defineProperty(s,Y,{enumerable:!0,configurable:!0,get:()=>ot.value,set:$e=>ot.value=$e})}if(l)for(const Y in l)Ai(l[Y],s,n,Y);if(c){const Y=q(c)?c.call(n):c;Reflect.ownKeys(Y).forEach(F=>{ql(F,Y[F])})}a&&nr(a,e,"c");function H(Y,F){B(F)?F.forEach(ae=>Y(ae.bind(n))):F&&Y(F.bind(n))}if(H(Ml,h),H(Ot,v),H(Il,S),H(Ll,L),H(Al,O),H(Rl,G),H(Hl,K),H(Fl,D),H(Nl,j),H(Si,W),H(Hn,p),H(Pl,I),B(_))if(_.length){const Y=e.exposed||(e.exposed={});_.forEach(F=>{Object.defineProperty(Y,F,{get:()=>n[F],set:ae=>n[F]=ae})})}else e.exposed||(e.exposed={});A&&e.render===Ue&&(e.render=A),P!=null&&(e.inheritAttrs=P),b&&(e.components=b),V&&(e.directives=V),I&&bi(e)}function Vl(e,t,n=Ue){B(e)&&(e=ys(e));for(const s in e){const r=e[s];let i;ne(r)?"default"in r?i=At(r.from||s,r.default,!0):i=At(r.from||s):i=At(r),le(i)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>i.value,set:o=>i.value=o}):t[s]=i}}function nr(e,t,n){He(B(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function Ai(e,t,n,s){let r=s.includes(".")?ki(n,s):()=>n[s];if(re(e)){const i=t[e];q(i)&&Fe(r,i)}else if(q(e))Fe(r,e.bind(n));else if(ne(e))if(B(e))e.forEach(i=>Ai(i,t,n,s));else{const i=q(e.handler)?e.handler.bind(n):t[e.handler];q(i)&&Fe(r,i,e)}}function Ri(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,l=i.get(t);let c;return l?c=l:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(u=>Sn(c,u,o,!0)),Sn(c,t,o)),ne(t)&&i.set(t,c),c}function Sn(e,t,n,s=!1){const{mixins:r,extends:i}=t;i&&Sn(e,i,n,!0),r&&r.forEach(o=>Sn(e,o,n,!0));for(const o in t)if(!(s&&o==="expose")){const l=Ul[o]||n&&n[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const Ul={data:sr,props:rr,emits:rr,methods:Nt,computed:Nt,beforeCreate:_e,created:_e,beforeMount:_e,mounted:_e,beforeUpdate:_e,updated:_e,beforeDestroy:_e,beforeUnmount:_e,destroyed:_e,unmounted:_e,activated:_e,deactivated:_e,errorCaptured:_e,serverPrefetch:_e,components:Nt,directives:Nt,watch:kl,provide:sr,inject:Bl};function sr(e,t){return t?e?function(){return ce(q(e)?e.call(this,this):e,q(t)?t.call(this,this):t)}:t:e}function Bl(e,t){return Nt(ys(e),ys(t))}function ys(e){if(B(e)){const t={};for(let n=0;n1)return n&&q(t)?t.call(s&&s.proxy):t}}const Mi={},Ii=()=>Object.create(Mi),Li=e=>Object.getPrototypeOf(e)===Mi;function Gl(e,t,n,s=!1){const r={},i=Ii();e.propsDefaults=Object.create(null),Pi(e,t,r,i);for(const o in e.propsOptions[0])o in r||(r[o]=void 0);n?e.props=s?r:nl(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function Yl(e,t,n,s){const{props:r,attrs:i,vnode:{patchFlag:o}}=e,l=J(r),[c]=e.propsOptions;let u=!1;if((s||o>0)&&!(o&16)){if(o&8){const a=e.vnode.dynamicProps;for(let h=0;h{c=!0;const[v,S]=Ni(h,t,!0);ce(o,v),S&&l.push(...S)};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}if(!i&&!c)return ne(e)&&s.set(e,St),St;if(B(i))for(let a=0;ae[0]==="_"||e==="$stable",$s=e=>B(e)?e.map(Me):[Me(e)],Jl=(e,t,n)=>{if(t._n)return t;const s=vl((...r)=>$s(t(...r)),n);return s._c=!1,s},Hi=(e,t,n)=>{const s=e._ctx;for(const r in e){if(Fi(r))continue;const i=e[r];if(q(i))t[r]=Jl(r,i,s);else if(i!=null){const o=$s(i);t[r]=()=>o}}},$i=(e,t)=>{const n=$s(t);e.slots.default=()=>n},Di=(e,t,n)=>{for(const s in t)(n||s!=="_")&&(e[s]=t[s])},zl=(e,t,n)=>{const s=e.slots=Ii();if(e.vnode.shapeFlag&32){const r=t._;r?(Di(s,t,n),n&&Vr(s,"_",r,!0)):Hi(t,s)}else t&&$i(e,t)},Ql=(e,t,n)=>{const{vnode:s,slots:r}=e;let i=!0,o=te;if(s.shapeFlag&32){const l=t._;l?n&&l===1?i=!1:Di(r,t,n):(i=!t.$stable,Hi(t,r)),o=t}else t&&($i(e,t),o={default:1});if(i)for(const l in r)!Fi(l)&&o[l]==null&&delete r[l]},Ce=Gi;function Zl(e){return ec(e,Tl)}function ec(e,t){const n=An();n.__VUE__=!0;const{insert:s,remove:r,patchProp:i,createElement:o,createText:l,createComment:c,setText:u,setElementText:a,parentNode:h,nextSibling:v,setScopeId:S=Ue,insertStaticContent:L}=e,O=(f,d,m,x=null,y=null,w=null,R=void 0,C=null,T=!!d.dynamicChildren)=>{if(f===d)return;f&&!ut(f,d)&&(x=Zt(f),$e(f,y,w,!0),f=null),d.patchFlag===-2&&(T=!1,d.dynamicChildren=null);const{type:E,ref:$,shapeFlag:M}=d;switch(E){case gt:G(f,d,m,x);break;case ye:U(f,d,m,x);break;case Dt:f==null&&W(d,m,x,R);break;case Se:b(f,d,m,x,y,w,R,C,T);break;default:M&1?A(f,d,m,x,y,w,R,C,T):M&6?V(f,d,m,x,y,w,R,C,T):(M&64||M&128)&&E.process(f,d,m,x,y,w,R,C,T,vt)}$!=null&&y&&Wt($,f&&f.ref,w,d||f,!d)},G=(f,d,m,x)=>{if(f==null)s(d.el=l(d.children),m,x);else{const y=d.el=f.el;d.children!==f.children&&u(y,d.children)}},U=(f,d,m,x)=>{f==null?s(d.el=c(d.children||""),m,x):d.el=f.el},W=(f,d,m,x)=>{[f.el,f.anchor]=L(f.children,d,m,x,f.el,f.anchor)},g=({el:f,anchor:d},m,x)=>{let y;for(;f&&f!==d;)y=v(f),s(f,m,x),f=y;s(d,m,x)},p=({el:f,anchor:d})=>{let m;for(;f&&f!==d;)m=v(f),r(f),f=m;r(d)},A=(f,d,m,x,y,w,R,C,T)=>{d.type==="svg"?R="svg":d.type==="math"&&(R="mathml"),f==null?D(d,m,x,y,w,R,C,T):I(f,d,y,w,R,C,T)},D=(f,d,m,x,y,w,R,C)=>{let T,E;const{props:$,shapeFlag:M,transition:N,dirs:k}=f;if(T=f.el=o(f.type,w,$&&$.is,$),M&8?a(T,f.children):M&16&&K(f.children,T,null,x,y,Jn(f,w),R,C),k&&Ve(f,null,x,"created"),j(T,f,f.scopeId,R,x),$){for(const Z in $)Z!=="value"&&!Et(Z)&&i(T,Z,null,$[Z],w,x);"value"in $&&i(T,"value",null,$.value,w),(E=$.onVnodeBeforeMount)&&Oe(E,x,f)}k&&Ve(f,null,x,"beforeMount");const X=ji(y,N);X&&N.beforeEnter(T),s(T,d,m),((E=$&&$.onVnodeMounted)||X||k)&&Ce(()=>{E&&Oe(E,x,f),X&&N.enter(T),k&&Ve(f,null,x,"mounted")},y)},j=(f,d,m,x,y)=>{if(m&&S(f,m),x)for(let w=0;w{for(let E=T;E{const C=d.el=f.el;let{patchFlag:T,dynamicChildren:E,dirs:$}=d;T|=f.patchFlag&16;const M=f.props||te,N=d.props||te;let k;if(m&<(m,!1),(k=N.onVnodeBeforeUpdate)&&Oe(k,m,d,f),$&&Ve(d,f,m,"beforeUpdate"),m&<(m,!0),(M.innerHTML&&N.innerHTML==null||M.textContent&&N.textContent==null)&&a(C,""),E?_(f.dynamicChildren,E,C,m,x,Jn(d,y),w):R||F(f,d,C,null,m,x,Jn(d,y),w,!1),T>0){if(T&16)P(C,M,N,m,y);else if(T&2&&M.class!==N.class&&i(C,"class",null,N.class,y),T&4&&i(C,"style",M.style,N.style,y),T&8){const X=d.dynamicProps;for(let Z=0;Z{k&&Oe(k,m,d,f),$&&Ve(d,f,m,"updated")},x)},_=(f,d,m,x,y,w,R)=>{for(let C=0;C{if(d!==m){if(d!==te)for(const w in d)!Et(w)&&!(w in m)&&i(f,w,d[w],null,y,x);for(const w in m){if(Et(w))continue;const R=m[w],C=d[w];R!==C&&w!=="value"&&i(f,w,C,R,y,x)}"value"in m&&i(f,"value",d.value,m.value,y)}},b=(f,d,m,x,y,w,R,C,T)=>{const E=d.el=f?f.el:l(""),$=d.anchor=f?f.anchor:l("");let{patchFlag:M,dynamicChildren:N,slotScopeIds:k}=d;k&&(C=C?C.concat(k):k),f==null?(s(E,m,x),s($,m,x),K(d.children||[],m,$,y,w,R,C,T)):M>0&&M&64&&N&&f.dynamicChildren?(_(f.dynamicChildren,N,m,y,w,R,C),(d.key!=null||y&&d===y.subTree)&&Vi(f,d,!0)):F(f,d,m,$,y,w,R,C,T)},V=(f,d,m,x,y,w,R,C,T)=>{d.slotScopeIds=C,f==null?d.shapeFlag&512?y.ctx.activate(d,m,x,R,T):se(d,m,x,y,w,R,T):ie(f,d,T)},se=(f,d,m,x,y,w,R)=>{const C=f.component=mc(f,x,y);if(Nn(f)&&(C.ctx.renderer=vt),yc(C,!1,R),C.asyncDep){if(y&&y.registerDep(C,H,R),!f.el){const T=C.subTree=de(ye);U(null,T,d,m)}}else H(C,f,d,m,y,w,R)},ie=(f,d,m)=>{const x=d.component=f.component;if(ac(f,d,m))if(x.asyncDep&&!x.asyncResolved){Y(x,d,m);return}else x.next=d,x.update();else d.el=f.el,x.vnode=d},H=(f,d,m,x,y,w,R)=>{const C=()=>{if(f.isMounted){let{next:M,bu:N,u:k,parent:X,vnode:Z}=f;{const Ee=Ui(f);if(Ee){M&&(M.el=Z.el,Y(f,M,R)),Ee.asyncDep.then(()=>{f.isUnmounted||C()});return}}let Q=M,xe;lt(f,!1),M?(M.el=Z.el,Y(f,M,R)):M=Z,N&&kn(N),(xe=M.props&&M.props.onVnodeBeforeUpdate)&&Oe(xe,X,M,Z),lt(f,!0);const he=zn(f),Pe=f.subTree;f.subTree=he,O(Pe,he,h(Pe.el),Zt(Pe),f,y,w),M.el=he.el,Q===null&&Ki(f,he.el),k&&Ce(k,y),(xe=M.props&&M.props.onVnodeUpdated)&&Ce(()=>Oe(xe,X,M,Z),y)}else{let M;const{el:N,props:k}=d,{bm:X,m:Z,parent:Q,root:xe,type:he}=f,Pe=pt(d);if(lt(f,!1),X&&kn(X),!Pe&&(M=k&&k.onVnodeBeforeMount)&&Oe(M,Q,d),lt(f,!0),N&&Bn){const Ee=()=>{f.subTree=zn(f),Bn(N,f.subTree,f,y,null)};Pe&&he.__asyncHydrate?he.__asyncHydrate(N,f,Ee):Ee()}else{xe.ce&&xe.ce._injectChildStyle(he);const Ee=f.subTree=zn(f);O(null,Ee,m,x,f,y,w),d.el=Ee.el}if(Z&&Ce(Z,y),!Pe&&(M=k&&k.onVnodeMounted)){const Ee=d;Ce(()=>Oe(M,Q,Ee),y)}(d.shapeFlag&256||Q&&pt(Q.vnode)&&Q.vnode.shapeFlag&256)&&f.a&&Ce(f.a,y),f.isMounted=!0,d=m=x=null}};f.scope.on();const T=f.effect=new Kr(C);f.scope.off();const E=f.update=T.run.bind(T),$=f.job=T.runIfDirty.bind(T);$.i=f,$.id=f.uid,T.scheduler=()=>Hs($),lt(f,!0),E()},Y=(f,d,m)=>{d.component=f;const x=f.vnode.props;f.vnode=d,f.next=null,Yl(f,d.props,x,m),Ql(f,d.children,m),rt(),Js(f),it()},F=(f,d,m,x,y,w,R,C,T=!1)=>{const E=f&&f.children,$=f?f.shapeFlag:0,M=d.children,{patchFlag:N,shapeFlag:k}=d;if(N>0){if(N&128){Qt(E,M,m,x,y,w,R,C,T);return}else if(N&256){ae(E,M,m,x,y,w,R,C,T);return}}k&8?($&16&&Mt(E,y,w),M!==E&&a(m,M)):$&16?k&16?Qt(E,M,m,x,y,w,R,C,T):Mt(E,y,w,!0):($&8&&a(m,""),k&16&&K(M,m,x,y,w,R,C,T))},ae=(f,d,m,x,y,w,R,C,T)=>{f=f||St,d=d||St;const E=f.length,$=d.length,M=Math.min(E,$);let N;for(N=0;N$?Mt(f,y,w,!0,!1,M):K(d,m,x,y,w,R,C,T,M)},Qt=(f,d,m,x,y,w,R,C,T)=>{let E=0;const $=d.length;let M=f.length-1,N=$-1;for(;E<=M&&E<=N;){const k=f[E],X=d[E]=T?Qe(d[E]):Me(d[E]);if(ut(k,X))O(k,X,m,null,y,w,R,C,T);else break;E++}for(;E<=M&&E<=N;){const k=f[M],X=d[N]=T?Qe(d[N]):Me(d[N]);if(ut(k,X))O(k,X,m,null,y,w,R,C,T);else break;M--,N--}if(E>M){if(E<=N){const k=N+1,X=k<$?d[k].el:x;for(;E<=N;)O(null,d[E]=T?Qe(d[E]):Me(d[E]),m,X,y,w,R,C,T),E++}}else if(E>N)for(;E<=M;)$e(f[E],y,w,!0),E++;else{const k=E,X=E,Z=new Map;for(E=X;E<=N;E++){const Te=d[E]=T?Qe(d[E]):Me(d[E]);Te.key!=null&&Z.set(Te.key,E)}let Q,xe=0;const he=N-X+1;let Pe=!1,Ee=0;const It=new Array(he);for(E=0;E=he){$e(Te,y,w,!0);continue}let De;if(Te.key!=null)De=Z.get(Te.key);else for(Q=X;Q<=N;Q++)if(It[Q-X]===0&&ut(Te,d[Q])){De=Q;break}De===void 0?$e(Te,y,w,!0):(It[De-X]=E+1,De>=Ee?Ee=De:Pe=!0,O(Te,d[De],m,null,y,w,R,C,T),xe++)}const Ks=Pe?tc(It):St;for(Q=Ks.length-1,E=he-1;E>=0;E--){const Te=X+E,De=d[Te],qs=Te+1<$?d[Te+1].el:x;It[E]===0?O(null,De,m,qs,y,w,R,C,T):Pe&&(Q<0||E!==Ks[Q]?ot(De,m,qs,2):Q--)}}},ot=(f,d,m,x,y=null)=>{const{el:w,type:R,transition:C,children:T,shapeFlag:E}=f;if(E&6){ot(f.component.subTree,d,m,x);return}if(E&128){f.suspense.move(d,m,x);return}if(E&64){R.move(f,d,m,vt);return}if(R===Se){s(w,d,m);for(let M=0;MC.enter(w),y);else{const{leave:M,delayLeave:N,afterLeave:k}=C,X=()=>s(w,d,m),Z=()=>{M(w,()=>{X(),k&&k()})};N?N(w,X,Z):Z()}else s(w,d,m)},$e=(f,d,m,x=!1,y=!1)=>{const{type:w,props:R,ref:C,children:T,dynamicChildren:E,shapeFlag:$,patchFlag:M,dirs:N,cacheIndex:k}=f;if(M===-2&&(y=!1),C!=null&&Wt(C,null,m,f,!0),k!=null&&(d.renderCache[k]=void 0),$&256){d.ctx.deactivate(f);return}const X=$&1&&N,Z=!pt(f);let Q;if(Z&&(Q=R&&R.onVnodeBeforeUnmount)&&Oe(Q,d,f),$&6)bo(f.component,m,x);else{if($&128){f.suspense.unmount(m,x);return}X&&Ve(f,null,d,"beforeUnmount"),$&64?f.type.remove(f,d,m,vt,x):E&&!E.hasOnce&&(w!==Se||M>0&&M&64)?Mt(E,d,m,!1,!0):(w===Se&&M&384||!y&&$&16)&&Mt(T,d,m),x&&ks(f)}(Z&&(Q=R&&R.onVnodeUnmounted)||X)&&Ce(()=>{Q&&Oe(Q,d,f),X&&Ve(f,null,d,"unmounted")},m)},ks=f=>{const{type:d,el:m,anchor:x,transition:y}=f;if(d===Se){_o(m,x);return}if(d===Dt){p(f);return}const w=()=>{r(m),y&&!y.persisted&&y.afterLeave&&y.afterLeave()};if(f.shapeFlag&1&&y&&!y.persisted){const{leave:R,delayLeave:C}=y,T=()=>R(m,w);C?C(f.el,w,T):T()}else w()},_o=(f,d)=>{let m;for(;f!==d;)m=v(f),r(f),f=m;r(d)},bo=(f,d,m)=>{const{bum:x,scope:y,job:w,subTree:R,um:C,m:T,a:E}=f;or(T),or(E),x&&kn(x),y.stop(),w&&(w.flags|=8,$e(R,f,d,m)),C&&Ce(C,d),Ce(()=>{f.isUnmounted=!0},d),d&&d.pendingBranch&&!d.isUnmounted&&f.asyncDep&&!f.asyncResolved&&f.suspenseId===d.pendingId&&(d.deps--,d.deps===0&&d.resolve())},Mt=(f,d,m,x=!1,y=!1,w=0)=>{for(let R=w;R{if(f.shapeFlag&6)return Zt(f.component.subTree);if(f.shapeFlag&128)return f.suspense.next();const d=v(f.anchor||f.el),m=d&&d[_l];return m?v(m):d};let Vn=!1;const Ws=(f,d,m)=>{f==null?d._vnode&&$e(d._vnode,null,null,!0):O(d._vnode||null,f,d,null,null,null,m),d._vnode=f,Vn||(Vn=!0,Js(),bn(),Vn=!1)},vt={p:O,um:$e,m:ot,r:ks,mt:se,mc:K,pc:F,pbc:_,n:Zt,o:e};let Un,Bn;return[Un,Bn]=t(vt),{render:Ws,hydrate:Un,createApp:Kl(Ws,Un)}}function Jn({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function lt({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function ji(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function Vi(e,t,n=!1){const s=e.children,r=t.children;if(B(s)&&B(r))for(let i=0;i>1,e[n[l]]0&&(t[s]=n[i-1]),n[i]=s)}}for(i=n.length,o=n[i-1];i-- >0;)n[i]=o,o=t[o];return n}function Ui(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Ui(t)}function or(e){if(e)for(let t=0;tAt(nc);function Bi(e,t){return $n(e,null,t)}function tf(e,t){return $n(e,null,{flush:"post"})}function Fe(e,t,n){return $n(e,t,n)}function $n(e,t,n=te){const{immediate:s,deep:r,flush:i,once:o}=n,l=ce({},n),c=t&&s||!t&&i!=="post";let u;if(Gt){if(i==="sync"){const S=sc();u=S.__watcherHandles||(S.__watcherHandles=[])}else if(!c){const S=()=>{};return S.stop=Ue,S.resume=Ue,S.pause=Ue,S}}const a=fe;l.call=(S,L,O)=>He(S,a,L,O);let h=!1;i==="post"?l.scheduler=S=>{Ce(S,a&&a.suspense)}:i!=="sync"&&(h=!0,l.scheduler=(S,L)=>{L?S():Hs(S)}),l.augmentJob=S=>{t&&(S.flags|=4),h&&(S.flags|=2,a&&(S.id=a.uid,S.i=a))};const v=pl(e,t,l);return Gt&&(u?u.push(v):c&&v()),v}function rc(e,t,n){const s=this.proxy,r=re(e)?e.includes(".")?ki(s,e):()=>s[e]:e.bind(s,s);let i;q(t)?i=t:(i=t.handler,n=t);const o=zt(this),l=$n(r,i.bind(s),n);return o(),l}function ki(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;rt==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Le(t)}Modifiers`]||e[`${st(t)}Modifiers`];function oc(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||te;let r=n;const i=t.startsWith("update:"),o=i&&ic(s,t.slice(7));o&&(o.trim&&(r=n.map(a=>re(a)?a.trim():a)),o.number&&(r=n.map(Co)));let l,c=s[l=dn(t)]||s[l=dn(Le(t))];!c&&i&&(c=s[l=dn(st(t))]),c&&He(c,e,6,r);const u=s[l+"Once"];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,He(u,e,6,r)}}function Wi(e,t,n=!1){const s=t.emitsCache,r=s.get(e);if(r!==void 0)return r;const i=e.emits;let o={},l=!1;if(!q(e)){const c=u=>{const a=Wi(u,t,!0);a&&(l=!0,ce(o,a))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!i&&!l?(ne(e)&&s.set(e,null),null):(B(i)?i.forEach(c=>o[c]=null):ce(o,i),ne(e)&&s.set(e,o),o)}function Dn(e,t){return!e||!Xt(t)?!1:(t=t.slice(2).replace(/Once$/,""),z(e,t[0].toLowerCase()+t.slice(1))||z(e,st(t))||z(e,t))}function zn(e){const{type:t,vnode:n,proxy:s,withProxy:r,propsOptions:[i],slots:o,attrs:l,emit:c,render:u,renderCache:a,props:h,data:v,setupState:S,ctx:L,inheritAttrs:O}=e,G=wn(e);let U,W;try{if(n.shapeFlag&4){const p=r||s,A=p;U=Me(u.call(A,p,a,h,S,v,L)),W=l}else{const p=t;U=Me(p.length>1?p(h,{attrs:l,slots:o,emit:c}):p(h,null)),W=t.props?l:lc(l)}}catch(p){jt.length=0,Ln(p,e,1),U=de(ye)}let g=U;if(W&&O!==!1){const p=Object.keys(W),{shapeFlag:A}=g;p.length&&A&7&&(i&&p.some(Ts)&&(W=cc(W,i)),g=nt(g,W,!1,!0))}return n.dirs&&(g=nt(g,null,!1,!0),g.dirs=g.dirs?g.dirs.concat(n.dirs):n.dirs),n.transition&&kt(g,n.transition),U=g,wn(G),U}const lc=e=>{let t;for(const n in e)(n==="class"||n==="style"||Xt(n))&&((t||(t={}))[n]=e[n]);return t},cc=(e,t)=>{const n={};for(const s in e)(!Ts(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function ac(e,t,n){const{props:s,children:r,component:i}=e,{props:o,children:l,patchFlag:c}=t,u=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?lr(s,o,u):!!o;if(c&8){const a=t.dynamicProps;for(let h=0;he.__isSuspense;function Gi(e,t){t&&t.pendingBranch?B(e)?t.effects.push(...e):t.effects.push(e):yl(e)}const Se=Symbol.for("v-fgt"),gt=Symbol.for("v-txt"),ye=Symbol.for("v-cmt"),Dt=Symbol.for("v-stc"),jt=[];let Ae=null;function _s(e=!1){jt.push(Ae=e?null:[])}function fc(){jt.pop(),Ae=jt[jt.length-1]||null}let Kt=1;function cr(e,t=!1){Kt+=e,e<0&&Ae&&t&&(Ae.hasOnce=!0)}function Yi(e){return e.dynamicChildren=Kt>0?Ae||St:null,fc(),Kt>0&&Ae&&Ae.push(e),e}function nf(e,t,n,s,r,i){return Yi(Ji(e,t,n,s,r,i,!0))}function bs(e,t,n,s,r){return Yi(de(e,t,n,s,r,!0))}function qt(e){return e?e.__v_isVNode===!0:!1}function ut(e,t){return e.type===t.type&&e.key===t.key}const Xi=({key:e})=>e??null,pn=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?re(e)||le(e)||q(e)?{i:ve,r:e,k:t,f:!!n}:e:null);function Ji(e,t=null,n=null,s=0,r=null,i=e===Se?0:1,o=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Xi(t),ref:t&&pn(t),scopeId:di,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:ve};return l?(Ds(c,n),i&128&&e.normalize(c)):n&&(c.shapeFlag|=re(n)?8:16),Kt>0&&!o&&Ae&&(c.patchFlag>0||i&6)&&c.patchFlag!==32&&Ae.push(c),c}const de=uc;function uc(e,t=null,n=null,s=0,r=null,i=!1){if((!e||e===Ei)&&(e=ye),qt(e)){const l=nt(e,t,!0);return n&&Ds(l,n),Kt>0&&!i&&Ae&&(l.shapeFlag&6?Ae[Ae.indexOf(e)]=l:Ae.push(l)),l.patchFlag=-2,l}if(wc(e)&&(e=e.__vccOpts),t){t=dc(t);let{class:l,style:c}=t;l&&!re(l)&&(t.class=Os(l)),ne(c)&&(Fs(c)&&!B(c)&&(c=ce({},c)),t.style=Rs(c))}const o=re(e)?1:qi(e)?128:hi(e)?64:ne(e)?4:q(e)?2:0;return Ji(e,t,n,s,r,o,i,!0)}function dc(e){return e?Fs(e)||Li(e)?ce({},e):e:null}function nt(e,t,n=!1,s=!1){const{props:r,ref:i,patchFlag:o,children:l,transition:c}=e,u=t?hc(r||{},t):r,a={__v_isVNode:!0,__v_skip:!0,type:e.type,props:u,key:u&&Xi(u),ref:t&&t.ref?n&&i?B(i)?i.concat(pn(t)):[i,pn(t)]:pn(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Se?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&nt(e.ssContent),ssFallback:e.ssFallback&&nt(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&s&&kt(a,c.clone(a)),a}function zi(e=" ",t=0){return de(gt,null,e,t)}function sf(e,t){const n=de(Dt,null,e);return n.staticCount=t,n}function rf(e="",t=!1){return t?(_s(),bs(ye,null,e)):de(ye,null,e)}function Me(e){return e==null||typeof e=="boolean"?de(ye):B(e)?de(Se,null,e.slice()):qt(e)?Qe(e):de(gt,null,String(e))}function Qe(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:nt(e)}function Ds(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(B(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),Ds(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!Li(t)?t._ctx=ve:r===3&&ve&&(ve.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else q(t)?(t={default:t,_ctx:ve},n=32):(t=String(t),s&64?(n=16,t=[zi(t)]):n=8);e.children=t,e.shapeFlag|=n}function hc(...e){const t={};for(let n=0;nfe||ve;let xn,ws;{const e=An(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),i=>{r.length>1?r.forEach(o=>o(i)):r[0](i)}};xn=t("__VUE_INSTANCE_SETTERS__",n=>fe=n),ws=t("__VUE_SSR_SETTERS__",n=>Gt=n)}const zt=e=>{const t=fe;return xn(e),e.scope.on(),()=>{e.scope.off(),xn(t)}},ar=()=>{fe&&fe.scope.off(),xn(null)};function Qi(e){return e.vnode.shapeFlag&4}let Gt=!1;function yc(e,t=!1,n=!1){t&&ws(t);const{props:s,children:r}=e.vnode,i=Qi(e);Gl(e,s,i,t),zl(e,r,n);const o=i?vc(e,t):void 0;return t&&ws(!1),o}function vc(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,$l);const{setup:s}=n;if(s){rt();const r=e.setupContext=s.length>1?eo(e):null,i=zt(e),o=Jt(s,e,0,[e.props,r]),l=$r(o);if(it(),i(),(l||e.sp)&&!pt(e)&&bi(e),l){if(o.then(ar,ar),t)return o.then(c=>{fr(e,c)}).catch(c=>{Ln(c,e,0)});e.asyncDep=o}else fr(e,o)}else Zi(e)}function fr(e,t,n){q(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:ne(t)&&(e.setupState=ci(t)),Zi(e)}function Zi(e,t,n){const s=e.type;e.render||(e.render=s.render||Ue);{const r=zt(e);rt();try{jl(e)}finally{it(),r()}}}const _c={get(e,t){return ge(e,"get",""),e[t]}};function eo(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,_c),slots:e.slots,emit:e.emit,expose:t}}function js(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(ci(hn(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in $t)return $t[n](e)},has(t,n){return n in t||n in $t}})):e.proxy}function bc(e,t=!0){return q(e)?e.displayName||e.name:e.name||t&&e.__name}function wc(e){return q(e)&&"__vccOpts"in e}const oe=(e,t)=>dl(e,t,Gt);function Ss(e,t,n){const s=arguments.length;return s===2?ne(t)&&!B(t)?qt(t)?de(e,null,[t]):de(e,t):de(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&qt(n)&&(n=[n]),de(e,t,n))}const Sc="3.5.13";/** +* @vue/runtime-dom v3.5.13 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let xs;const ur=typeof window<"u"&&window.trustedTypes;if(ur)try{xs=ur.createPolicy("vue",{createHTML:e=>e})}catch{}const to=xs?e=>xs.createHTML(e):e=>e,xc="http://www.w3.org/2000/svg",Ec="http://www.w3.org/1998/Math/MathML",We=typeof document<"u"?document:null,dr=We&&We.createElement("template"),Tc={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?We.createElementNS(xc,e):t==="mathml"?We.createElementNS(Ec,e):n?We.createElement(e,{is:n}):We.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>We.createTextNode(e),createComment:e=>We.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>We.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,i){const o=n?n.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===i||!(r=r.nextSibling)););else{dr.innerHTML=to(s==="svg"?`${e}`:s==="mathml"?`${e}`:e);const l=dr.content;if(s==="svg"||s==="mathml"){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},Xe="transition",Pt="animation",Yt=Symbol("_vtc"),no={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Cc=ce({},pi,no),Ac=e=>(e.displayName="Transition",e.props=Cc,e),of=Ac((e,{slots:t})=>Ss(Sl,Rc(e),t)),ct=(e,t=[])=>{B(e)?e.forEach(n=>n(...t)):e&&e(...t)},hr=e=>e?B(e)?e.some(t=>t.length>1):e.length>1:!1;function Rc(e){const t={};for(const b in e)b in no||(t[b]=e[b]);if(e.css===!1)return t;const{name:n="v",type:s,duration:r,enterFromClass:i=`${n}-enter-from`,enterActiveClass:o=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:c=i,appearActiveClass:u=o,appearToClass:a=l,leaveFromClass:h=`${n}-leave-from`,leaveActiveClass:v=`${n}-leave-active`,leaveToClass:S=`${n}-leave-to`}=e,L=Oc(r),O=L&&L[0],G=L&&L[1],{onBeforeEnter:U,onEnter:W,onEnterCancelled:g,onLeave:p,onLeaveCancelled:A,onBeforeAppear:D=U,onAppear:j=W,onAppearCancelled:K=g}=t,I=(b,V,se,ie)=>{b._enterCancelled=ie,at(b,V?a:l),at(b,V?u:o),se&&se()},_=(b,V)=>{b._isLeaving=!1,at(b,h),at(b,S),at(b,v),V&&V()},P=b=>(V,se)=>{const ie=b?j:W,H=()=>I(V,b,se);ct(ie,[V,H]),pr(()=>{at(V,b?c:i),ke(V,b?a:l),hr(ie)||gr(V,s,O,H)})};return ce(t,{onBeforeEnter(b){ct(U,[b]),ke(b,i),ke(b,o)},onBeforeAppear(b){ct(D,[b]),ke(b,c),ke(b,u)},onEnter:P(!1),onAppear:P(!0),onLeave(b,V){b._isLeaving=!0;const se=()=>_(b,V);ke(b,h),b._enterCancelled?(ke(b,v),vr()):(vr(),ke(b,v)),pr(()=>{b._isLeaving&&(at(b,h),ke(b,S),hr(p)||gr(b,s,G,se))}),ct(p,[b,se])},onEnterCancelled(b){I(b,!1,void 0,!0),ct(g,[b])},onAppearCancelled(b){I(b,!0,void 0,!0),ct(K,[b])},onLeaveCancelled(b){_(b),ct(A,[b])}})}function Oc(e){if(e==null)return null;if(ne(e))return[Qn(e.enter),Qn(e.leave)];{const t=Qn(e);return[t,t]}}function Qn(e){return Ao(e)}function ke(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[Yt]||(e[Yt]=new Set)).add(t)}function at(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.remove(s));const n=e[Yt];n&&(n.delete(t),n.size||(e[Yt]=void 0))}function pr(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let Mc=0;function gr(e,t,n,s){const r=e._endId=++Mc,i=()=>{r===e._endId&&s()};if(n!=null)return setTimeout(i,n);const{type:o,timeout:l,propCount:c}=Ic(e,t);if(!o)return s();const u=o+"end";let a=0;const h=()=>{e.removeEventListener(u,v),i()},v=S=>{S.target===e&&++a>=c&&h()};setTimeout(()=>{a(n[L]||"").split(", "),r=s(`${Xe}Delay`),i=s(`${Xe}Duration`),o=mr(r,i),l=s(`${Pt}Delay`),c=s(`${Pt}Duration`),u=mr(l,c);let a=null,h=0,v=0;t===Xe?o>0&&(a=Xe,h=o,v=i.length):t===Pt?u>0&&(a=Pt,h=u,v=c.length):(h=Math.max(o,u),a=h>0?o>u?Xe:Pt:null,v=a?a===Xe?i.length:c.length:0);const S=a===Xe&&/\b(transform|all)(,|$)/.test(s(`${Xe}Property`).toString());return{type:a,timeout:h,propCount:v,hasTransform:S}}function mr(e,t){for(;e.lengthyr(n)+yr(e[s])))}function yr(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function vr(){return document.body.offsetHeight}function Lc(e,t,n){const s=e[Yt];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const _r=Symbol("_vod"),Pc=Symbol("_vsh"),Nc=Symbol(""),Fc=/(^|;)\s*display\s*:/;function Hc(e,t,n){const s=e.style,r=re(n);let i=!1;if(n&&!r){if(t)if(re(t))for(const o of t.split(";")){const l=o.slice(0,o.indexOf(":")).trim();n[l]==null&&gn(s,l,"")}else for(const o in t)n[o]==null&&gn(s,o,"");for(const o in n)o==="display"&&(i=!0),gn(s,o,n[o])}else if(r){if(t!==n){const o=s[Nc];o&&(n+=";"+o),s.cssText=n,i=Fc.test(n)}}else t&&e.removeAttribute("style");_r in e&&(e[_r]=i?s.display:"",e[Pc]&&(s.display="none"))}const br=/\s*!important$/;function gn(e,t,n){if(B(n))n.forEach(s=>gn(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=$c(e,t);br.test(n)?e.setProperty(st(s),n.replace(br,""),"important"):e[s]=n}}const wr=["Webkit","Moz","ms"],Zn={};function $c(e,t){const n=Zn[t];if(n)return n;let s=Le(t);if(s!=="filter"&&s in e)return Zn[t]=s;s=Cn(s);for(let r=0;res||(Bc.then(()=>es=0),es=Date.now());function Wc(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;He(Kc(s,n.value),t,5,[s])};return n.value=e,n.attached=kc(),n}function Kc(e,t){if(B(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const Ar=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,qc=(e,t,n,s,r,i)=>{const o=r==="svg";t==="class"?Lc(e,s,o):t==="style"?Hc(e,n,s):Xt(t)?Ts(t)||Vc(e,t,n,s,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Gc(e,t,s,o))?(Er(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&xr(e,t,s,o,i,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!re(s))?Er(e,Le(t),s,i,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),xr(e,t,s,o))};function Gc(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&Ar(t)&&q(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return Ar(t)&&re(n)?!1:t in e}const Yc=["ctrl","shift","alt","meta"],Xc={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>Yc.some(n=>e[`${n}Key`]&&!t.includes(n))},lf=(e,t)=>{const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=(r,...i)=>{for(let o=0;o{const n=e._withKeys||(e._withKeys={}),s=t.join(".");return n[s]||(n[s]=r=>{if(!("key"in r))return;const i=st(r.key);if(t.some(o=>o===i||Jc[o]===i))return e(r)})},zc=ce({patchProp:qc},Tc);let ts,Rr=!1;function Qc(){return ts=Rr?ts:Zl(zc),Rr=!0,ts}const af=(...e)=>{const t=Qc().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=ea(s);if(r)return n(r,!0,Zc(r))},t};function Zc(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function ea(e){return re(e)?document.querySelector(e):e}const ff=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n},ta=window.__VP_SITE_DATA__;function Vs(e){return Wr()?(Ho(e),!0):!1}function tt(e){return typeof e=="function"?e():li(e)}const so=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const na=Object.prototype.toString,sa=e=>na.call(e)==="[object Object]",ro=()=>{},Or=ra();function ra(){var e,t;return so&&((e=window==null?void 0:window.navigator)==null?void 0:e.userAgent)&&(/iP(?:ad|hone|od)/.test(window.navigator.userAgent)||((t=window==null?void 0:window.navigator)==null?void 0:t.maxTouchPoints)>2&&/iPad|Macintosh/.test(window==null?void 0:window.navigator.userAgent))}function ia(e,t){function n(...s){return new Promise((r,i)=>{Promise.resolve(e(()=>t.apply(this,s),{fn:t,thisArg:this,args:s})).then(r).catch(i)})}return n}const io=e=>e();function oa(e=io){const t=ue(!0);function n(){t.value=!1}function s(){t.value=!0}const r=(...i)=>{t.value&&e(...i)};return{isActive:In(t),pause:n,resume:s,eventFilter:r}}function la(e){return jn()}function oo(...e){if(e.length!==1)return al(...e);const t=e[0];return typeof t=="function"?In(ol(()=>({get:t,set:ro}))):ue(t)}function ca(e,t,n={}){const{eventFilter:s=io,...r}=n;return Fe(e,ia(s,t),r)}function aa(e,t,n={}){const{eventFilter:s,...r}=n,{eventFilter:i,pause:o,resume:l,isActive:c}=oa(s);return{stop:ca(e,t,{...r,eventFilter:i}),pause:o,resume:l,isActive:c}}function Us(e,t=!0,n){la()?Ot(e,n):t?e():Pn(e)}const qe=so?window:void 0;function lo(e){var t;const n=tt(e);return(t=n==null?void 0:n.$el)!=null?t:n}function Rt(...e){let t,n,s,r;if(typeof e[0]=="string"||Array.isArray(e[0])?([n,s,r]=e,t=qe):[t,n,s,r]=e,!t)return ro;Array.isArray(n)||(n=[n]),Array.isArray(s)||(s=[s]);const i=[],o=()=>{i.forEach(a=>a()),i.length=0},l=(a,h,v,S)=>(a.addEventListener(h,v,S),()=>a.removeEventListener(h,v,S)),c=Fe(()=>[lo(t),tt(r)],([a,h])=>{if(o(),!a)return;const v=sa(h)?{...h}:h;i.push(...n.flatMap(S=>s.map(L=>l(a,S,L,v))))},{immediate:!0,flush:"post"}),u=()=>{c(),o()};return Vs(u),u}function fa(e){return typeof e=="function"?e:typeof e=="string"?t=>t.key===e:Array.isArray(e)?t=>e.includes(t.key):()=>!0}function uf(...e){let t,n,s={};e.length===3?(t=e[0],n=e[1],s=e[2]):e.length===2?typeof e[1]=="object"?(t=!0,n=e[0],s=e[1]):(t=e[0],n=e[1]):(t=!0,n=e[0]);const{target:r=qe,eventName:i="keydown",passive:o=!1,dedupe:l=!1}=s,c=fa(t);return Rt(r,i,a=>{a.repeat&&tt(l)||c(a)&&n(a)},o)}function ua(){const e=ue(!1),t=jn();return t&&Ot(()=>{e.value=!0},t),e}function da(e){const t=ua();return oe(()=>(t.value,!!e()))}function co(e,t={}){const{window:n=qe}=t,s=da(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function");let r;const i=ue(!1),o=u=>{i.value=u.matches},l=()=>{r&&("removeEventListener"in r?r.removeEventListener("change",o):r.removeListener(o))},c=Bi(()=>{s.value&&(l(),r=n.matchMedia(tt(e)),"addEventListener"in r?r.addEventListener("change",o):r.addListener(o),i.value=r.matches)});return Vs(()=>{c(),l(),r=void 0}),i}const cn=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},an="__vueuse_ssr_handlers__",ha=pa();function pa(){return an in cn||(cn[an]=cn[an]||{}),cn[an]}function ao(e,t){return ha[e]||t}function Bs(e){return co("(prefers-color-scheme: dark)",e)}function ga(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}const ma={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},Mr="vueuse-storage";function ya(e,t,n,s={}){var r;const{flush:i="pre",deep:o=!0,listenToStorageChanges:l=!0,writeDefaults:c=!0,mergeDefaults:u=!1,shallow:a,window:h=qe,eventFilter:v,onError:S=_=>{console.error(_)},initOnMounted:L}=s,O=(a?ii:ue)(typeof t=="function"?t():t);if(!n)try{n=ao("getDefaultStorage",()=>{var _;return(_=qe)==null?void 0:_.localStorage})()}catch(_){S(_)}if(!n)return O;const G=tt(t),U=ga(G),W=(r=s.serializer)!=null?r:ma[U],{pause:g,resume:p}=aa(O,()=>D(O.value),{flush:i,deep:o,eventFilter:v});h&&l&&Us(()=>{n instanceof Storage?Rt(h,"storage",K):Rt(h,Mr,I),L&&K()}),L||K();function A(_,P){if(h){const b={key:e,oldValue:_,newValue:P,storageArea:n};h.dispatchEvent(n instanceof Storage?new StorageEvent("storage",b):new CustomEvent(Mr,{detail:b}))}}function D(_){try{const P=n.getItem(e);if(_==null)A(P,null),n.removeItem(e);else{const b=W.write(_);P!==b&&(n.setItem(e,b),A(P,b))}}catch(P){S(P)}}function j(_){const P=_?_.newValue:n.getItem(e);if(P==null)return c&&G!=null&&n.setItem(e,W.write(G)),G;if(!_&&u){const b=W.read(P);return typeof u=="function"?u(b,G):U==="object"&&!Array.isArray(b)?{...G,...b}:b}else return typeof P!="string"?P:W.read(P)}function K(_){if(!(_&&_.storageArea!==n)){if(_&&_.key==null){O.value=G;return}if(!(_&&_.key!==e)){g();try{(_==null?void 0:_.newValue)!==W.write(O.value)&&(O.value=j(_))}catch(P){S(P)}finally{_?Pn(p):p()}}}}function I(_){K(_.detail)}return O}const va="*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}";function _a(e={}){const{selector:t="html",attribute:n="class",initialValue:s="auto",window:r=qe,storage:i,storageKey:o="vueuse-color-scheme",listenToStorageChanges:l=!0,storageRef:c,emitAuto:u,disableTransition:a=!0}=e,h={auto:"",light:"light",dark:"dark",...e.modes||{}},v=Bs({window:r}),S=oe(()=>v.value?"dark":"light"),L=c||(o==null?oo(s):ya(o,s,i,{window:r,listenToStorageChanges:l})),O=oe(()=>L.value==="auto"?S.value:L.value),G=ao("updateHTMLAttrs",(p,A,D)=>{const j=typeof p=="string"?r==null?void 0:r.document.querySelector(p):lo(p);if(!j)return;const K=new Set,I=new Set;let _=null;if(A==="class"){const b=D.split(/\s/g);Object.values(h).flatMap(V=>(V||"").split(/\s/g)).filter(Boolean).forEach(V=>{b.includes(V)?K.add(V):I.add(V)})}else _={key:A,value:D};if(K.size===0&&I.size===0&&_===null)return;let P;a&&(P=r.document.createElement("style"),P.appendChild(document.createTextNode(va)),r.document.head.appendChild(P));for(const b of K)j.classList.add(b);for(const b of I)j.classList.remove(b);_&&j.setAttribute(_.key,_.value),a&&(r.getComputedStyle(P).opacity,document.head.removeChild(P))});function U(p){var A;G(t,n,(A=h[p])!=null?A:p)}function W(p){e.onChanged?e.onChanged(p,U):U(p)}Fe(O,W,{flush:"post",immediate:!0}),Us(()=>W(O.value));const g=oe({get(){return u?L.value:O.value},set(p){L.value=p}});try{return Object.assign(g,{store:L,system:S,state:O})}catch{return g}}function ba(e={}){const{valueDark:t="dark",valueLight:n="",window:s=qe}=e,r=_a({...e,onChanged:(l,c)=>{var u;e.onChanged?(u=e.onChanged)==null||u.call(e,l==="dark",c,l):c(l)},modes:{dark:t,light:n}}),i=oe(()=>r.system?r.system.value:Bs({window:s}).value?"dark":"light");return oe({get(){return r.value==="dark"},set(l){const c=l?"dark":"light";i.value===c?r.value="auto":r.value=c}})}function ns(e){return typeof Window<"u"&&e instanceof Window?e.document.documentElement:typeof Document<"u"&&e instanceof Document?e.documentElement:e}function fo(e){const t=window.getComputedStyle(e);if(t.overflowX==="scroll"||t.overflowY==="scroll"||t.overflowX==="auto"&&e.clientWidth1?!0:(t.preventDefault&&t.preventDefault(),!1)}const ss=new WeakMap;function df(e,t=!1){const n=ue(t);let s=null,r="";Fe(oo(e),l=>{const c=ns(tt(l));if(c){const u=c;if(ss.get(u)||ss.set(u,u.style.overflow),u.style.overflow!=="hidden"&&(r=u.style.overflow),u.style.overflow==="hidden")return n.value=!0;if(n.value)return u.style.overflow="hidden"}},{immediate:!0});const i=()=>{const l=ns(tt(e));!l||n.value||(Or&&(s=Rt(l,"touchmove",c=>{wa(c)},{passive:!1})),l.style.overflow="hidden",n.value=!0)},o=()=>{const l=ns(tt(e));!l||!n.value||(Or&&(s==null||s()),l.style.overflow=r,ss.delete(l),n.value=!1)};return Vs(o),oe({get(){return n.value},set(l){l?i():o()}})}function hf(e={}){const{window:t=qe,behavior:n="auto"}=e;if(!t)return{x:ue(0),y:ue(0)};const s=ue(t.scrollX),r=ue(t.scrollY),i=oe({get(){return s.value},set(l){scrollTo({left:l,behavior:n})}}),o=oe({get(){return r.value},set(l){scrollTo({top:l,behavior:n})}});return Rt(t,"scroll",()=>{s.value=t.scrollX,r.value=t.scrollY},{capture:!1,passive:!0}),{x:i,y:o}}function pf(e={}){const{window:t=qe,initialWidth:n=Number.POSITIVE_INFINITY,initialHeight:s=Number.POSITIVE_INFINITY,listenOrientation:r=!0,includeScrollbar:i=!0,type:o="inner"}=e,l=ue(n),c=ue(s),u=()=>{t&&(o==="outer"?(l.value=t.outerWidth,c.value=t.outerHeight):i?(l.value=t.innerWidth,c.value=t.innerHeight):(l.value=t.document.documentElement.clientWidth,c.value=t.document.documentElement.clientHeight))};if(u(),Us(u),Rt("resize",u,{passive:!0}),r){const a=co("(orientation: portrait)");Fe(a,()=>u())}return{width:l,height:c}}const rs={BASE_URL:"/",DEV:!1,MODE:"production",PROD:!0,SSR:!1};var is={};const uo=/^(?:[a-z]+:|\/\/)/i,Sa="vitepress-theme-appearance",xa=/#.*$/,Ea=/[?#].*$/,Ta=/(?:(^|\/)index)?\.(?:md|html)$/,pe=typeof document<"u",ho={relativePath:"404.md",filePath:"",title:"404",description:"Not Found",headers:[],frontmatter:{sidebar:!1,layout:"page"},lastUpdated:0,isNotFound:!0};function Ca(e,t,n=!1){if(t===void 0)return!1;if(e=Ir(`/${e}`),n)return new RegExp(t).test(e);if(Ir(t)!==e)return!1;const s=t.match(xa);return s?(pe?location.hash:"")===s[0]:!0}function Ir(e){return decodeURI(e).replace(Ea,"").replace(Ta,"$1")}function Aa(e){return uo.test(e)}function Ra(e,t){return Object.keys((e==null?void 0:e.locales)||{}).find(n=>n!=="root"&&!Aa(n)&&Ca(t,`/${n}/`,!0))||"root"}function Oa(e,t){var s,r,i,o,l,c,u;const n=Ra(e,t);return Object.assign({},e,{localeIndex:n,lang:((s=e.locales[n])==null?void 0:s.lang)??e.lang,dir:((r=e.locales[n])==null?void 0:r.dir)??e.dir,title:((i=e.locales[n])==null?void 0:i.title)??e.title,titleTemplate:((o=e.locales[n])==null?void 0:o.titleTemplate)??e.titleTemplate,description:((l=e.locales[n])==null?void 0:l.description)??e.description,head:go(e.head,((c=e.locales[n])==null?void 0:c.head)??[]),themeConfig:{...e.themeConfig,...(u=e.locales[n])==null?void 0:u.themeConfig}})}function po(e,t){const n=t.title||e.title,s=t.titleTemplate??e.titleTemplate;if(typeof s=="string"&&s.includes(":title"))return s.replace(/:title/g,n);const r=Ma(e.title,s);return n===r.slice(3)?n:`${n}${r}`}function Ma(e,t){return t===!1?"":t===!0||t===void 0?` | ${e}`:e===t?"":` | ${t}`}function Ia(e,t){const[n,s]=t;if(n!=="meta")return!1;const r=Object.entries(s)[0];return r==null?!1:e.some(([i,o])=>i===n&&o[r[0]]===r[1])}function go(e,t){return[...e.filter(n=>!Ia(t,n)),...t]}const La=/[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g,Pa=/^[a-z]:/i;function Lr(e){const t=Pa.exec(e),n=t?t[0]:"";return n+e.slice(n.length).replace(La,"_").replace(/(^|\/)_+(?=[^/]*$)/,"$1")}const os=new Set;function Na(e){if(os.size===0){const n=typeof process=="object"&&(is==null?void 0:is.VITE_EXTRA_EXTENSIONS)||(rs==null?void 0:rs.VITE_EXTRA_EXTENSIONS)||"";("3g2,3gp,aac,ai,apng,au,avif,bin,bmp,cer,class,conf,crl,css,csv,dll,doc,eps,epub,exe,gif,gz,ics,ief,jar,jpe,jpeg,jpg,js,json,jsonld,m4a,man,mid,midi,mjs,mov,mp2,mp3,mp4,mpe,mpeg,mpg,mpp,oga,ogg,ogv,ogx,opus,otf,p10,p7c,p7m,p7s,pdf,png,ps,qt,roff,rtf,rtx,ser,svg,t,tif,tiff,tr,ts,tsv,ttf,txt,vtt,wav,weba,webm,webp,woff,woff2,xhtml,xml,yaml,yml,zip"+(n&&typeof n=="string"?","+n:"")).split(",").forEach(s=>os.add(s))}const t=e.split(".").pop();return t==null||!os.has(t.toLowerCase())}const Fa=Symbol(),mt=ii(ta);function gf(e){const t=oe(()=>Oa(mt.value,e.data.relativePath)),n=t.value.appearance,s=n==="force-dark"?ue(!0):n==="force-auto"?Bs():n?ba({storageKey:Sa,initialValue:()=>n==="dark"?"dark":"auto",...typeof n=="object"?n:{}}):ue(!1),r=ue(pe?location.hash:"");return pe&&window.addEventListener("hashchange",()=>{r.value=location.hash}),Fe(()=>e.data,()=>{r.value=pe?location.hash:""}),{site:t,theme:oe(()=>t.value.themeConfig),page:oe(()=>e.data),frontmatter:oe(()=>e.data.frontmatter),params:oe(()=>e.data.params),lang:oe(()=>t.value.lang),dir:oe(()=>e.data.frontmatter.dir||t.value.dir),localeIndex:oe(()=>t.value.localeIndex||"root"),title:oe(()=>po(t.value,e.data)),description:oe(()=>e.data.description||t.value.description),isDark:s,hash:oe(()=>r.value)}}function Ha(){const e=At(Fa);if(!e)throw new Error("vitepress data not properly injected in app");return e}function $a(e,t){return`${e}${t}`.replace(/\/+/g,"/")}function Pr(e){return uo.test(e)||!e.startsWith("/")?e:$a(mt.value.base,e)}function Da(e){let t=e.replace(/\.html$/,"");if(t=decodeURIComponent(t),t=t.replace(/\/$/,"/index"),pe){const n="/";t=Lr(t.slice(n.length).replace(/\//g,"_")||"index")+".md";let s=__VP_HASH_MAP__[t.toLowerCase()];if(s||(t=t.endsWith("_index.md")?t.slice(0,-9)+".md":t.slice(0,-3)+"_index.md",s=__VP_HASH_MAP__[t.toLowerCase()]),!s)return null;t=`${n}assets/${t}.${s}.js`}else t=`./${Lr(t.slice(1).replace(/\//g,"_"))}.md.js`;return t}let mn=[];function mf(e){mn.push(e),Hn(()=>{mn=mn.filter(t=>t!==e)})}function ja(){let e=mt.value.scrollOffset,t=0,n=24;if(typeof e=="object"&&"padding"in e&&(n=e.padding,e=e.selector),typeof e=="number")t=e;else if(typeof e=="string")t=Nr(e,n);else if(Array.isArray(e))for(const s of e){const r=Nr(s,n);if(r){t=r;break}}return t}function Nr(e,t){const n=document.querySelector(e);if(!n)return 0;const s=n.getBoundingClientRect().bottom;return s<0?0:s+t}const Va=Symbol(),mo="http://a.com",Ua=()=>({path:"/",component:null,data:ho});function yf(e,t){const n=Mn(Ua()),s={route:n,go:r};async function r(l=pe?location.href:"/"){var c,u;l=ls(l),await((c=s.onBeforeRouteChange)==null?void 0:c.call(s,l))!==!1&&(pe&&l!==ls(location.href)&&(history.replaceState({scrollPosition:window.scrollY},""),history.pushState({},"",l)),await o(l),await((u=s.onAfterRouteChanged)==null?void 0:u.call(s,l)))}let i=null;async function o(l,c=0,u=!1){var v,S;if(await((v=s.onBeforePageLoad)==null?void 0:v.call(s,l))===!1)return;const a=new URL(l,mo),h=i=a.pathname;try{let L=await e(h);if(!L)throw new Error(`Page not found: ${h}`);if(i===h){i=null;const{default:O,__pageData:G}=L;if(!O)throw new Error(`Invalid route component: ${O}`);await((S=s.onAfterPageLoad)==null?void 0:S.call(s,l)),n.path=pe?h:Pr(h),n.component=hn(O),n.data=hn(G),pe&&Pn(()=>{let U=mt.value.base+G.relativePath.replace(/(?:(^|\/)index)?\.md$/,"$1");if(!mt.value.cleanUrls&&!U.endsWith("/")&&(U+=".html"),U!==a.pathname&&(a.pathname=U,l=U+a.search+a.hash,history.replaceState({},"",l)),a.hash&&!c){let W=null;try{W=document.getElementById(decodeURIComponent(a.hash).slice(1))}catch(g){console.warn(g)}if(W){Fr(W,a.hash);return}}window.scrollTo(0,c)})}}catch(L){if(!/fetch|Page not found/.test(L.message)&&!/^\/404(\.html|\/)?$/.test(l)&&console.error(L),!u)try{const O=await fetch(mt.value.base+"hashmap.json");window.__VP_HASH_MAP__=await O.json(),await o(l,c,!0);return}catch{}if(i===h){i=null,n.path=pe?h:Pr(h),n.component=t?hn(t):null;const O=pe?h.replace(/(^|\/)$/,"$1index").replace(/(\.html)?$/,".md").replace(/^\//,""):"404.md";n.data={...ho,relativePath:O}}}}return pe&&(history.state===null&&history.replaceState({},""),window.addEventListener("click",l=>{if(l.defaultPrevented||!(l.target instanceof Element)||l.target.closest("button")||l.button!==0||l.ctrlKey||l.shiftKey||l.altKey||l.metaKey)return;const c=l.target.closest("a");if(!c||c.closest(".vp-raw")||c.hasAttribute("download")||c.hasAttribute("target"))return;const u=c.getAttribute("href")??(c instanceof SVGAElement?c.getAttribute("xlink:href"):null);if(u==null)return;const{href:a,origin:h,pathname:v,hash:S,search:L}=new URL(u,c.baseURI),O=new URL(location.href);h===O.origin&&Na(v)&&(l.preventDefault(),v===O.pathname&&L===O.search?(S!==O.hash&&(history.pushState({},"",a),window.dispatchEvent(new HashChangeEvent("hashchange",{oldURL:O.href,newURL:a}))),S?Fr(c,S,c.classList.contains("header-anchor")):window.scrollTo(0,0)):r(a))},{capture:!0}),window.addEventListener("popstate",async l=>{var c;l.state!==null&&(await o(ls(location.href),l.state&&l.state.scrollPosition||0),(c=s.onAfterRouteChanged)==null||c.call(s,location.href))}),window.addEventListener("hashchange",l=>{l.preventDefault()})),s}function Ba(){const e=At(Va);if(!e)throw new Error("useRouter() is called without provider.");return e}function yo(){return Ba().route}function Fr(e,t,n=!1){let s=null;try{s=e.classList.contains("header-anchor")?e:document.getElementById(decodeURIComponent(t).slice(1))}catch(r){console.warn(r)}if(s){let r=function(){!n||Math.abs(o-window.scrollY)>window.innerHeight?window.scrollTo(0,o):window.scrollTo({left:0,top:o,behavior:"smooth"})};const i=parseInt(window.getComputedStyle(s).paddingTop,10),o=window.scrollY+s.getBoundingClientRect().top-ja()+i;requestAnimationFrame(r)}}function ls(e){const t=new URL(e,mo);return t.pathname=t.pathname.replace(/(^|\/)index(\.html)?$/,"$1"),mt.value.cleanUrls?t.pathname=t.pathname.replace(/\.html$/,""):!t.pathname.endsWith("/")&&!t.pathname.endsWith(".html")&&(t.pathname+=".html"),t.pathname+t.search+t.hash}const fn=()=>mn.forEach(e=>e()),vf=_i({name:"VitePressContent",props:{as:{type:[Object,String],default:"div"}},setup(e){const t=yo(),{frontmatter:n,site:s}=Ha();return Fe(n,fn,{deep:!0,flush:"post"}),()=>Ss(e.as,s.value.contentProps??{style:{position:"relative"}},[t.component?Ss(t.component,{onVnodeMounted:fn,onVnodeUpdated:fn,onVnodeUnmounted:fn}):"404 Page Not Found"])}}),_f=_i({setup(e,{slots:t}){const n=ue(!1);return Ot(()=>{n.value=!0}),()=>n.value&&t.default?t.default():null}});function bf(){pe&&window.addEventListener("click",e=>{var n;const t=e.target;if(t.matches(".vp-code-group input")){const s=(n=t.parentElement)==null?void 0:n.parentElement;if(!s)return;const r=Array.from(s.querySelectorAll("input")).indexOf(t);if(r<0)return;const i=s.querySelector(".blocks");if(!i)return;const o=Array.from(i.children).find(u=>u.classList.contains("active"));if(!o)return;const l=i.children[r];if(!l||o===l)return;o.classList.remove("active"),l.classList.add("active");const c=s==null?void 0:s.querySelector(`label[for="${t.id}"]`);c==null||c.scrollIntoView({block:"nearest"})}})}function wf(){if(pe){const e=new WeakMap;window.addEventListener("click",t=>{var s;const n=t.target;if(n.matches('div[class*="language-"] > button.copy')){const r=n.parentElement,i=(s=n.nextElementSibling)==null?void 0:s.nextElementSibling;if(!r||!i)return;const o=/language-(shellscript|shell|bash|sh|zsh)/.test(r.className),l=[".vp-copy-ignore",".diff.remove"],c=i.cloneNode(!0);c.querySelectorAll(l.join(",")).forEach(a=>a.remove());let u=c.textContent||"";o&&(u=u.replace(/^ *(\$|>) /gm,"").trim()),ka(u).then(()=>{n.classList.add("copied"),clearTimeout(e.get(n));const a=setTimeout(()=>{n.classList.remove("copied"),n.blur(),e.delete(n)},2e3);e.set(n,a)})}})}}async function ka(e){try{return navigator.clipboard.writeText(e)}catch{const t=document.createElement("textarea"),n=document.activeElement;t.value=e,t.setAttribute("readonly",""),t.style.contain="strict",t.style.position="absolute",t.style.left="-9999px",t.style.fontSize="12pt";const s=document.getSelection(),r=s?s.rangeCount>0&&s.getRangeAt(0):null;document.body.appendChild(t),t.select(),t.selectionStart=0,t.selectionEnd=e.length,document.execCommand("copy"),document.body.removeChild(t),r&&(s.removeAllRanges(),s.addRange(r)),n&&n.focus()}}function Sf(e,t){let n=!0,s=[];const r=i=>{if(n){n=!1,i.forEach(l=>{const c=cs(l);for(const u of document.head.children)if(u.isEqualNode(c)){s.push(u);return}});return}const o=i.map(cs);s.forEach((l,c)=>{const u=o.findIndex(a=>a==null?void 0:a.isEqualNode(l??null));u!==-1?delete o[u]:(l==null||l.remove(),delete s[c])}),o.forEach(l=>l&&document.head.appendChild(l)),s=[...s,...o].filter(Boolean)};Bi(()=>{const i=e.data,o=t.value,l=i&&i.description,c=i&&i.frontmatter.head||[],u=po(o,i);u!==document.title&&(document.title=u);const a=l||o.description;let h=document.querySelector("meta[name=description]");h?h.getAttribute("content")!==a&&h.setAttribute("content",a):cs(["meta",{name:"description",content:a}]),r(go(o.head,Ka(c)))})}function cs([e,t,n]){const s=document.createElement(e);for(const r in t)s.setAttribute(r,t[r]);return n&&(s.innerHTML=n),e==="script"&&t.async==null&&(s.async=!1),s}function Wa(e){return e[0]==="meta"&&e[1]&&e[1].name==="description"}function Ka(e){return e.filter(t=>!Wa(t))}const as=new Set,vo=()=>document.createElement("link"),qa=e=>{const t=vo();t.rel="prefetch",t.href=e,document.head.appendChild(t)},Ga=e=>{const t=new XMLHttpRequest;t.open("GET",e,t.withCredentials=!0),t.send()};let un;const Ya=pe&&(un=vo())&&un.relList&&un.relList.supports&&un.relList.supports("prefetch")?qa:Ga;function xf(){if(!pe||!window.IntersectionObserver)return;let e;if((e=navigator.connection)&&(e.saveData||/2g/.test(e.effectiveType)))return;const t=window.requestIdleCallback||setTimeout;let n=null;const s=()=>{n&&n.disconnect(),n=new IntersectionObserver(i=>{i.forEach(o=>{if(o.isIntersecting){const l=o.target;n.unobserve(l);const{pathname:c}=l;if(!as.has(c)){as.add(c);const u=Da(c);u&&Ya(u)}}})}),t(()=>{document.querySelectorAll("#app a").forEach(i=>{const{hostname:o,pathname:l}=new URL(i.href instanceof SVGAnimatedString?i.href.animVal:i.href,i.baseURI),c=l.match(/\.\w+$/);c&&c[0]!==".html"||i.target!=="_blank"&&o===location.hostname&&(l!==location.pathname?n.observe(i):as.add(l))})})};Ot(s);const r=yo();Fe(()=>r.path,s),Hn(()=>{n&&n.disconnect()})}export{ef as $,ja as A,Xa as B,za as C,ii as D,mf as E,Se as F,de as G,Ja as H,uo as I,yo as J,hc as K,At as L,pf as M,Rs as N,uf as O,Pn as P,hf as Q,pe as R,In as S,of as T,df as U,ql as V,cf as W,Za as X,Si as Y,lf as Z,ff as _,zi as a,sf as a0,Sf as a1,Va as a2,gf as a3,Fa as a4,vf as a5,_f as a6,mt as a7,af as a8,yf as a9,Da as aa,xf as ab,wf as ac,bf as ad,Ss as ae,bs as b,nf as c,_i as d,rf as e,Na as f,Pr as g,oe as h,Aa as i,Ji as j,li as k,Ca as l,co as m,Os as n,_s as o,ue as p,Fe as q,Qa as r,Bi as s,No as t,Ha as u,Ot as v,vl as w,Hn as x,tf as y,Ll as z}; diff --git a/assets/chunks/theme.DXZpxO3K.js b/assets/chunks/theme.DXZpxO3K.js new file mode 100644 index 00000000..827a7c3c --- /dev/null +++ b/assets/chunks/theme.DXZpxO3K.js @@ -0,0 +1 @@ +import{d as m,o as a,c,r as d,n as w,a as z,t as S,b as k,w as f,e as h,T as ue,_ as g,u as $e,i as Ae,f as Be,g as de,h as y,j as l,k as r,l as q,m as re,p as T,q as D,s as X,v as O,x as Q,y as ve,z as Ee,A as Fe,B as W,F as C,C as A,D as ye,E as Z,G as _,H as E,I as Pe,J as x,K as j,L as ee,M as De,N as pe,O as Oe,P as Le,Q as Ve,R as te,S as Ge,U as Se,V as Te,W as Ue,X as je,Y as ze,Z as qe,$ as We}from"./framework.P9qPzDnn.js";const Ke=m({__name:"VPBadge",props:{text:{},type:{default:"tip"}},setup(o){return(e,t)=>(a(),c("span",{class:w(["VPBadge",e.type])},[d(e.$slots,"default",{},()=>[z(S(e.text),1)])],2))}}),Je={key:0,class:"VPBackdrop"},Re=m({__name:"VPBackdrop",props:{show:{type:Boolean}},setup(o){return(e,t)=>(a(),k(ue,{name:"fade"},{default:f(()=>[e.show?(a(),c("div",Je)):h("",!0)]),_:1}))}}),Ye=g(Re,[["__scopeId","data-v-d220041e"]]),V=$e;function Xe(o,e){let t,s=!1;return()=>{t&&clearTimeout(t),s?t=setTimeout(o,e):(o(),(s=!0)&&setTimeout(()=>s=!1,e))}}function ie(o){return/^\//.test(o)?o:`/${o}`}function fe(o){const{pathname:e,search:t,hash:s,protocol:n}=new URL(o,"http://a.com");if(Ae(o)||o.startsWith("#")||!n.startsWith("http")||!Be(e))return o;const{site:i}=V(),u=e.endsWith("/")||e.endsWith(".html")?o:o.replace(/(?:(^\.+)\/)?.*$/,`$1${e.replace(/(\.md)?$/,i.value.cleanUrls?"":".html")}${t}${s}`);return de(u)}function J({correspondingLink:o=!1}={}){const{site:e,localeIndex:t,page:s,theme:n,hash:i}=V(),u=y(()=>{var v,$;return{label:(v=e.value.locales[t.value])==null?void 0:v.label,link:(($=e.value.locales[t.value])==null?void 0:$.link)||(t.value==="root"?"/":`/${t.value}/`)}});return{localeLinks:y(()=>Object.entries(e.value.locales).flatMap(([v,$])=>u.value.label===$.label?[]:{text:$.label,link:Qe($.link||(v==="root"?"/":`/${v}/`),n.value.i18nRouting!==!1&&o,s.value.relativePath.slice(u.value.link.length-1),!e.value.cleanUrls)+i.value})),currentLang:u}}function Qe(o,e,t,s){return e?o.replace(/\/$/,"")+ie(t.replace(/(^|\/)index\.md$/,"$1").replace(/\.md$/,s?".html":"")):o}const Ze={class:"NotFound"},xe={class:"code"},et={class:"title"},tt={class:"quote"},nt={class:"action"},st=["href","aria-label"],ot=m({__name:"NotFound",setup(o){const{theme:e}=V(),{currentLang:t}=J();return(s,n)=>{var i,u,p,v,$;return a(),c("div",Ze,[l("p",xe,S(((i=r(e).notFound)==null?void 0:i.code)??"404"),1),l("h1",et,S(((u=r(e).notFound)==null?void 0:u.title)??"PAGE NOT FOUND"),1),n[0]||(n[0]=l("div",{class:"divider"},null,-1)),l("blockquote",tt,S(((p=r(e).notFound)==null?void 0:p.quote)??"But if you don't change your direction, and if you keep looking, you may end up where you are heading."),1),l("div",nt,[l("a",{class:"link",href:r(de)(r(t).link),"aria-label":((v=r(e).notFound)==null?void 0:v.linkLabel)??"go to home"},S((($=r(e).notFound)==null?void 0:$.linkText)??"Take me home"),9,st)])])}}}),at=g(ot,[["__scopeId","data-v-314d6823"]]);function Ne(o,e){if(Array.isArray(o))return R(o);if(o==null)return[];e=ie(e);const t=Object.keys(o).sort((n,i)=>i.split("/").length-n.split("/").length).find(n=>e.startsWith(ie(n))),s=t?o[t]:[];return Array.isArray(s)?R(s):R(s.items,s.base)}function rt(o){const e=[];let t=0;for(const s in o){const n=o[s];if(n.items){t=e.push(n);continue}e[t]||e.push({items:[]}),e[t].items.push(n)}return e}function it(o){const e=[];function t(s){for(const n of s)n.text&&n.link&&e.push({text:n.text,link:n.link,docFooterText:n.docFooterText}),n.items&&t(n.items)}return t(o),e}function le(o,e){return Array.isArray(e)?e.some(t=>le(o,t)):q(o,e.link)?!0:e.items?le(o,e.items):!1}function R(o,e){return[...o].map(t=>{const s={...t},n=s.base||e;return n&&s.link&&(s.link=n+s.link),s.items&&(s.items=R(s.items,n)),s})}function G(){const{frontmatter:o,page:e,theme:t}=V(),s=re("(min-width: 960px)"),n=T(!1),i=y(()=>{const H=t.value.sidebar,N=e.value.relativePath;return H?Ne(H,N):[]}),u=T(i.value);D(i,(H,N)=>{JSON.stringify(H)!==JSON.stringify(N)&&(u.value=i.value)});const p=y(()=>o.value.sidebar!==!1&&u.value.length>0&&o.value.layout!=="home"),v=y(()=>$?o.value.aside==null?t.value.aside==="left":o.value.aside==="left":!1),$=y(()=>o.value.layout==="home"?!1:o.value.aside!=null?!!o.value.aside:t.value.aside!==!1),L=y(()=>p.value&&s.value),b=y(()=>p.value?rt(u.value):[]);function P(){n.value=!0}function M(){n.value=!1}function I(){n.value?M():P()}return{isOpen:n,sidebar:u,sidebarGroups:b,hasSidebar:p,hasAside:$,leftAside:v,isSidebarEnabled:L,open:P,close:M,toggle:I}}function lt(o,e){let t;X(()=>{t=o.value?document.activeElement:void 0}),O(()=>{window.addEventListener("keyup",s)}),Q(()=>{window.removeEventListener("keyup",s)});function s(n){n.key==="Escape"&&o.value&&(e(),t==null||t.focus())}}function ct(o){const{page:e,hash:t}=V(),s=T(!1),n=y(()=>o.value.collapsed!=null),i=y(()=>!!o.value.link),u=T(!1),p=()=>{u.value=q(e.value.relativePath,o.value.link)};D([e,o,t],p),O(p);const v=y(()=>u.value?!0:o.value.items?le(e.value.relativePath,o.value.items):!1),$=y(()=>!!(o.value.items&&o.value.items.length));X(()=>{s.value=!!(n.value&&o.value.collapsed)}),ve(()=>{(u.value||v.value)&&(s.value=!1)});function L(){n.value&&(s.value=!s.value)}return{collapsed:s,collapsible:n,isLink:i,isActiveLink:u,hasActiveLink:v,hasChildren:$,toggle:L}}function ut(){const{hasSidebar:o}=G(),e=re("(min-width: 960px)"),t=re("(min-width: 1280px)");return{isAsideEnabled:y(()=>!t.value&&!e.value?!1:o.value?t.value:e.value)}}const ce=[];function Me(o){return typeof o.outline=="object"&&!Array.isArray(o.outline)&&o.outline.label||o.outlineTitle||"On this page"}function he(o){const e=[...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)")].filter(t=>t.id&&t.hasChildNodes()).map(t=>{const s=Number(t.tagName[1]);return{element:t,title:dt(t),link:"#"+t.id,level:s}});return vt(e,o)}function dt(o){let e="";for(const t of o.childNodes)if(t.nodeType===1){if(t.classList.contains("VPBadge")||t.classList.contains("header-anchor")||t.classList.contains("ignore-header"))continue;e+=t.textContent}else t.nodeType===3&&(e+=t.textContent);return e.trim()}function vt(o,e){if(e===!1)return[];const t=(typeof e=="object"&&!Array.isArray(e)?e.level:e)||2,[s,n]=typeof t=="number"?[t,t]:t==="deep"?[2,6]:t;return ht(o,s,n)}function pt(o,e){const{isAsideEnabled:t}=ut(),s=Xe(i,100);let n=null;O(()=>{requestAnimationFrame(i),window.addEventListener("scroll",s)}),Ee(()=>{u(location.hash)}),Q(()=>{window.removeEventListener("scroll",s)});function i(){if(!t.value)return;const p=window.scrollY,v=window.innerHeight,$=document.body.offsetHeight,L=Math.abs(p+v-$)<1,b=ce.map(({element:M,link:I})=>({link:I,top:ft(M)})).filter(({top:M})=>!Number.isNaN(M)).sort((M,I)=>M.top-I.top);if(!b.length){u(null);return}if(p<1){u(null);return}if(L){u(b[b.length-1].link);return}let P=null;for(const{link:M,top:I}of b){if(I>p+Fe()+4)break;P=M}u(P)}function u(p){n&&n.classList.remove("active"),p==null?n=null:n=o.value.querySelector(`a[href="${decodeURIComponent(p)}"]`);const v=n;v?(v.classList.add("active"),e.value.style.top=v.offsetTop+39+"px",e.value.style.opacity="1"):(e.value.style.top="33px",e.value.style.opacity="0")}}function ft(o){let e=0;for(;o!==document.body;){if(o===null)return NaN;e+=o.offsetTop,o=o.offsetParent}return e}function ht(o,e,t){ce.length=0;const s=[],n=[];return o.forEach(i=>{const u={...i,children:[]};let p=n[n.length-1];for(;p&&p.level>=u.level;)n.pop(),p=n[n.length-1];if(u.element.classList.contains("ignore-header")||p&&"shouldIgnore"in p){n.push({level:u.level,shouldIgnore:!0});return}u.level>t||u.level{const n=W("VPDocOutlineItem",!0);return a(),c("ul",{class:w(["VPDocOutlineItem",t.root?"root":"nested"])},[(a(!0),c(C,null,A(t.headers,({children:i,link:u,title:p})=>(a(),c("li",null,[l("a",{class:"outline-link",href:u,onClick:e,title:p},S(p),9,mt),i!=null&&i.length?(a(),k(n,{key:0,headers:i},null,8,["headers"])):h("",!0)]))),256))],2)}}}),Ie=g(_t,[["__scopeId","data-v-9e933ec1"]]),kt={class:"content"},gt={"aria-level":"2",class:"outline-title",id:"doc-outline-aria-label",role:"heading"},bt=m({__name:"VPDocAsideOutline",setup(o){const{frontmatter:e,theme:t}=V(),s=ye([]);Z(()=>{s.value=he(e.value.outline??t.value.outline)});const n=T(),i=T();return pt(n,i),(u,p)=>(a(),c("nav",{"aria-labelledby":"doc-outline-aria-label",class:w(["VPDocAsideOutline",{"has-outline":s.value.length>0}]),ref_key:"container",ref:n},[l("div",kt,[l("div",{class:"outline-marker",ref_key:"marker",ref:i},null,512),l("div",gt,S(r(Me)(r(t))),1),_(Ie,{headers:s.value,root:!0},null,8,["headers"])])],2))}}),$t=g(bt,[["__scopeId","data-v-d595de97"]]),yt={class:"VPDocAsideCarbonAds"},Pt=m({__name:"VPDocAsideCarbonAds",props:{carbonAds:{}},setup(o){const e=()=>null;return(t,s)=>(a(),c("div",yt,[_(r(e),{"carbon-ads":t.carbonAds},null,8,["carbon-ads"])]))}}),Lt={class:"VPDocAside"},Vt=m({__name:"VPDocAside",setup(o){const{theme:e}=V();return(t,s)=>(a(),c("div",Lt,[d(t.$slots,"aside-top",{},void 0,!0),d(t.$slots,"aside-outline-before",{},void 0,!0),_($t),d(t.$slots,"aside-outline-after",{},void 0,!0),s[0]||(s[0]=l("div",{class:"spacer"},null,-1)),d(t.$slots,"aside-ads-before",{},void 0,!0),r(e).carbonAds?(a(),k(Pt,{key:0,"carbon-ads":r(e).carbonAds},null,8,["carbon-ads"])):h("",!0),d(t.$slots,"aside-ads-after",{},void 0,!0),d(t.$slots,"aside-bottom",{},void 0,!0)]))}}),St=g(Vt,[["__scopeId","data-v-3de71ef9"]]);function Tt(){const{theme:o,page:e}=V();return y(()=>{const{text:t="Edit this page",pattern:s=""}=o.value.editLink||{};let n;return typeof s=="function"?n=s(e.value):n=s.replace(/:path/g,e.value.filePath),{url:n,text:t}})}function Nt(){const{page:o,theme:e,frontmatter:t}=V();return y(()=>{var $,L,b,P,M,I,H,N;const s=Ne(e.value.sidebar,o.value.relativePath),n=it(s),i=Mt(n,B=>B.link.replace(/[?#].*$/,"")),u=i.findIndex(B=>q(o.value.relativePath,B.link)),p=(($=e.value.docFooter)==null?void 0:$.prev)===!1&&!t.value.prev||t.value.prev===!1,v=((L=e.value.docFooter)==null?void 0:L.next)===!1&&!t.value.next||t.value.next===!1;return{prev:p?void 0:{text:(typeof t.value.prev=="string"?t.value.prev:typeof t.value.prev=="object"?t.value.prev.text:void 0)??((b=i[u-1])==null?void 0:b.docFooterText)??((P=i[u-1])==null?void 0:P.text),link:(typeof t.value.prev=="object"?t.value.prev.link:void 0)??((M=i[u-1])==null?void 0:M.link)},next:v?void 0:{text:(typeof t.value.next=="string"?t.value.next:typeof t.value.next=="object"?t.value.next.text:void 0)??((I=i[u+1])==null?void 0:I.docFooterText)??((H=i[u+1])==null?void 0:H.text),link:(typeof t.value.next=="object"?t.value.next.link:void 0)??((N=i[u+1])==null?void 0:N.link)}}})}function Mt(o,e){const t=new Set;return o.filter(s=>{const n=e(s);return t.has(n)?!1:t.add(n)})}const F=m({__name:"VPLink",props:{tag:{},href:{},noIcon:{type:Boolean},target:{},rel:{}},setup(o){const e=o,t=y(()=>e.tag??(e.href?"a":"span")),s=y(()=>e.href&&Pe.test(e.href)||e.target==="_blank");return(n,i)=>(a(),k(E(t.value),{class:w(["VPLink",{link:n.href,"vp-external-link-icon":s.value,"no-icon":n.noIcon}]),href:n.href?r(fe)(n.href):void 0,target:n.target??(s.value?"_blank":void 0),rel:n.rel??(s.value?"noreferrer":void 0)},{default:f(()=>[d(n.$slots,"default")]),_:3},8,["class","href","target","rel"]))}}),It={class:"VPLastUpdated"},wt=["datetime"],Ct=m({__name:"VPDocFooterLastUpdated",setup(o){const{theme:e,page:t,lang:s}=V(),n=y(()=>new Date(t.value.lastUpdated)),i=y(()=>n.value.toISOString()),u=T("");return O(()=>{X(()=>{var p,v,$;u.value=new Intl.DateTimeFormat((v=(p=e.value.lastUpdated)==null?void 0:p.formatOptions)!=null&&v.forceLocale?s.value:void 0,(($=e.value.lastUpdated)==null?void 0:$.formatOptions)??{dateStyle:"short",timeStyle:"short"}).format(n.value)})}),(p,v)=>{var $;return a(),c("p",It,[z(S((($=r(e).lastUpdated)==null?void 0:$.text)||r(e).lastUpdatedText||"Last updated")+": ",1),l("time",{datetime:i.value},S(u.value),9,wt)])}}}),Ht=g(Ct,[["__scopeId","data-v-12f2da6a"]]),At={key:0,class:"VPDocFooter"},Bt={key:0,class:"edit-info"},Et={key:0,class:"edit-link"},Ft={key:1,class:"last-updated"},Dt={key:1,class:"prev-next","aria-labelledby":"doc-footer-aria-label"},Ot={class:"pager"},Gt=["innerHTML"],Ut=["innerHTML"],jt={class:"pager"},zt=["innerHTML"],qt=["innerHTML"],Wt=m({__name:"VPDocFooter",setup(o){const{theme:e,page:t,frontmatter:s}=V(),n=Tt(),i=Nt(),u=y(()=>e.value.editLink&&s.value.editLink!==!1),p=y(()=>t.value.lastUpdated),v=y(()=>u.value||p.value||i.value.prev||i.value.next);return($,L)=>{var b,P,M,I;return v.value?(a(),c("footer",At,[d($.$slots,"doc-footer-before",{},void 0,!0),u.value||p.value?(a(),c("div",Bt,[u.value?(a(),c("div",Et,[_(F,{class:"edit-link-button",href:r(n).url,"no-icon":!0},{default:f(()=>[L[0]||(L[0]=l("span",{class:"vpi-square-pen edit-link-icon"},null,-1)),z(" "+S(r(n).text),1)]),_:1},8,["href"])])):h("",!0),p.value?(a(),c("div",Ft,[_(Ht)])):h("",!0)])):h("",!0),(b=r(i).prev)!=null&&b.link||(P=r(i).next)!=null&&P.link?(a(),c("nav",Dt,[L[1]||(L[1]=l("span",{class:"visually-hidden",id:"doc-footer-aria-label"},"Pager",-1)),l("div",Ot,[(M=r(i).prev)!=null&&M.link?(a(),k(F,{key:0,class:"pager-link prev",href:r(i).prev.link},{default:f(()=>{var H;return[l("span",{class:"desc",innerHTML:((H=r(e).docFooter)==null?void 0:H.prev)||"Previous page"},null,8,Gt),l("span",{class:"title",innerHTML:r(i).prev.text},null,8,Ut)]}),_:1},8,["href"])):h("",!0)]),l("div",jt,[(I=r(i).next)!=null&&I.link?(a(),k(F,{key:0,class:"pager-link next",href:r(i).next.link},{default:f(()=>{var H;return[l("span",{class:"desc",innerHTML:((H=r(e).docFooter)==null?void 0:H.next)||"Next page"},null,8,zt),l("span",{class:"title",innerHTML:r(i).next.text},null,8,qt)]}),_:1},8,["href"])):h("",!0)])])):h("",!0)])):h("",!0)}}}),Kt=g(Wt,[["__scopeId","data-v-e4c276ab"]]),Jt={class:"container"},Rt={class:"aside-container"},Yt={class:"aside-content"},Xt={class:"content"},Qt={class:"content-container"},Zt={class:"main"},xt=m({__name:"VPDoc",setup(o){const{theme:e}=V(),t=x(),{hasSidebar:s,hasAside:n,leftAside:i}=G(),u=y(()=>t.path.replace(/[./]+/g,"_").replace(/_html$/,""));return(p,v)=>{const $=W("Content");return a(),c("div",{class:w(["VPDoc",{"has-sidebar":r(s),"has-aside":r(n)}])},[d(p.$slots,"doc-top",{},void 0,!0),l("div",Jt,[r(n)?(a(),c("div",{key:0,class:w(["aside",{"left-aside":r(i)}])},[v[0]||(v[0]=l("div",{class:"aside-curtain"},null,-1)),l("div",Rt,[l("div",Yt,[_(St,null,{"aside-top":f(()=>[d(p.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":f(()=>[d(p.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":f(()=>[d(p.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":f(()=>[d(p.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":f(()=>[d(p.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":f(()=>[d(p.$slots,"aside-ads-after",{},void 0,!0)]),_:3})])])],2)):h("",!0),l("div",Xt,[l("div",Qt,[d(p.$slots,"doc-before",{},void 0,!0),l("main",Zt,[_($,{class:w(["vp-doc",[u.value,r(e).externalLinkIcon&&"external-link-icon-enabled"]])},null,8,["class"])]),_(Kt,null,{"doc-footer-before":f(()=>[d(p.$slots,"doc-footer-before",{},void 0,!0)]),_:3}),d(p.$slots,"doc-after",{},void 0,!0)])])]),d(p.$slots,"doc-bottom",{},void 0,!0)],2)}}}),en=g(xt,[["__scopeId","data-v-13b8f1e2"]]),tn=m({__name:"VPButton",props:{tag:{},size:{default:"medium"},theme:{default:"brand"},text:{},href:{},target:{},rel:{}},setup(o){const e=o,t=y(()=>e.href&&Pe.test(e.href)),s=y(()=>e.tag||(e.href?"a":"button"));return(n,i)=>(a(),k(E(s.value),{class:w(["VPButton",[n.size,n.theme]]),href:n.href?r(fe)(n.href):void 0,target:e.target??(t.value?"_blank":void 0),rel:e.rel??(t.value?"noreferrer":void 0)},{default:f(()=>[z(S(n.text),1)]),_:1},8,["class","href","target","rel"]))}}),nn=g(tn,[["__scopeId","data-v-df2cf507"]]),sn=["src","alt"],on=m({inheritAttrs:!1,__name:"VPImage",props:{image:{},alt:{}},setup(o){return(e,t)=>{const s=W("VPImage",!0);return e.image?(a(),c(C,{key:0},[typeof e.image=="string"||"src"in e.image?(a(),c("img",j({key:0,class:"VPImage"},typeof e.image=="string"?e.$attrs:{...e.image,...e.$attrs},{src:r(de)(typeof e.image=="string"?e.image:e.image.src),alt:e.alt??(typeof e.image=="string"?"":e.image.alt||"")}),null,16,sn)):(a(),c(C,{key:1},[_(s,j({class:"dark",image:e.image.dark,alt:e.image.alt},e.$attrs),null,16,["image","alt"]),_(s,j({class:"light",image:e.image.light,alt:e.image.alt},e.$attrs),null,16,["image","alt"])],64))],64)):h("",!0)}}}),Y=g(on,[["__scopeId","data-v-964b1c62"]]),an={class:"container"},rn={class:"main"},ln={key:0,class:"name"},cn=["innerHTML"],un=["innerHTML"],dn=["innerHTML"],vn={key:0,class:"actions"},pn={key:0,class:"image"},fn={class:"image-container"},hn=m({__name:"VPHero",props:{name:{},text:{},tagline:{},image:{},actions:{}},setup(o){const e=ee("hero-image-slot-exists");return(t,s)=>(a(),c("div",{class:w(["VPHero",{"has-image":t.image||r(e)}])},[l("div",an,[l("div",rn,[d(t.$slots,"home-hero-info-before",{},void 0,!0),d(t.$slots,"home-hero-info",{},()=>[t.name?(a(),c("h1",ln,[l("span",{innerHTML:t.name,class:"clip"},null,8,cn)])):h("",!0),t.text?(a(),c("p",{key:1,innerHTML:t.text,class:"text"},null,8,un)):h("",!0),t.tagline?(a(),c("p",{key:2,innerHTML:t.tagline,class:"tagline"},null,8,dn)):h("",!0)],!0),d(t.$slots,"home-hero-info-after",{},void 0,!0),t.actions?(a(),c("div",vn,[(a(!0),c(C,null,A(t.actions,n=>(a(),c("div",{key:n.link,class:"action"},[_(nn,{tag:"a",size:"medium",theme:n.theme,text:n.text,href:n.link,target:n.target,rel:n.rel},null,8,["theme","text","href","target","rel"])]))),128))])):h("",!0),d(t.$slots,"home-hero-actions-after",{},void 0,!0)]),t.image||r(e)?(a(),c("div",pn,[l("div",fn,[s[0]||(s[0]=l("div",{class:"image-bg"},null,-1)),d(t.$slots,"home-hero-image",{},()=>[t.image?(a(),k(Y,{key:0,class:"image-src",image:t.image},null,8,["image"])):h("",!0)],!0)])])):h("",!0)])],2))}}),mn=g(hn,[["__scopeId","data-v-8d45dfd5"]]),_n=m({__name:"VPHomeHero",setup(o){const{frontmatter:e}=V();return(t,s)=>r(e).hero?(a(),k(mn,{key:0,class:"VPHomeHero",name:r(e).hero.name,text:r(e).hero.text,tagline:r(e).hero.tagline,image:r(e).hero.image,actions:r(e).hero.actions},{"home-hero-info-before":f(()=>[d(t.$slots,"home-hero-info-before")]),"home-hero-info":f(()=>[d(t.$slots,"home-hero-info")]),"home-hero-info-after":f(()=>[d(t.$slots,"home-hero-info-after")]),"home-hero-actions-after":f(()=>[d(t.$slots,"home-hero-actions-after")]),"home-hero-image":f(()=>[d(t.$slots,"home-hero-image")]),_:3},8,["name","text","tagline","image","actions"])):h("",!0)}}),kn={class:"box"},gn={key:0,class:"icon"},bn=["innerHTML"],$n=["innerHTML"],yn=["innerHTML"],Pn={key:4,class:"link-text"},Ln={class:"link-text-value"},Vn=m({__name:"VPFeature",props:{icon:{},title:{},details:{},link:{},linkText:{},rel:{},target:{}},setup(o){return(e,t)=>(a(),k(F,{class:"VPFeature",href:e.link,rel:e.rel,target:e.target,"no-icon":!0,tag:e.link?"a":"div"},{default:f(()=>[l("article",kn,[typeof e.icon=="object"&&e.icon.wrap?(a(),c("div",gn,[_(Y,{image:e.icon,alt:e.icon.alt,height:e.icon.height||48,width:e.icon.width||48},null,8,["image","alt","height","width"])])):typeof e.icon=="object"?(a(),k(Y,{key:1,image:e.icon,alt:e.icon.alt,height:e.icon.height||48,width:e.icon.width||48},null,8,["image","alt","height","width"])):e.icon?(a(),c("div",{key:2,class:"icon",innerHTML:e.icon},null,8,bn)):h("",!0),l("h2",{class:"title",innerHTML:e.title},null,8,$n),e.details?(a(),c("p",{key:3,class:"details",innerHTML:e.details},null,8,yn)):h("",!0),e.linkText?(a(),c("div",Pn,[l("p",Ln,[z(S(e.linkText)+" ",1),t[0]||(t[0]=l("span",{class:"vpi-arrow-right link-text-icon"},null,-1))])])):h("",!0)])]),_:1},8,["href","rel","target","tag"]))}}),Sn=g(Vn,[["__scopeId","data-v-810fe4d1"]]),Tn={key:0,class:"VPFeatures"},Nn={class:"container"},Mn={class:"items"},In=m({__name:"VPFeatures",props:{features:{}},setup(o){const e=o,t=y(()=>{const s=e.features.length;if(s){if(s===2)return"grid-2";if(s===3)return"grid-3";if(s%3===0)return"grid-6";if(s>3)return"grid-4"}else return});return(s,n)=>s.features?(a(),c("div",Tn,[l("div",Nn,[l("div",Mn,[(a(!0),c(C,null,A(s.features,i=>(a(),c("div",{key:i.title,class:w(["item",[t.value]])},[_(Sn,{icon:i.icon,title:i.title,details:i.details,link:i.link,"link-text":i.linkText,rel:i.rel,target:i.target},null,8,["icon","title","details","link","link-text","rel","target"])],2))),128))])])])):h("",!0)}}),wn=g(In,[["__scopeId","data-v-1d713b99"]]),Cn=m({__name:"VPHomeFeatures",setup(o){const{frontmatter:e}=V();return(t,s)=>r(e).features?(a(),k(wn,{key:0,class:"VPHomeFeatures",features:r(e).features},null,8,["features"])):h("",!0)}}),Hn=m({__name:"VPHomeContent",setup(o){const{width:e}=De({initialWidth:0,includeScrollbar:!1});return(t,s)=>(a(),c("div",{class:"vp-doc container",style:pe(r(e)?{"--vp-offset":`calc(50% - ${r(e)/2}px)`}:{})},[d(t.$slots,"default",{},void 0,!0)],4))}}),An=g(Hn,[["__scopeId","data-v-c6e93e4e"]]),Bn={class:"VPHome"},En=m({__name:"VPHome",setup(o){const{frontmatter:e}=V();return(t,s)=>{const n=W("Content");return a(),c("div",Bn,[d(t.$slots,"home-hero-before",{},void 0,!0),_(_n,null,{"home-hero-info-before":f(()=>[d(t.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":f(()=>[d(t.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":f(()=>[d(t.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":f(()=>[d(t.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":f(()=>[d(t.$slots,"home-hero-image",{},void 0,!0)]),_:3}),d(t.$slots,"home-hero-after",{},void 0,!0),d(t.$slots,"home-features-before",{},void 0,!0),_(Cn),d(t.$slots,"home-features-after",{},void 0,!0),r(e).markdownStyles!==!1?(a(),k(An,{key:0},{default:f(()=>[_(n)]),_:1})):(a(),k(n,{key:1}))])}}}),Fn=g(En,[["__scopeId","data-v-f253ebe2"]]),Dn={},On={class:"VPPage"};function Gn(o,e){const t=W("Content");return a(),c("div",On,[d(o.$slots,"page-top"),_(t),d(o.$slots,"page-bottom")])}const Un=g(Dn,[["render",Gn]]),jn=m({__name:"VPContent",setup(o){const{page:e,frontmatter:t}=V(),{hasSidebar:s}=G();return(n,i)=>(a(),c("div",{class:w(["VPContent",{"has-sidebar":r(s),"is-home":r(t).layout==="home"}]),id:"VPContent"},[r(e).isNotFound?d(n.$slots,"not-found",{key:0},()=>[_(at)],!0):r(t).layout==="page"?(a(),k(Un,{key:1},{"page-top":f(()=>[d(n.$slots,"page-top",{},void 0,!0)]),"page-bottom":f(()=>[d(n.$slots,"page-bottom",{},void 0,!0)]),_:3})):r(t).layout==="home"?(a(),k(Fn,{key:2},{"home-hero-before":f(()=>[d(n.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info-before":f(()=>[d(n.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":f(()=>[d(n.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":f(()=>[d(n.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":f(()=>[d(n.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":f(()=>[d(n.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":f(()=>[d(n.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":f(()=>[d(n.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":f(()=>[d(n.$slots,"home-features-after",{},void 0,!0)]),_:3})):r(t).layout&&r(t).layout!=="doc"?(a(),k(E(r(t).layout),{key:3})):(a(),k(en,{key:4},{"doc-top":f(()=>[d(n.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":f(()=>[d(n.$slots,"doc-bottom",{},void 0,!0)]),"doc-footer-before":f(()=>[d(n.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":f(()=>[d(n.$slots,"doc-before",{},void 0,!0)]),"doc-after":f(()=>[d(n.$slots,"doc-after",{},void 0,!0)]),"aside-top":f(()=>[d(n.$slots,"aside-top",{},void 0,!0)]),"aside-outline-before":f(()=>[d(n.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":f(()=>[d(n.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":f(()=>[d(n.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":f(()=>[d(n.$slots,"aside-ads-after",{},void 0,!0)]),"aside-bottom":f(()=>[d(n.$slots,"aside-bottom",{},void 0,!0)]),_:3}))],2))}}),zn=g(jn,[["__scopeId","data-v-7eb770b7"]]),qn={class:"container"},Wn=["innerHTML"],Kn=["innerHTML"],Jn=m({__name:"VPFooter",setup(o){const{theme:e,frontmatter:t}=V(),{hasSidebar:s}=G();return(n,i)=>r(e).footer&&r(t).footer!==!1?(a(),c("footer",{key:0,class:w(["VPFooter",{"has-sidebar":r(s)}])},[l("div",qn,[r(e).footer.message?(a(),c("p",{key:0,class:"message",innerHTML:r(e).footer.message},null,8,Wn)):h("",!0),r(e).footer.copyright?(a(),c("p",{key:1,class:"copyright",innerHTML:r(e).footer.copyright},null,8,Kn)):h("",!0)])],2)):h("",!0)}}),Rn=g(Jn,[["__scopeId","data-v-8c586fe8"]]);function Yn(){const{theme:o,frontmatter:e}=V(),t=ye([]),s=y(()=>t.value.length>0);return Z(()=>{t.value=he(e.value.outline??o.value.outline)}),{headers:t,hasLocalNav:s}}const Xn={class:"menu-text"},Qn={class:"header"},Zn={class:"outline"},xn=m({__name:"VPLocalNavOutlineDropdown",props:{headers:{},navHeight:{}},setup(o){const e=o,{theme:t}=V(),s=T(!1),n=T(0),i=T(),u=T();function p(b){var P;(P=i.value)!=null&&P.contains(b.target)||(s.value=!1)}D(s,b=>{if(b){document.addEventListener("click",p);return}document.removeEventListener("click",p)}),Oe("Escape",()=>{s.value=!1}),Z(()=>{s.value=!1});function v(){s.value=!s.value,n.value=window.innerHeight+Math.min(window.scrollY-e.navHeight,0)}function $(b){b.target.classList.contains("outline-link")&&(u.value&&(u.value.style.transition="none"),Le(()=>{s.value=!1}))}function L(){s.value=!1,window.scrollTo({top:0,left:0,behavior:"smooth"})}return(b,P)=>(a(),c("div",{class:"VPLocalNavOutlineDropdown",style:pe({"--vp-vh":n.value+"px"}),ref_key:"main",ref:i},[b.headers.length>0?(a(),c("button",{key:0,onClick:v,class:w({open:s.value})},[l("span",Xn,S(r(Me)(r(t))),1),P[0]||(P[0]=l("span",{class:"vpi-chevron-right icon"},null,-1))],2)):(a(),c("button",{key:1,onClick:L},S(r(t).returnToTopLabel||"Return to top"),1)),_(ue,{name:"flyout"},{default:f(()=>[s.value?(a(),c("div",{key:0,ref_key:"items",ref:u,class:"items",onClick:$},[l("div",Qn,[l("a",{class:"top-link",href:"#",onClick:L},S(r(t).returnToTopLabel||"Return to top"),1)]),l("div",Zn,[_(Ie,{headers:b.headers},null,8,["headers"])])],512)):h("",!0)]),_:1})],4))}}),es=g(xn,[["__scopeId","data-v-5bcce627"]]),ts={class:"container"},ns=["aria-expanded"],ss={class:"menu-text"},os=m({__name:"VPLocalNav",props:{open:{type:Boolean}},emits:["open-menu"],setup(o){const{theme:e,frontmatter:t}=V(),{hasSidebar:s}=G(),{headers:n}=Yn(),{y:i}=Ve(),u=T(0);O(()=>{u.value=parseInt(getComputedStyle(document.documentElement).getPropertyValue("--vp-nav-height"))}),Z(()=>{n.value=he(t.value.outline??e.value.outline)});const p=y(()=>n.value.length===0),v=y(()=>p.value&&!s.value),$=y(()=>({VPLocalNav:!0,"has-sidebar":s.value,empty:p.value,fixed:v.value}));return(L,b)=>r(t).layout!=="home"&&(!v.value||r(i)>=u.value)?(a(),c("div",{key:0,class:w($.value)},[l("div",ts,[r(s)?(a(),c("button",{key:0,class:"menu","aria-expanded":L.open,"aria-controls":"VPSidebarNav",onClick:b[0]||(b[0]=P=>L.$emit("open-menu"))},[b[1]||(b[1]=l("span",{class:"vpi-align-left menu-icon"},null,-1)),l("span",ss,S(r(e).sidebarMenuLabel||"Menu"),1)],8,ns)):h("",!0),_(es,{headers:r(n),navHeight:u.value},null,8,["headers","navHeight"])])],2)):h("",!0)}}),as=g(os,[["__scopeId","data-v-d38f1bb7"]]);function rs(){const o=T(!1);function e(){o.value=!0,window.addEventListener("resize",n)}function t(){o.value=!1,window.removeEventListener("resize",n)}function s(){o.value?t():e()}function n(){window.outerWidth>=768&&t()}const i=x();return D(()=>i.path,t),{isScreenOpen:o,openScreen:e,closeScreen:t,toggleScreen:s}}const is={},ls={class:"VPSwitch",type:"button",role:"switch"},cs={class:"check"},us={key:0,class:"icon"};function ds(o,e){return a(),c("button",ls,[l("span",cs,[o.$slots.default?(a(),c("span",us,[d(o.$slots,"default",{},void 0,!0)])):h("",!0)])])}const vs=g(is,[["render",ds],["__scopeId","data-v-e23c1592"]]),ps=m({__name:"VPSwitchAppearance",setup(o){const{isDark:e,theme:t}=V(),s=ee("toggle-appearance",()=>{e.value=!e.value}),n=T("");return ve(()=>{n.value=e.value?t.value.lightModeSwitchTitle||"Switch to light theme":t.value.darkModeSwitchTitle||"Switch to dark theme"}),(i,u)=>(a(),k(vs,{title:n.value,class:"VPSwitchAppearance","aria-checked":r(e),onClick:r(s)},{default:f(()=>u[0]||(u[0]=[l("span",{class:"vpi-sun sun"},null,-1),l("span",{class:"vpi-moon moon"},null,-1)])),_:1},8,["title","aria-checked","onClick"]))}}),me=g(ps,[["__scopeId","data-v-327cf911"]]),fs={key:0,class:"VPNavBarAppearance"},hs=m({__name:"VPNavBarAppearance",setup(o){const{site:e}=V();return(t,s)=>r(e).appearance&&r(e).appearance!=="force-dark"&&r(e).appearance!=="force-auto"?(a(),c("div",fs,[_(me)])):h("",!0)}}),ms=g(hs,[["__scopeId","data-v-c319a4c2"]]),_e=T();let we=!1,ae=0;function _s(o){const e=T(!1);if(te){!we&&ks(),ae++;const t=D(_e,s=>{var n,i,u;s===o.el.value||(n=o.el.value)!=null&&n.contains(s)?(e.value=!0,(i=o.onFocus)==null||i.call(o)):(e.value=!1,(u=o.onBlur)==null||u.call(o))});Q(()=>{t(),ae--,ae||gs()})}return Ge(e)}function ks(){document.addEventListener("focusin",Ce),we=!0,_e.value=document.activeElement}function gs(){document.removeEventListener("focusin",Ce)}function Ce(){_e.value=document.activeElement}const bs={class:"VPMenuLink"},$s=["innerHTML"],ys=m({__name:"VPMenuLink",props:{item:{}},setup(o){const{page:e}=V();return(t,s)=>(a(),c("div",bs,[_(F,{class:w({active:r(q)(r(e).relativePath,t.item.activeMatch||t.item.link,!!t.item.activeMatch)}),href:t.item.link,target:t.item.target,rel:t.item.rel,"no-icon":t.item.noIcon},{default:f(()=>[l("span",{innerHTML:t.item.text},null,8,$s)]),_:1},8,["class","href","target","rel","no-icon"])]))}}),ne=g(ys,[["__scopeId","data-v-6d9cb10e"]]),Ps={class:"VPMenuGroup"},Ls={key:0,class:"title"},Vs=m({__name:"VPMenuGroup",props:{text:{},items:{}},setup(o){return(e,t)=>(a(),c("div",Ps,[e.text?(a(),c("p",Ls,S(e.text),1)):h("",!0),(a(!0),c(C,null,A(e.items,s=>(a(),c(C,null,["link"in s?(a(),k(ne,{key:0,item:s},null,8,["item"])):h("",!0)],64))),256))]))}}),Ss=g(Vs,[["__scopeId","data-v-c73fface"]]),Ts={class:"VPMenu"},Ns={key:0,class:"items"},Ms=m({__name:"VPMenu",props:{items:{}},setup(o){return(e,t)=>(a(),c("div",Ts,[e.items?(a(),c("div",Ns,[(a(!0),c(C,null,A(e.items,s=>(a(),c(C,{key:JSON.stringify(s)},["link"in s?(a(),k(ne,{key:0,item:s},null,8,["item"])):"component"in s?(a(),k(E(s.component),j({key:1,ref_for:!0},s.props),null,16)):(a(),k(Ss,{key:2,text:s.text,items:s.items},null,8,["text","items"]))],64))),128))])):h("",!0),d(e.$slots,"default",{},void 0,!0)]))}}),Is=g(Ms,[["__scopeId","data-v-4abdfca2"]]),ws=["aria-expanded","aria-label"],Cs={key:0,class:"text"},Hs=["innerHTML"],As={key:1,class:"vpi-more-horizontal icon"},Bs={class:"menu"},Es=m({__name:"VPFlyout",props:{icon:{},button:{},label:{},items:{}},setup(o){const e=T(!1),t=T();_s({el:t,onBlur:s});function s(){e.value=!1}return(n,i)=>(a(),c("div",{class:"VPFlyout",ref_key:"el",ref:t,onMouseenter:i[1]||(i[1]=u=>e.value=!0),onMouseleave:i[2]||(i[2]=u=>e.value=!1)},[l("button",{type:"button",class:"button","aria-haspopup":"true","aria-expanded":e.value,"aria-label":n.label,onClick:i[0]||(i[0]=u=>e.value=!e.value)},[n.button||n.icon?(a(),c("span",Cs,[n.icon?(a(),c("span",{key:0,class:w([n.icon,"option-icon"])},null,2)):h("",!0),n.button?(a(),c("span",{key:1,innerHTML:n.button},null,8,Hs)):h("",!0),i[3]||(i[3]=l("span",{class:"vpi-chevron-down text-icon"},null,-1))])):(a(),c("span",As))],8,ws),l("div",Bs,[_(Is,{items:n.items},{default:f(()=>[d(n.$slots,"default",{},void 0,!0)]),_:3},8,["items"])])],544))}}),ke=g(Es,[["__scopeId","data-v-405682cb"]]),Fs=["href","aria-label","innerHTML"],Ds=m({__name:"VPSocialLink",props:{icon:{},link:{},ariaLabel:{}},setup(o){const e=o,t=T();O(async()=>{var i;await Le();const n=(i=t.value)==null?void 0:i.children[0];n instanceof HTMLElement&&n.className.startsWith("vpi-social-")&&(getComputedStyle(n).maskImage||getComputedStyle(n).webkitMaskImage)==="none"&&n.style.setProperty("--icon",`url('https://api.iconify.design/simple-icons/${e.icon}.svg')`)});const s=y(()=>typeof e.icon=="object"?e.icon.svg:``);return(n,i)=>(a(),c("a",{ref_key:"el",ref:t,class:"VPSocialLink no-icon",href:n.link,"aria-label":n.ariaLabel??(typeof n.icon=="string"?n.icon:""),target:"_blank",rel:"noopener",innerHTML:s.value},null,8,Fs))}}),Os=g(Ds,[["__scopeId","data-v-3edd214b"]]),Gs={class:"VPSocialLinks"},Us=m({__name:"VPSocialLinks",props:{links:{}},setup(o){return(e,t)=>(a(),c("div",Gs,[(a(!0),c(C,null,A(e.links,({link:s,icon:n,ariaLabel:i})=>(a(),k(Os,{key:s,icon:n,link:s,ariaLabel:i},null,8,["icon","link","ariaLabel"]))),128))]))}}),ge=g(Us,[["__scopeId","data-v-5e24eb64"]]),js={key:0,class:"group translations"},zs={class:"trans-title"},qs={key:1,class:"group"},Ws={class:"item appearance"},Ks={class:"label"},Js={class:"appearance-action"},Rs={key:2,class:"group"},Ys={class:"item social-links"},Xs=m({__name:"VPNavBarExtra",setup(o){const{site:e,theme:t}=V(),{localeLinks:s,currentLang:n}=J({correspondingLink:!0}),i=y(()=>s.value.length&&n.value.label||e.value.appearance||t.value.socialLinks);return(u,p)=>i.value?(a(),k(ke,{key:0,class:"VPNavBarExtra",label:"extra navigation"},{default:f(()=>[r(s).length&&r(n).label?(a(),c("div",js,[l("p",zs,S(r(n).label),1),(a(!0),c(C,null,A(r(s),v=>(a(),k(ne,{key:v.link,item:v},null,8,["item"]))),128))])):h("",!0),r(e).appearance&&r(e).appearance!=="force-dark"&&r(e).appearance!=="force-auto"?(a(),c("div",qs,[l("div",Ws,[l("p",Ks,S(r(t).darkModeSwitchLabel||"Appearance"),1),l("div",Js,[_(me)])])])):h("",!0),r(t).socialLinks?(a(),c("div",Rs,[l("div",Ys,[_(ge,{class:"social-links-list",links:r(t).socialLinks},null,8,["links"])])])):h("",!0)]),_:1})):h("",!0)}}),Qs=g(Xs,[["__scopeId","data-v-432caf41"]]),Zs=["aria-expanded"],xs=m({__name:"VPNavBarHamburger",props:{active:{type:Boolean}},emits:["click"],setup(o){return(e,t)=>(a(),c("button",{type:"button",class:w(["VPNavBarHamburger",{active:e.active}]),"aria-label":"mobile navigation","aria-expanded":e.active,"aria-controls":"VPNavScreen",onClick:t[0]||(t[0]=s=>e.$emit("click"))},t[1]||(t[1]=[l("span",{class:"container"},[l("span",{class:"top"}),l("span",{class:"middle"}),l("span",{class:"bottom"})],-1)]),10,Zs))}}),eo=g(xs,[["__scopeId","data-v-a20145d7"]]),to=["innerHTML"],no=m({__name:"VPNavBarMenuLink",props:{item:{}},setup(o){const{page:e}=V();return(t,s)=>(a(),k(F,{class:w({VPNavBarMenuLink:!0,active:r(q)(r(e).relativePath,t.item.activeMatch||t.item.link,!!t.item.activeMatch)}),href:t.item.link,target:t.item.target,rel:t.item.rel,"no-icon":t.item.noIcon,tabindex:"0"},{default:f(()=>[l("span",{innerHTML:t.item.text},null,8,to)]),_:1},8,["class","href","target","rel","no-icon"]))}}),so=g(no,[["__scopeId","data-v-22ff4594"]]),oo=m({__name:"VPNavBarMenuGroup",props:{item:{}},setup(o){const e=o,{page:t}=V(),s=i=>"component"in i?!1:"link"in i?q(t.value.relativePath,i.link,!!e.item.activeMatch):i.items.some(s),n=y(()=>s(e.item));return(i,u)=>(a(),k(ke,{class:w({VPNavBarMenuGroup:!0,active:r(q)(r(t).relativePath,i.item.activeMatch,!!i.item.activeMatch)||n.value}),button:i.item.text,items:i.item.items},null,8,["class","button","items"]))}}),ao={key:0,"aria-labelledby":"main-nav-aria-label",class:"VPNavBarMenu"},ro=m({__name:"VPNavBarMenu",setup(o){const{theme:e}=V();return(t,s)=>r(e).nav?(a(),c("nav",ao,[s[0]||(s[0]=l("span",{id:"main-nav-aria-label",class:"visually-hidden"}," Main Navigation ",-1)),(a(!0),c(C,null,A(r(e).nav,n=>(a(),c(C,{key:JSON.stringify(n)},["link"in n?(a(),k(so,{key:0,item:n},null,8,["item"])):"component"in n?(a(),k(E(n.component),j({key:1,ref_for:!0},n.props),null,16)):(a(),k(oo,{key:2,item:n},null,8,["item"]))],64))),128))])):h("",!0)}}),io=g(ro,[["__scopeId","data-v-6e009845"]]);function lo(o){const{localeIndex:e,theme:t}=V();function s(n){var I,H,N;const i=n.split("."),u=(I=t.value.search)==null?void 0:I.options,p=u&&typeof u=="object",v=p&&((N=(H=u.locales)==null?void 0:H[e.value])==null?void 0:N.translations)||null,$=p&&u.translations||null;let L=v,b=$,P=o;const M=i.pop();for(const B of i){let U=null;const K=P==null?void 0:P[B];K&&(U=P=K);const se=b==null?void 0:b[B];se&&(U=b=se);const oe=L==null?void 0:L[B];oe&&(U=L=oe),K||(P=U),se||(b=U),oe||(L=U)}return(L==null?void 0:L[M])??(b==null?void 0:b[M])??(P==null?void 0:P[M])??""}return s}const co=["aria-label"],uo={class:"DocSearch-Button-Container"},vo={class:"DocSearch-Button-Placeholder"},be=m({__name:"VPNavBarSearchButton",setup(o){const t=lo({button:{buttonText:"Search",buttonAriaLabel:"Search"}});return(s,n)=>(a(),c("button",{type:"button",class:"DocSearch DocSearch-Button","aria-label":r(t)("button.buttonAriaLabel")},[l("span",uo,[n[0]||(n[0]=l("span",{class:"vp-icon DocSearch-Search-Icon"},null,-1)),l("span",vo,S(r(t)("button.buttonText")),1)]),n[1]||(n[1]=l("span",{class:"DocSearch-Button-Keys"},[l("kbd",{class:"DocSearch-Button-Key"}),l("kbd",{class:"DocSearch-Button-Key"},"K")],-1))],8,co))}}),po={class:"VPNavBarSearch"},fo={id:"local-search"},ho={key:1,id:"docsearch"},mo=m({__name:"VPNavBarSearch",setup(o){const e=()=>null,t=()=>null,{theme:s}=V(),n=T(!1),i=T(!1);O(()=>{});function u(){n.value||(n.value=!0,setTimeout(p,16))}function p(){const L=new Event("keydown");L.key="k",L.metaKey=!0,window.dispatchEvent(L),setTimeout(()=>{document.querySelector(".DocSearch-Modal")||p()},16)}const v=T(!1),$="";return(L,b)=>{var P;return a(),c("div",po,[r($)==="local"?(a(),c(C,{key:0},[v.value?(a(),k(r(e),{key:0,onClose:b[0]||(b[0]=M=>v.value=!1)})):h("",!0),l("div",fo,[_(be,{onClick:b[1]||(b[1]=M=>v.value=!0)})])],64)):r($)==="algolia"?(a(),c(C,{key:1},[n.value?(a(),k(r(t),{key:0,algolia:((P=r(s).search)==null?void 0:P.options)??r(s).algolia,onVnodeBeforeMount:b[2]||(b[2]=M=>i.value=!0)},null,8,["algolia"])):h("",!0),i.value?h("",!0):(a(),c("div",ho,[_(be,{onClick:u})]))],64)):h("",!0)])}}}),_o=m({__name:"VPNavBarSocialLinks",setup(o){const{theme:e}=V();return(t,s)=>r(e).socialLinks?(a(),k(ge,{key:0,class:"VPNavBarSocialLinks",links:r(e).socialLinks},null,8,["links"])):h("",!0)}}),ko=g(_o,[["__scopeId","data-v-8f8d6fed"]]),go=["href","rel","target"],bo=["innerHTML"],$o={key:2},yo=m({__name:"VPNavBarTitle",setup(o){const{site:e,theme:t}=V(),{hasSidebar:s}=G(),{currentLang:n}=J(),i=y(()=>{var v;return typeof t.value.logoLink=="string"?t.value.logoLink:(v=t.value.logoLink)==null?void 0:v.link}),u=y(()=>{var v;return typeof t.value.logoLink=="string"||(v=t.value.logoLink)==null?void 0:v.rel}),p=y(()=>{var v;return typeof t.value.logoLink=="string"||(v=t.value.logoLink)==null?void 0:v.target});return(v,$)=>(a(),c("div",{class:w(["VPNavBarTitle",{"has-sidebar":r(s)}])},[l("a",{class:"title",href:i.value??r(fe)(r(n).link),rel:u.value,target:p.value},[d(v.$slots,"nav-bar-title-before",{},void 0,!0),r(t).logo?(a(),k(Y,{key:0,class:"logo",image:r(t).logo},null,8,["image"])):h("",!0),r(t).siteTitle?(a(),c("span",{key:1,innerHTML:r(t).siteTitle},null,8,bo)):r(t).siteTitle===void 0?(a(),c("span",$o,S(r(e).title),1)):h("",!0),d(v.$slots,"nav-bar-title-after",{},void 0,!0)],8,go)],2))}}),Po=g(yo,[["__scopeId","data-v-34d5de26"]]),Lo={class:"items"},Vo={class:"title"},So=m({__name:"VPNavBarTranslations",setup(o){const{theme:e}=V(),{localeLinks:t,currentLang:s}=J({correspondingLink:!0});return(n,i)=>r(t).length&&r(s).label?(a(),k(ke,{key:0,class:"VPNavBarTranslations",icon:"vpi-languages",label:r(e).langMenuLabel||"Change language"},{default:f(()=>[l("div",Lo,[l("p",Vo,S(r(s).label),1),(a(!0),c(C,null,A(r(t),u=>(a(),k(ne,{key:u.link,item:u},null,8,["item"]))),128))])]),_:1},8,["label"])):h("",!0)}}),To=g(So,[["__scopeId","data-v-9bfc6a91"]]),No={class:"wrapper"},Mo={class:"container"},Io={class:"title"},wo={class:"content"},Co={class:"content-body"},Ho=m({__name:"VPNavBar",props:{isScreenOpen:{type:Boolean}},emits:["toggle-screen"],setup(o){const e=o,{y:t}=Ve(),{hasSidebar:s}=G(),{frontmatter:n}=V(),i=T({});return ve(()=>{i.value={"has-sidebar":s.value,home:n.value.layout==="home",top:t.value===0,"screen-open":e.isScreenOpen}}),(u,p)=>(a(),c("div",{class:w(["VPNavBar",i.value])},[l("div",No,[l("div",Mo,[l("div",Io,[_(Po,null,{"nav-bar-title-before":f(()=>[d(u.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":f(()=>[d(u.$slots,"nav-bar-title-after",{},void 0,!0)]),_:3})]),l("div",wo,[l("div",Co,[d(u.$slots,"nav-bar-content-before",{},void 0,!0),_(mo,{class:"search"}),_(io,{class:"menu"}),_(To,{class:"translations"}),_(ms,{class:"appearance"}),_(ko,{class:"social-links"}),_(Qs,{class:"extra"}),d(u.$slots,"nav-bar-content-after",{},void 0,!0),_(eo,{class:"hamburger",active:u.isScreenOpen,onClick:p[0]||(p[0]=v=>u.$emit("toggle-screen"))},null,8,["active"])])])])]),p[1]||(p[1]=l("div",{class:"divider"},[l("div",{class:"divider-line"})],-1))],2))}}),Ao=g(Ho,[["__scopeId","data-v-a01e6a85"]]),Bo={key:0,class:"VPNavScreenAppearance"},Eo={class:"text"},Fo=m({__name:"VPNavScreenAppearance",setup(o){const{site:e,theme:t}=V();return(s,n)=>r(e).appearance&&r(e).appearance!=="force-dark"&&r(e).appearance!=="force-auto"?(a(),c("div",Bo,[l("p",Eo,S(r(t).darkModeSwitchLabel||"Appearance"),1),_(me)])):h("",!0)}}),Do=g(Fo,[["__scopeId","data-v-d75f9d29"]]),Oo=["innerHTML"],Go=m({__name:"VPNavScreenMenuLink",props:{item:{}},setup(o){const e=ee("close-screen");return(t,s)=>(a(),k(F,{class:"VPNavScreenMenuLink",href:t.item.link,target:t.item.target,rel:t.item.rel,"no-icon":t.item.noIcon,onClick:r(e)},{default:f(()=>[l("span",{innerHTML:t.item.text},null,8,Oo)]),_:1},8,["href","target","rel","no-icon","onClick"]))}}),Uo=g(Go,[["__scopeId","data-v-a0e8f5e2"]]),jo=["innerHTML"],zo=m({__name:"VPNavScreenMenuGroupLink",props:{item:{}},setup(o){const e=ee("close-screen");return(t,s)=>(a(),k(F,{class:"VPNavScreenMenuGroupLink",href:t.item.link,target:t.item.target,rel:t.item.rel,"no-icon":t.item.noIcon,onClick:r(e)},{default:f(()=>[l("span",{innerHTML:t.item.text},null,8,jo)]),_:1},8,["href","target","rel","no-icon","onClick"]))}}),He=g(zo,[["__scopeId","data-v-774c2599"]]),qo={class:"VPNavScreenMenuGroupSection"},Wo={key:0,class:"title"},Ko=m({__name:"VPNavScreenMenuGroupSection",props:{text:{},items:{}},setup(o){return(e,t)=>(a(),c("div",qo,[e.text?(a(),c("p",Wo,S(e.text),1)):h("",!0),(a(!0),c(C,null,A(e.items,s=>(a(),k(He,{key:s.text,item:s},null,8,["item"]))),128))]))}}),Jo=g(Ko,[["__scopeId","data-v-104421fc"]]),Ro=["aria-controls","aria-expanded"],Yo=["innerHTML"],Xo=["id"],Qo={key:0,class:"item"},Zo={key:1,class:"item"},xo={key:2,class:"group"},ea=m({__name:"VPNavScreenMenuGroup",props:{text:{},items:{}},setup(o){const e=o,t=T(!1),s=y(()=>`NavScreenGroup-${e.text.replace(" ","-").toLowerCase()}`);function n(){t.value=!t.value}return(i,u)=>(a(),c("div",{class:w(["VPNavScreenMenuGroup",{open:t.value}])},[l("button",{class:"button","aria-controls":s.value,"aria-expanded":t.value,onClick:n},[l("span",{class:"button-text",innerHTML:i.text},null,8,Yo),u[0]||(u[0]=l("span",{class:"vpi-plus button-icon"},null,-1))],8,Ro),l("div",{id:s.value,class:"items"},[(a(!0),c(C,null,A(i.items,p=>(a(),c(C,{key:JSON.stringify(p)},["link"in p?(a(),c("div",Qo,[_(He,{item:p},null,8,["item"])])):"component"in p?(a(),c("div",Zo,[(a(),k(E(p.component),j({ref_for:!0},p.props,{"screen-menu":""}),null,16))])):(a(),c("div",xo,[_(Jo,{text:p.text,items:p.items},null,8,["text","items"])]))],64))),128))],8,Xo)],2))}}),ta=g(ea,[["__scopeId","data-v-dc123f70"]]),na={key:0,class:"VPNavScreenMenu"},sa=m({__name:"VPNavScreenMenu",setup(o){const{theme:e}=V();return(t,s)=>r(e).nav?(a(),c("nav",na,[(a(!0),c(C,null,A(r(e).nav,n=>(a(),c(C,{key:JSON.stringify(n)},["link"in n?(a(),k(Uo,{key:0,item:n},null,8,["item"])):"component"in n?(a(),k(E(n.component),j({key:1,ref_for:!0},n.props,{"screen-menu":""}),null,16)):(a(),k(ta,{key:2,text:n.text||"",items:n.items},null,8,["text","items"]))],64))),128))])):h("",!0)}}),oa=m({__name:"VPNavScreenSocialLinks",setup(o){const{theme:e}=V();return(t,s)=>r(e).socialLinks?(a(),k(ge,{key:0,class:"VPNavScreenSocialLinks",links:r(e).socialLinks},null,8,["links"])):h("",!0)}}),aa={class:"list"},ra=m({__name:"VPNavScreenTranslations",setup(o){const{localeLinks:e,currentLang:t}=J({correspondingLink:!0}),s=T(!1);function n(){s.value=!s.value}return(i,u)=>r(e).length&&r(t).label?(a(),c("div",{key:0,class:w(["VPNavScreenTranslations",{open:s.value}])},[l("button",{class:"title",onClick:n},[u[0]||(u[0]=l("span",{class:"vpi-languages icon lang"},null,-1)),z(" "+S(r(t).label)+" ",1),u[1]||(u[1]=l("span",{class:"vpi-chevron-down icon chevron"},null,-1))]),l("ul",aa,[(a(!0),c(C,null,A(r(e),p=>(a(),c("li",{key:p.link,class:"item"},[_(F,{class:"link",href:p.link},{default:f(()=>[z(S(p.text),1)]),_:2},1032,["href"])]))),128))])],2)):h("",!0)}}),ia=g(ra,[["__scopeId","data-v-82c9b97d"]]),la={class:"container"},ca=m({__name:"VPNavScreen",props:{open:{type:Boolean}},setup(o){const e=T(null),t=Se(te?document.body:null);return(s,n)=>(a(),k(ue,{name:"fade",onEnter:n[0]||(n[0]=i=>t.value=!0),onAfterLeave:n[1]||(n[1]=i=>t.value=!1)},{default:f(()=>[s.open?(a(),c("div",{key:0,class:"VPNavScreen",ref_key:"screen",ref:e,id:"VPNavScreen"},[l("div",la,[d(s.$slots,"nav-screen-content-before",{},void 0,!0),_(sa,{class:"menu"}),_(ia,{class:"translations"}),_(Do,{class:"appearance"}),_(oa,{class:"social-links"}),d(s.$slots,"nav-screen-content-after",{},void 0,!0)])],512)):h("",!0)]),_:3}))}}),ua=g(ca,[["__scopeId","data-v-eed74814"]]),da={key:0,class:"VPNav"},va=m({__name:"VPNav",setup(o){const{isScreenOpen:e,closeScreen:t,toggleScreen:s}=rs(),{frontmatter:n}=V(),i=y(()=>n.value.navbar!==!1);return Te("close-screen",t),X(()=>{te&&document.documentElement.classList.toggle("hide-nav",!i.value)}),(u,p)=>i.value?(a(),c("header",da,[_(Ao,{"is-screen-open":r(e),onToggleScreen:r(s)},{"nav-bar-title-before":f(()=>[d(u.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":f(()=>[d(u.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":f(()=>[d(u.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":f(()=>[d(u.$slots,"nav-bar-content-after",{},void 0,!0)]),_:3},8,["is-screen-open","onToggleScreen"]),_(ua,{open:r(e)},{"nav-screen-content-before":f(()=>[d(u.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":f(()=>[d(u.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3},8,["open"])])):h("",!0)}}),pa=g(va,[["__scopeId","data-v-1d136424"]]),fa=["role","tabindex"],ha={key:1,class:"items"},ma=m({__name:"VPSidebarItem",props:{item:{},depth:{}},setup(o){const e=o,{collapsed:t,collapsible:s,isLink:n,isActiveLink:i,hasActiveLink:u,hasChildren:p,toggle:v}=ct(y(()=>e.item)),$=y(()=>p.value?"section":"div"),L=y(()=>n.value?"a":"div"),b=y(()=>p.value?e.depth+2===7?"p":`h${e.depth+2}`:"p"),P=y(()=>n.value?void 0:"button"),M=y(()=>[[`level-${e.depth}`],{collapsible:s.value},{collapsed:t.value},{"is-link":n.value},{"is-active":i.value},{"has-active":u.value}]);function I(N){"key"in N&&N.key!=="Enter"||!e.item.link&&v()}function H(){e.item.link&&v()}return(N,B)=>{const U=W("VPSidebarItem",!0);return a(),k(E($.value),{class:w(["VPSidebarItem",M.value])},{default:f(()=>[N.item.text?(a(),c("div",j({key:0,class:"item",role:P.value},je(N.item.items?{click:I,keydown:I}:{},!0),{tabindex:N.item.items&&0}),[B[1]||(B[1]=l("div",{class:"indicator"},null,-1)),N.item.link?(a(),k(F,{key:0,tag:L.value,class:"link",href:N.item.link,rel:N.item.rel,target:N.item.target},{default:f(()=>[(a(),k(E(b.value),{class:"text",innerHTML:N.item.text},null,8,["innerHTML"]))]),_:1},8,["tag","href","rel","target"])):(a(),k(E(b.value),{key:1,class:"text",innerHTML:N.item.text},null,8,["innerHTML"])),N.item.collapsed!=null&&N.item.items&&N.item.items.length?(a(),c("div",{key:2,class:"caret",role:"button","aria-label":"toggle section",onClick:H,onKeydown:Ue(H,["enter"]),tabindex:"0"},B[0]||(B[0]=[l("span",{class:"vpi-chevron-right caret-icon"},null,-1)]),32)):h("",!0)],16,fa)):h("",!0),N.item.items&&N.item.items.length?(a(),c("div",ha,[N.depth<5?(a(!0),c(C,{key:0},A(N.item.items,K=>(a(),k(U,{key:K.text,item:K,depth:N.depth+1},null,8,["item","depth"]))),128)):h("",!0)])):h("",!0)]),_:1},8,["class"])}}}),_a=g(ma,[["__scopeId","data-v-dad44e86"]]),ka=m({__name:"VPSidebarGroup",props:{items:{}},setup(o){const e=T(!0);let t=null;return O(()=>{t=setTimeout(()=>{t=null,e.value=!1},300)}),ze(()=>{t!=null&&(clearTimeout(t),t=null)}),(s,n)=>(a(!0),c(C,null,A(s.items,i=>(a(),c("div",{key:i.text,class:w(["group",{"no-transition":e.value}])},[_(_a,{item:i,depth:0},null,8,["item"])],2))),128))}}),ga=g(ka,[["__scopeId","data-v-994dc249"]]),ba={class:"nav",id:"VPSidebarNav","aria-labelledby":"sidebar-aria-label",tabindex:"-1"},$a=m({__name:"VPSidebar",props:{open:{type:Boolean}},setup(o){const{sidebarGroups:e,hasSidebar:t}=G(),s=o,n=T(null),i=Se(te?document.body:null);D([s,n],()=>{var p;s.open?(i.value=!0,(p=n.value)==null||p.focus()):i.value=!1},{immediate:!0,flush:"post"});const u=T(0);return D(e,()=>{u.value+=1},{deep:!0}),(p,v)=>r(t)?(a(),c("aside",{key:0,class:w(["VPSidebar",{open:p.open}]),ref_key:"navEl",ref:n,onClick:v[0]||(v[0]=qe(()=>{},["stop"]))},[v[2]||(v[2]=l("div",{class:"curtain"},null,-1)),l("nav",ba,[v[1]||(v[1]=l("span",{class:"visually-hidden",id:"sidebar-aria-label"}," Sidebar Navigation ",-1)),d(p.$slots,"sidebar-nav-before",{},void 0,!0),(a(),k(ga,{items:r(e),key:u.value},null,8,["items"])),d(p.$slots,"sidebar-nav-after",{},void 0,!0)])],2)):h("",!0)}}),ya=g($a,[["__scopeId","data-v-3746e67f"]]),Pa=m({__name:"VPSkipLink",setup(o){const e=x(),t=T();D(()=>e.path,()=>t.value.focus());function s({target:n}){const i=document.getElementById(decodeURIComponent(n.hash).slice(1));if(i){const u=()=>{i.removeAttribute("tabindex"),i.removeEventListener("blur",u)};i.setAttribute("tabindex","-1"),i.addEventListener("blur",u),i.focus(),window.scrollTo(0,0)}}return(n,i)=>(a(),c(C,null,[l("span",{ref_key:"backToTop",ref:t,tabindex:"-1"},null,512),l("a",{href:"#VPContent",class:"VPSkipLink visually-hidden",onClick:s}," Skip to content ")],64))}}),La=g(Pa,[["__scopeId","data-v-474ecce8"]]),Va=m({__name:"Layout",setup(o){const{isOpen:e,open:t,close:s}=G(),n=x();D(()=>n.path,s),lt(e,s);const{frontmatter:i}=V(),u=We(),p=y(()=>!!u["home-hero-image"]);return Te("hero-image-slot-exists",p),(v,$)=>{const L=W("Content");return r(i).layout!==!1?(a(),c("div",{key:0,class:w(["Layout",r(i).pageClass])},[d(v.$slots,"layout-top",{},void 0,!0),_(La),_(Ye,{class:"backdrop",show:r(e),onClick:r(s)},null,8,["show","onClick"]),_(pa,null,{"nav-bar-title-before":f(()=>[d(v.$slots,"nav-bar-title-before",{},void 0,!0)]),"nav-bar-title-after":f(()=>[d(v.$slots,"nav-bar-title-after",{},void 0,!0)]),"nav-bar-content-before":f(()=>[d(v.$slots,"nav-bar-content-before",{},void 0,!0)]),"nav-bar-content-after":f(()=>[d(v.$slots,"nav-bar-content-after",{},void 0,!0)]),"nav-screen-content-before":f(()=>[d(v.$slots,"nav-screen-content-before",{},void 0,!0)]),"nav-screen-content-after":f(()=>[d(v.$slots,"nav-screen-content-after",{},void 0,!0)]),_:3}),_(as,{open:r(e),onOpenMenu:r(t)},null,8,["open","onOpenMenu"]),_(ya,{open:r(e)},{"sidebar-nav-before":f(()=>[d(v.$slots,"sidebar-nav-before",{},void 0,!0)]),"sidebar-nav-after":f(()=>[d(v.$slots,"sidebar-nav-after",{},void 0,!0)]),_:3},8,["open"]),_(zn,null,{"page-top":f(()=>[d(v.$slots,"page-top",{},void 0,!0)]),"page-bottom":f(()=>[d(v.$slots,"page-bottom",{},void 0,!0)]),"not-found":f(()=>[d(v.$slots,"not-found",{},void 0,!0)]),"home-hero-before":f(()=>[d(v.$slots,"home-hero-before",{},void 0,!0)]),"home-hero-info-before":f(()=>[d(v.$slots,"home-hero-info-before",{},void 0,!0)]),"home-hero-info":f(()=>[d(v.$slots,"home-hero-info",{},void 0,!0)]),"home-hero-info-after":f(()=>[d(v.$slots,"home-hero-info-after",{},void 0,!0)]),"home-hero-actions-after":f(()=>[d(v.$slots,"home-hero-actions-after",{},void 0,!0)]),"home-hero-image":f(()=>[d(v.$slots,"home-hero-image",{},void 0,!0)]),"home-hero-after":f(()=>[d(v.$slots,"home-hero-after",{},void 0,!0)]),"home-features-before":f(()=>[d(v.$slots,"home-features-before",{},void 0,!0)]),"home-features-after":f(()=>[d(v.$slots,"home-features-after",{},void 0,!0)]),"doc-footer-before":f(()=>[d(v.$slots,"doc-footer-before",{},void 0,!0)]),"doc-before":f(()=>[d(v.$slots,"doc-before",{},void 0,!0)]),"doc-after":f(()=>[d(v.$slots,"doc-after",{},void 0,!0)]),"doc-top":f(()=>[d(v.$slots,"doc-top",{},void 0,!0)]),"doc-bottom":f(()=>[d(v.$slots,"doc-bottom",{},void 0,!0)]),"aside-top":f(()=>[d(v.$slots,"aside-top",{},void 0,!0)]),"aside-bottom":f(()=>[d(v.$slots,"aside-bottom",{},void 0,!0)]),"aside-outline-before":f(()=>[d(v.$slots,"aside-outline-before",{},void 0,!0)]),"aside-outline-after":f(()=>[d(v.$slots,"aside-outline-after",{},void 0,!0)]),"aside-ads-before":f(()=>[d(v.$slots,"aside-ads-before",{},void 0,!0)]),"aside-ads-after":f(()=>[d(v.$slots,"aside-ads-after",{},void 0,!0)]),_:3}),_(Rn),d(v.$slots,"layout-bottom",{},void 0,!0)],2)):(a(),k(L,{key:1}))}}}),Sa=g(Va,[["__scopeId","data-v-793c26c5"]]),Ta={Layout:Sa,enhanceApp:({app:o})=>{o.component("Badge",Ke)}},Na={class:"custom-home"},Ma={class:"hero"},Ia={class:"hero-content"},wa={class:"hero-title"},Ca={class:"gradient-text"},Ha={class:"hero-subtitle"},Aa={class:"hero-tagline"},Ba={class:"hero-buttons"},Ea=["href"],Fa=["href"],Da={class:"hero-image"},Oa={class:"image-container"},Ga=["src","alt"],Ua={class:"features"},ja={class:"features-grid"},za={class:"feature-icon"},qa={class:"carousel-section"},Wa={class:"carousel-container"},Ka=["src","alt"],Ja=["disabled"],Ra=["disabled"],Ya={class:"carousel-dots"},Xa=["onClick"],Qa={__name:"CustomHome",setup(o){const{frontmatter:e}=$e(),t=T(0),s=T(!1),n=T(null),i=()=>{s.value||(s.value=!0,t.value=(t.value+1)%e.value.images.length,setTimeout(()=>{s.value=!1},500))},u=()=>{s.value||(s.value=!0,t.value=t.value===0?e.value.images.length-1:t.value-1,setTimeout(()=>{s.value=!1},500))},p=P=>{s.value||t.value===P||(s.value=!0,t.value=P,setTimeout(()=>{s.value=!1},500))},v=()=>{n.value=setInterval(i,5e3)},$=()=>{n.value&&(clearInterval(n.value),n.value=null)},L=()=>$(),b=()=>v();return O(()=>{v();const P=document.querySelector(".carousel-container");P&&(P.addEventListener("mouseenter",L),P.addEventListener("mouseleave",b))}),Q(()=>{$();const P=document.querySelector(".carousel-container");P&&(P.removeEventListener("mouseenter",L),P.removeEventListener("mouseleave",b))}),(P,M)=>(a(),c("div",Na,[l("div",Ma,[l("div",Ia,[l("h1",wa,[l("span",Ca,S(r(e).hero.name),1)]),l("p",Ha,S(r(e).hero.text),1),l("p",Aa,S(r(e).hero.tagline),1),l("div",Ba,[l("a",{class:"button primary",href:r(e).hero.actions[0].link},S(r(e).hero.actions[0].text),9,Ea),r(e).hero.actions[1]?(a(),c("a",{key:0,class:"button secondary",href:r(e).hero.actions[1].link},S(r(e).hero.actions[1].text),9,Fa)):h("",!0)])]),l("div",Da,[l("div",Oa,[l("img",{src:r(e).hero.image.src,alt:r(e).hero.image.alt},null,8,Ga)]),M[0]||(M[0]=l("div",{class:"floating-elements"},[l("div",{class:"element element-1"}),l("div",{class:"element element-2"}),l("div",{class:"element element-3"})],-1))])]),l("div",Ua,[l("div",ja,[(a(!0),c(C,null,A(r(e).features,I=>(a(),c("div",{key:I.title,class:"feature-card"},[l("div",za,S(I.icon),1),l("strong",null,S(I.title),1),l("p",null,S(I.details),1)]))),128))])]),l("div",qa,[l("div",Wa,[l("div",{class:"carousel-track",style:pe({transform:`translateX(-${t.value*100}%)`})},[(a(!0),c(C,null,A(r(e).images,(I,H)=>(a(),c("div",{key:H,class:"carousel-slide"},[l("img",{src:I.src,alt:I.alt},null,8,Ka)]))),128))],4),l("button",{class:"carousel-button prev",onClick:u,disabled:s.value}," ← ",8,Ja),l("button",{class:"carousel-button next",onClick:i,disabled:s.value}," → ",8,Ra),l("div",Ya,[(a(!0),c(C,null,A(r(e).images,(I,H)=>(a(),c("button",{key:H,class:w(["carousel-dot",{active:t.value===H}]),onClick:N=>p(H)},null,10,Xa))),128))])])])]))}},Za=g(Qa,[["__scopeId","data-v-58648596"]]),er={...Ta,enhanceApp({app:o,router:e,siteData:t}){o.component("CustomHome",Za)}};export{er as t}; diff --git a/assets/data_details.json b/assets/data_details.json new file mode 100644 index 00000000..95ec8fea --- /dev/null +++ b/assets/data_details.json @@ -0,0 +1,958 @@ +{ + "meta": { + "registry": "https://registry.npmjs.org/", + "time": 1735302762830, + "total": 38 + }, + "list": [ + { + "name": "@kotori-bot/kotori-plugin-webui", + "description": "网页控制台", + "category": [ + "official", + "plugin" + ], + "version": "1.4.1", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1708491654698, + "modified": 1727856998729 + }, + "dist": { + "dependencies": 1, + "fileCount": 84, + "unpackedSize": 3952587, + "tarball": "https://registry.npmjs.org/@kotori-bot/kotori-plugin-webui/-/kotori-plugin-webui-1.4.1.tgz" + }, + "keywords": [], + "readme": "# @kotori-bot/kotori-plugin-webui\r\n\r\nWeb front end repository: [kotorijs/webui](https://github.com/kotorijs/webui)\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n\r\n## TODO\r\n\r\n- [x] Add authorization for websocket server\r\n- [x] Login at the same time\r\n- [x] Console outputting support for cmd adapter\r\n- [x] Websocket verify\r\n- [x] Sync offline and other data\r\n" + }, + { + "name": "kotori-plugin-goodnight", + "description": "早晚安插件", + "category": [ + "plugin" + ], + "version": "1.3.5", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848079212, + "modified": 1723200621314 + }, + "dist": { + "dependencies": 0, + "fileCount": 9, + "unpackedSize": 48231, + "tarball": "https://registry.npmjs.org/kotori-plugin-goodnight/-/kotori-plugin-goodnight-1.3.5.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-goodnight\r\n\r\nAnd in case i don't see you,good afternoon,good evening,and good night!\r\n\r\n## Regexps\r\n\r\n- 早|早安|早上好\r\n- 晚|晚安|晚上好\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n" + }, + { + "name": "@kotori-bot/kotori-plugin-core", + "description": "核心插件", + "category": [ + "official", + "plugin" + ], + "version": "1.4.4", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848079647, + "modified": 1723200616576 + }, + "dist": { + "dependencies": 0, + "fileCount": 9, + "unpackedSize": 60989, + "tarball": "https://registry.npmjs.org/@kotori-bot/kotori-plugin-core/-/kotori-plugin-core-1.4.4.tgz" + }, + "keywords": [], + "readme": "# @kotori-bot/kotori-plugin-core\n\nBase on tencent api.\n\n## Commands\n\n- `/core` View instance statistics\n- `/bot` View current bot info and running status\n- `/bots` View all bots info and running status\n- `/about` View about info\n\n```text\n/core\n> Global Language: en_US\nRoot Directory: \\usr\\kotori\nRunning Mode: dev\nNumber of Modules: 21\nNumber of Services: 3\nNumber of Bots: 1\nNumber of Middlewares: 1\nNumber of Commands: 27\nNumber of Regexps: 25\n\n/bot\n> ID: cmd-test\nLanguage: en_US\nPlatform: cmd\nSelf ID: 720\nConnect Time: 24/2/4 18:36:36\nMessages Received: 16\nMessages Sent: 15\nOffline Times: 0\nLast Message Time: 24/2/4 18:45:45\n\n/bots\n> Bot List:\n----------\nID: cmd-test\nLanguage: en_US\nPlatform: cmd\nStatus: online\n\n/about\n> Kotori Version: 1.2.0\nLicense: GPL-3.0\nNodeJS Version: v18.18.1\n```\n\n## Reference\n\n- [Kotori Docs](https://kotori.js.org/)\n" + }, + { + "name": "kotori-plugin-better-join", + "description": "趣味的进群欢迎提示", + "category": [ + "plugin" + ], + "version": "1.2.2", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848078986, + "modified": 1723200531994 + }, + "dist": { + "dependencies": 0, + "fileCount": 7, + "unpackedSize": 47180, + "tarball": "https://registry.npmjs.org/kotori-plugin-better-join/-/kotori-plugin-better-join-1.2.2.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-better-join\r\n\r\n更好的(奇奇怪怪的)新人加入群聊欢迎。\r\n\r\n## 自定义语录\r\n\r\n```typescript\r\n// ./src/data.ts\r\nexport default [\r\n // ...\r\n];\r\n```\r\n\r\n## 鸣谢\r\n\r\n- QQ:337**\\*\\***042 提供的部分语录\r\n" + }, + { + "name": "@kotori-bot/adapter-telegram", + "description": "Telegram 适配器", + "category": [ + "official", + "adapter" + ], + "version": "1.0.2", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1723122127391, + "modified": 1723200527589 + }, + "dist": { + "dependencies": 3, + "fileCount": 11, + "unpackedSize": 49987, + "tarball": "https://registry.npmjs.org/@kotori-bot/adapter-telegram/-/adapter-telegram-1.0.2.tgz" + }, + "keywords": [ + "telegram" + ], + "readme": "# @kotori-bot/adapter-telegram\r\n\r\nSupports for telegram. Create own bot: [@BotFather](https://t.me/BotFather). And if you are in China, you can set proxy to connect telegram api.\r\n\r\n## Config\r\n\r\n```typescript\r\nexport const config = Tsu.Object({\r\n token: Tsu.String().describe(\"Bot's token\"),\r\n proxy: Tsu.String().optional().describe('Proxy address (if you are in China)')\r\n})\r\n```\r\n\r\n## Supports\r\n\r\n### Events\r\n\r\n- on_message (fully supported)\r\n\r\n### Api\r\n\r\n- sendPrivateMsg\r\n- sendGroupMsg\r\n- sendChannelMsg\r\n\r\n### Elements\r\n\r\n- text\r\n- image\r\n- voice\r\n- audio\r\n- video\r\n- location\r\n\r\n## TODO\r\n\r\nSupport more standard api...\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n- [Telegram APIs](https://core.telegram.org/api)\r\n" + }, + { + "name": "kotori-plugin-bilibili", + "description": "哔哩哔哩插件", + "category": [ + "plugin" + ], + "version": "1.3.0", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848078987, + "modified": 1723199800260 + }, + "dist": { + "dependencies": 0, + "fileCount": 5, + "unpackedSize": 41139, + "tarball": "https://registry.npmjs.org/kotori-plugin-bilibili/-/kotori-plugin-bilibili-1.3.0.tgz" + }, + "keywords": [], + "readme": "# ~~kotori-plugin-bilibili~~\r\n\r\n## Commands\r\n\r\n```\r\n/bili - Bilibili video info query\r\n/bilier - Bilibili User Information Query\r\n```\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n" + }, + { + "name": "kotori-plugin-weather", + "description": "天气查询", + "category": [ + "plugin" + ], + "version": "1.1.2", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848095942, + "modified": 1723122267841 + }, + "dist": { + "dependencies": 0, + "fileCount": 9, + "unpackedSize": 40762, + "tarball": "https://registry.npmjs.org/kotori-plugin-weather/-/kotori-plugin-weather-1.1.2.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-weather\r\n\r\n## Commands\r\n\r\n- `/weather ` Query city weather\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n" + }, + { + "name": "kotori-plugin-testing", + "description": "测试用的插件,提供了 eval 和 echo 指令", + "category": [ + "plugin" + ], + "version": "1.3.0", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848089484, + "modified": 1723122262797 + }, + "dist": { + "dependencies": 0, + "fileCount": 5, + "unpackedSize": 39810, + "tarball": "https://registry.npmjs.org/kotori-plugin-testing/-/kotori-plugin-testing-1.3.0.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-testing\n\n## Reference\n\n- [Kotori Docs](https://kotori.js.org/)\n" + }, + { + "name": "kotori-plugin-sed", + "description": "社工查询插件", + "category": [ + "plugin" + ], + "version": "1.0.1", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1708491637185, + "modified": 1723122257759 + }, + "dist": { + "dependencies": 0, + "fileCount": 10, + "unpackedSize": 46915, + "tarball": "https://registry.npmjs.org/kotori-plugin-sed/-/kotori-plugin-sed-1.0.1.tgz" + }, + "keywords": [], + "readme": "ERROR: No README data found!" + }, + { + "name": "kotori-plugin-run-code", + "description": "在线运行 JavaScript 和 Lua 代码", + "category": [ + "plugin" + ], + "version": "1.0.3", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1708491632698, + "modified": 1723122252764 + }, + "dist": { + "dependencies": 0, + "fileCount": 13, + "unpackedSize": 44925, + "tarball": "https://registry.npmjs.org/kotori-plugin-run-code/-/kotori-plugin-run-code-1.0.3.tgz" + }, + "keywords": [], + "readme": "# Run-code\r\n\r\nRun code with javascript and lua\r\n\r\n**Version:** 1.1.0\r\n**Author:** himeno\r\n**License:** GPL-3.0\r\n\r\n## List of command\r\n\r\n- /runjs <...code> - Run JavaScript code\r\n- /runlua <...code> - Run Lua Code\r\n\r\n## Lang Support\r\n\r\n- ja_JP\r\n- en_US\r\n- zh_TW\r\n- zh_CN\r\n" + }, + { + "name": "kotori-plugin-requester", + "description": "给主人报告的插件", + "category": [ + "plugin" + ], + "version": "1.1.2", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1708491627595, + "modified": 1723122247401 + }, + "dist": { + "dependencies": 0, + "fileCount": 10, + "unpackedSize": 53878, + "tarball": "https://registry.npmjs.org/kotori-plugin-requester/-/kotori-plugin-requester-1.1.2.tgz" + }, + "keywords": [], + "readme": "# Requester\r\n\r\nListen delete message and request event call to master\r\n\r\n**Version:** 1.0.0\r\n**Author:** himeno\r\n**License:** GPL-3.0\r\n\r\n## List of event\r\n\r\n- onGroupIncrease\r\n- onGroupDecrease\r\n- onGroupAdmin\r\n- onGroupBan\r\n- onGroupRecall\r\n- onFriendRecall\r\n- onGroupRequest\r\n- onFriendRequest\r\n- onGroupMsg\r\n- onPrivateMsg\r\n\r\n## Lang Support\r\n\r\n- ja_JP\r\n- en_US\r\n- zh_TW\r\n- zh_CN\r\n" + }, + { + "name": "kotori-plugin-randomimg", + "description": "Pixiv 等多种随机图片", + "category": [ + "plugin" + ], + "version": "1.2.3", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848089223, + "modified": 1723122242285 + }, + "dist": { + "dependencies": 0, + "fileCount": 10, + "unpackedSize": 43666, + "tarball": "https://registry.npmjs.org/kotori-plugin-randomimg/-/kotori-plugin-randomimg-1.2.3.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-random-img\r\n\r\n## Commands\r\n\r\n- `/sex [tags]` Pixiv images\r\n- `/sexh` HuliImg images\r\n- `/bing` Bing daily image\r\n- `/day` See the world daily\r\n- `/earth` Real-time Earth\r\n- `/china` Real-time China\r\n\r\n```test\r\n/sex 足\r\n> Image coming right up...please be patient\r\n> PID: 84449466\r\nTitle: フランちゃん\r\nAuthor: うてきろ\r\nTags: 東方 东方 東方Project 东方Project フランドール・スカーレット 芙兰朵露・斯卡蕾特 絆創膏 创可贴 理性を破壊する程度の能力 足以破坏理性的能力 くわえたくしあげ shirt held up with the mouth BAN装甲 剥ぎ取られたパンツ R-17.9\r\n> [image,https://i.pixiv.re/img-original/img/2020/09/18/18/23/39/84449466_p0.jpg]\r\n\r\n```\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n" + }, + { + "name": "kotori-plugin-nmsl", + "description": "孙笑川在线帮你翻译成抽象话", + "category": [ + "plugin" + ], + "version": "2.1.1", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848089133, + "modified": 1723122231195 + }, + "dist": { + "dependencies": 0, + "fileCount": 9, + "unpackedSize": 96768, + "tarball": "https://registry.npmjs.org/kotori-plugin-nmsl/-/kotori-plugin-nmsl-2.1.1.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-nmsl\r\n\r\n孙笑川在线帮你翻译成抽象话。\r\n\r\n## Commands\r\n\r\n- `/nmsl [...抽🐘话]` 孙G在🧵帮你翻译成抽🐘话\r\n\r\n```text\r\n/nmsl 愚者说:平等 !但是世人皆知,世上并没有平等! 愚者说:自由 !但是世人皆知,世上并没有自由! 愚者说:爱情 !但是世人皆知,爱情随时会背叛! 愚者说:莫杀人 !但是世人皆知,世界充满了杀戮! 愚者说:切莫说谎 !但是世人皆知,愚者就在说谎!——间宫卓司《美好的每一天~不连续的存在》 24/2/4 20:32:32\r\n> 翻译🎀果👇\r\n愚者说:平等!但是世人皆知,世⬆并没🈶平等! 愚者说:自由!但是世人皆知,世⬆并没🈶自由! 愚者说:💓情!但是世人皆知,💓情随时会背叛! 愚者说:莫🍤!但是世人皆知,🌏充🈵🌶️杀戮! 愚者说:切莫说谎!但是世人皆知,愚者就在说谎!——间宫卓司《美👌每1️⃣天~8️⃣连续💧存在》\r\n抽象度:11\r\n```\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n- [gaowanliang/NMSL](https://github.com/gaowanliang/NMSL)\r\n\r\n## Tips\r\n\r\n> 仅供娱乐\r\n" + }, + { + "name": "kotori-plugin-penis", + "description": "查看今日牛牛的长度和排行", + "category": [ + "plugin" + ], + "version": "1.2.3", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848089203, + "modified": 1723122225587 + }, + "dist": { + "dependencies": 0, + "fileCount": 9, + "unpackedSize": 47268, + "tarball": "https://registry.npmjs.org/kotori-plugin-penis/-/kotori-plugin-penis-1.2.3.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-newnew\r\n\r\n查看今日长度,fasong `/今日长度` 或 `今日长度`。\r\n\r\n## Config\r\n\r\n```typescript\r\ninterface Config {\r\n max?: number;\r\n min?: number;\r\n joke?: number;\r\n avgMinNum?: number;\r\n}\r\n```\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n" + }, + { + "name": "kotori-plugin-music", + "description": "网易云音乐搜索", + "category": [ + "plugin" + ], + "version": "1.2.4", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848089043, + "modified": 1723122219136 + }, + "dist": { + "dependencies": 0, + "fileCount": 13, + "unpackedSize": 45990, + "tarball": "https://registry.npmjs.org/kotori-plugin-music/-/kotori-plugin-music-1.2.4.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-music\r\n\r\n## Commands\r\n\r\n- `/music <...name>` NetEase Cloud music\r\n - `-o, --order: number` Order number\r\n\r\n```text\r\n/music 虚無の先で愛を見つける\r\n> Song ID: 2077749631\r\nSong Title: 虚無の先で愛を見つける-Piano ver\r\nSong Author: おはる\r\nSong Download: http://music.163.com/song/media/outer/url?id=2077749631.mp3\r\nSong Cover: [image,http://p1.music.126.net/77V-Bc4EoboXGOZZ8TxDBQ==/109951168879037291.jpg?param=300x300]\r\n\r\n/music 虚無の先で愛を見つける -o 0\r\n> 1.虚無の先で愛を見つける-Piano ver - おはる\r\nResend command and input [number] to choose corresponding content\r\nExample: /command content 2\r\n```\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n" + }, + { + "name": "kotori-plugin-minecraft", + "description": "Minecraft JE/BE 服务器,账号皮肤,最新版本查询", + "category": [ + "plugin" + ], + "version": "1.0.2", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1708491607634, + "modified": 1723122212921 + }, + "dist": { + "dependencies": 0, + "fileCount": 12, + "unpackedSize": 55105, + "tarball": "https://registry.npmjs.org/kotori-plugin-minecraft/-/kotori-plugin-minecraft-1.0.2.tgz" + }, + "keywords": [], + "readme": "ERROR: No README data found!" + }, + { + "name": "kotori-plugin-mediawiki", + "description": "维基搜索,支持自定义多个维基", + "category": [ + "plugin" + ], + "version": "1.2.5", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848088999, + "modified": 1723122207322 + }, + "dist": { + "dependencies": 0, + "fileCount": 13, + "unpackedSize": 48701, + "tarball": "https://registry.npmjs.org/kotori-plugin-mediawiki/-/kotori-plugin-mediawiki-1.2.5.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-mediawiki\r\n\r\nmediawiki search tool.\r\n\r\n## Commands\r\n\r\n- `/wiki [order:number]` Search MediaWiki\r\n- `/wikil` View MediaWiki list\r\n\r\n```test\r\n/wiki 月社妃\r\n> Title: 月社妃\r\nContent: 月社妃(日语:月社(つきやしろ) 妃(きさき))是由ウグイスカグラ所制作的18禁galgame《纸上的魔法使》及其衍生作品的登场角色。是主人公四条琉璃的同胞妹妹。\r\nhttps://mzh.moegirl.org.cn/index.php?curid=384932\r\nSource: 萌娘百科\r\n/wikil\r\n> MediaWiki List:\r\n1. 萌娘百科 - https://mzh.moegirl.org.cn/api.php\r\n2. MCWIKI - https://minecraft.fandom.com/zh/api.php\r\n\r\n```\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n" + }, + { + "name": "kotori-plugin-manger", + "description": "简易群管插件", + "category": [ + "plugin" + ], + "version": "1.0.5", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1708491596825, + "modified": 1723122201497 + }, + "dist": { + "dependencies": 0, + "fileCount": 9, + "unpackedSize": 50455, + "tarball": "https://registry.npmjs.org/kotori-plugin-manger/-/kotori-plugin-manger-1.0.5.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-manger\r\n\r\nGroups Mange tools.\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n" + }, + { + "name": "kotori-plugin-hitokotos", + "description": "随机一言,十七种不同语录", + "category": [ + "plugin" + ], + "version": "1.2.1", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848089056, + "modified": 1723122196673 + }, + "dist": { + "dependencies": 0, + "fileCount": 9, + "unpackedSize": 41187, + "tarball": "https://registry.npmjs.org/kotori-plugin-hitokotos/-/kotori-plugin-hitokotos-1.2.1.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-hitokotos\r\n\r\nSeventeen kind of different sentences.\r\n\r\n## Commands\r\n\r\n- `/hitokoto` Get A Hitokoto\r\n- `/hitokotos` Random Quotations\r\n\r\n```text\r\n/hitokoto\r\n> 愚者说:平等 !但是世人皆知,世上并没有平等! 愚者说:自由 !但是世人皆知,世上并没有自由! 愚者说:爱情 !但是世人皆知,爱情随时会背叛! 愚者说:莫杀人 !但是世人皆知,世界充满了杀戮! 愚者说:切莫说谎 !但是世人皆知,愚者就在说谎!——间宫卓司《美好的每一天~不连续的存在》\r\n\r\n/hitokotos\r\n> 随机语录\r\n一言 一言2\r\n诗词 情话\r\n骚话 笑话\r\n人生语录 社会语录\r\n网抑云 毒鸡汤\r\n舔狗语录 爱情语录\r\n温柔语录 个性签名\r\n经典语录 英汉语录\r\n```\r\n\r\n## Regexps\r\n\r\n- 一言\r\n- 一言2\r\n- 诗词\r\n- 情话\r\n- 骚话\r\n- 笑话\r\n- 人生语录\r\n- 社会语录\r\n- 网抑云\r\n- 毒鸡汤\r\n- 舔狗语录\r\n- 爱情语录\r\n- 温柔语录\r\n- 个性签名\r\n- 经典语录\r\n- 英汉语录\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n" + }, + { + "name": "@kotori-bot/kotori-plugin-helper", + "description": "查看指令帮助", + "category": [ + "official", + "plugin" + ], + "version": "1.3.1", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848088127, + "modified": 1723122191799 + }, + "dist": { + "dependencies": 0, + "fileCount": 9, + "unpackedSize": 45195, + "tarball": "https://registry.npmjs.org/@kotori-bot/kotori-plugin-helper/-/kotori-plugin-helper-1.3.1.tgz" + }, + "keywords": [], + "readme": "# @kotori-bot/kotori-plugin-helper\r\n\r\n## Commands\r\n\r\n- `/help [...command]` View command help info\r\n- `/menu` View Bot's menu\r\n\r\n```bash\r\n> Found the following related commands:\r\n/core - View instance statistics\r\n/bot - View current bot info and running status\r\n/bots - View all bots info and running status\r\n/about - View about info\r\n/help [...command] - View command help info\r\n```\r\n\r\n## Config\r\n\r\n```typescript\r\ninterface Config extends ModuleConfig {\r\n alias?: string; // alias of /menu\r\n keywords?: string[]; // keyowrds of /menu\r\n content: string; // content of menu\r\n}\r\n```\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n" + }, + { + "name": "kotori-plugin-grouper", + "description": "群聊活跃排行与小游戏", + "category": [ + "plugin" + ], + "version": "1.3.0", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848079073, + "modified": 1723122186371 + }, + "dist": { + "dependencies": 0, + "fileCount": 5, + "unpackedSize": 45590, + "tarball": "https://registry.npmjs.org/kotori-plugin-grouper/-/kotori-plugin-grouper-1.3.0.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-grouper\r\n\r\ngroups activity ranking and simple games!\r\n\r\n# Regexps\r\n\r\n- 签到|打卡\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n" + }, + { + "name": "kotori-plugin-github", + "description": "GitHub 仓库搜索", + "category": [ + "plugin" + ], + "version": "1.3.0", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848079248, + "modified": 1723122175873 + }, + "dist": { + "dependencies": 0, + "fileCount": 9, + "unpackedSize": 41295, + "tarball": "https://registry.npmjs.org/kotori-plugin-github/-/kotori-plugin-github-1.3.0.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-github\r\n\r\nGithub repository searching.\r\n\r\n## Commands\r\n\r\n- `/github ` Query Github repository info\r\n\r\n```text\r\n/github kotorijs/kotori\r\n> URL: kotorijs/kotori\r\nDescription: Cross platform, decoupled, and modernized ChatBot framework base on NodeJS\r\nLanguage: TypeScript\r\nOwner: kotorijs\r\nCreated: 2023-06-14T11:45:16Z\r\nLast updated: 2024-01-30T09:55:47Z\r\nLast push: 2024-02-03T12:51:17Z\r\nOpen source license: GNU General Public License v3.0\r\n> [image,https://opengraph.githubassets.com/c9f4179f4d560950b2355c82aa2b7750bffd945744f9b8ea3f93cc24779745a0/kotorijs/kotori]\r\n```\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n" + }, + { + "name": "@kotori-bot/kotori-plugin-filter", + "description": "模块加载过滤器", + "category": [ + "official", + "plugin" + ], + "version": "1.1.0", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1717680461375, + "modified": 1723122170497 + }, + "dist": { + "dependencies": 2, + "fileCount": 5, + "unpackedSize": 40594, + "tarball": "https://registry.npmjs.org/@kotori-bot/kotori-plugin-filter/-/kotori-plugin-filter-1.1.0.tgz" + }, + "keywords": [], + "readme": "# @kotori-bot/kotori-plugin-filter\r\n\r\nfilter plugin for kotori-bot loader\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n" + }, + { + "name": "kotori-plugin-drift-bottle", + "description": "漂流瓶插件", + "category": [ + "plugin" + ], + "version": "1.0.3", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1708491561490, + "modified": 1723122165520 + }, + "dist": { + "dependencies": 0, + "fileCount": 9, + "unpackedSize": 41696, + "tarball": "https://registry.npmjs.org/kotori-plugin-drift-bottle/-/kotori-plugin-drift-bottle-1.0.3.tgz" + }, + "keywords": [], + "readme": "# drift-bottle\r\n\r\nthrow and pick bottles\r\n\r\n**Version:** 1.0.0\r\n**Author:** himeno\r\n**License:** GPL-3.0\r\n**Front Plugin:** grouper\r\n\r\n## List of command\r\n\r\n- /print <...content> - send a message on privates#^^\r\n- /echo <...message> - send a message on group\\*^^\r\n\r\n## Config\r\n\r\n```ts\r\nexport default {\r\n\tmaxNum: 3,\r\n};\r\n```\r\n\r\n## Lang Support\r\n\r\n- ja_JP\r\n- en_US\r\n- zh_TW\r\n- zh_CN\r\n" + }, + { + "name": "kotori-plugin-color", + "description": "随机颜色图片、日本传统色、中国传统色", + "category": [ + "plugin" + ], + "version": "1.0.3", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1717818140140, + "modified": 1723122153732 + }, + "dist": { + "dependencies": 0, + "fileCount": 12, + "unpackedSize": 319937, + "tarball": "https://registry.npmjs.org/kotori-plugin-color/-/kotori-plugin-color-1.0.3.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-color\r\n\r\n## Reference\r\n\r\n- [Kotori Docs](https://kotori.js.org/)\r\n" + }, + { + "name": "kotori-plugin-bangumi", + "description": "番组计划插件", + "category": [ + "plugin" + ], + "version": "1.2.4", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848078986, + "modified": 1723122140012 + }, + "dist": { + "dependencies": 0, + "fileCount": 11, + "unpackedSize": 46856, + "tarball": "https://registry.npmjs.org/kotori-plugin-bangumi/-/kotori-plugin-bangumi-1.2.4.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-bangumi\r\n\r\n## Commands\r\n\r\n- `/bgm [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-plugin-bangumi", + "description": "番组计划插件", + "category": [ + "plugin" + ], + "version": "1.2.4", + "author": { + "name": "Arimura Sena", + "email": "biyuehuya@gmail.com" + }, + "time": { + "created": 1703848078986, + "modified": 1723122140012 + }, + "dist": { + "dependencies": 0, + "fileCount": 11, + "unpackedSize": 46856, + "tarball": "https://registry.npmjs.org/kotori-plugin-bangumi/-/kotori-plugin-bangumi-1.2.4.tgz" + }, + "keywords": [], + "readme": "# kotori-plugin-bangumi\r\n\r\n## Commands\r\n\r\n- `/bgm [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.B1nKVjKI.js b/assets/guide_base_command.md.B1nKVjKI.js new file mode 100644 index 00000000..8e4c2389 --- /dev/null +++ b/assets/guide_base_command.md.B1nKVjKI.js @@ -0,0 +1,204 @@ +import{_ as i,c as a,a0 as h,o as k}from"./chunks/framework.P9qPzDnn.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,支持的类型有: stringnumberboolean
  • 可选参数中可在参数类型后添加 =值 设置默认参数
  • 参数中可在参数名前添加 ... 设置剩余参数,与 TypeScript 不同的是,剩余参数的类型不需要加上数组表示
  • 在指令模板字符最后添加 - 指令描述 设置指令描述
  • 指令内容为截止到第一个参数出现之前的字符串(不含空格)
  • 参数名应尽量语义化;剩余参数应在所有参数最后面;应仅在可选参数中设置默认参数;必要参数应在可选参数之前;指令模板字符不应包含指令前缀

选项

通过 Command.option() 设置指令选项,接受两个参数:

  1. 该选项的缩写名
  2. 选项模板字符,可设置多个指令选项
typescript
ctx
+  .command('bar')
+  .option('S', 'speaker - 这是选项的描述')
+  .option('G', 'global:boolean - 这是一个布尔类型的选项')
+  .option('T', 'time:number - 这是一个数字类型的选项');
  • 一般地,使用单个大写字母作为缩写名,解析器将把字符串中单个连接符 - 开头内容作为选项缩写名解析
  • 使用多个小写字母作为全名(多个单词使用 单个连接符 - 解析),解析器将把字符串中两个连接符 -- 开头的内容作为选项全名解析
  • 在选项模板字符最后添加 - 指令描述 设置选项描述
  • 类似地,选项模板字符有着与指令模板字符一样的类型注解方式,默认 string,支持的类型有: stringnumberboolean
  • 选项模板字符不支持设置默认值

回调函数

通过 Command.action() 设置指令的回调函数,且每个指令仅可设置一个回调函数,回调函数中接收两个参数:

  1. argsoptions 两个键组成的对象,类型分别为 (string | number | boolean)[]Record<string, string | number | boolean>,分别代表用户输入的参数值与选项值
  2. 会话事件数据

options 中的键为对应选项的全名而非缩写名。

typescript
ctx.command('bar <...args>').action((data) => {
+  ctx.logger.info(data.args); // 输出参数值数组
+  ctx.logger.info(data.options); // 输出选项值对象
+  session.send('这是一条消息');
+});

回调函数中的第二个参数为当前会话事件数据 sessionsession 对象包含了当前指令触发产生的所有上下文信息,比如消息 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_querylist_addlist_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 事件有 messageIdsendermessagegroupId,而 EventDataPrivateMsg 事件没有 groupIdEventDataPrivateRecall 事件其中的仅有 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 中的 「模板字符串」)。接收两个参数:

  1. 源字符串
  2. 模板字符串参数,其类型有两种,分别为 ObjectArgsArrayArgs
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''voidnull0 则不作处理(一般不允许传入这些东西,主要发生在指令处理的回调函数返回值上)
  • 对于 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_mangerno_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.B1nKVjKI.lean.js b/assets/guide_base_command.md.B1nKVjKI.lean.js new file mode 100644 index 00000000..8e4c2389 --- /dev/null +++ b/assets/guide_base_command.md.B1nKVjKI.lean.js @@ -0,0 +1,204 @@ +import{_ as i,c as a,a0 as h,o as k}from"./chunks/framework.P9qPzDnn.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,支持的类型有: stringnumberboolean
  • 可选参数中可在参数类型后添加 =值 设置默认参数
  • 参数中可在参数名前添加 ... 设置剩余参数,与 TypeScript 不同的是,剩余参数的类型不需要加上数组表示
  • 在指令模板字符最后添加 - 指令描述 设置指令描述
  • 指令内容为截止到第一个参数出现之前的字符串(不含空格)
  • 参数名应尽量语义化;剩余参数应在所有参数最后面;应仅在可选参数中设置默认参数;必要参数应在可选参数之前;指令模板字符不应包含指令前缀

选项

通过 Command.option() 设置指令选项,接受两个参数:

  1. 该选项的缩写名
  2. 选项模板字符,可设置多个指令选项
typescript
ctx
+  .command('bar')
+  .option('S', 'speaker - 这是选项的描述')
+  .option('G', 'global:boolean - 这是一个布尔类型的选项')
+  .option('T', 'time:number - 这是一个数字类型的选项');
  • 一般地,使用单个大写字母作为缩写名,解析器将把字符串中单个连接符 - 开头内容作为选项缩写名解析
  • 使用多个小写字母作为全名(多个单词使用 单个连接符 - 解析),解析器将把字符串中两个连接符 -- 开头的内容作为选项全名解析
  • 在选项模板字符最后添加 - 指令描述 设置选项描述
  • 类似地,选项模板字符有着与指令模板字符一样的类型注解方式,默认 string,支持的类型有: stringnumberboolean
  • 选项模板字符不支持设置默认值

回调函数

通过 Command.action() 设置指令的回调函数,且每个指令仅可设置一个回调函数,回调函数中接收两个参数:

  1. argsoptions 两个键组成的对象,类型分别为 (string | number | boolean)[]Record<string, string | number | boolean>,分别代表用户输入的参数值与选项值
  2. 会话事件数据

options 中的键为对应选项的全名而非缩写名。

typescript
ctx.command('bar <...args>').action((data) => {
+  ctx.logger.info(data.args); // 输出参数值数组
+  ctx.logger.info(data.options); // 输出选项值对象
+  session.send('这是一条消息');
+});

回调函数中的第二个参数为当前会话事件数据 sessionsession 对象包含了当前指令触发产生的所有上下文信息,比如消息 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_querylist_addlist_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 事件有 messageIdsendermessagegroupId,而 EventDataPrivateMsg 事件没有 groupIdEventDataPrivateRecall 事件其中的仅有 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 中的 「模板字符串」)。接收两个参数:

  1. 源字符串
  2. 模板字符串参数,其类型有两种,分别为 ObjectArgsArrayArgs
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''voidnull0 则不作处理(一般不允许传入这些东西,主要发生在指令处理的回调函数返回值上)
  • 对于 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_mangerno_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.BKi_1qAf.js b/assets/guide_base_events.md.BKi_1qAf.js new file mode 100644 index 00000000..de0faa7a --- /dev/null +++ b/assets/guide_base_events.md.BKi_1qAf.js @@ -0,0 +1,63 @@ +import{_ as i,c as a,a0 as h,o as n}from"./chunks/framework.P9qPzDnn.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.apiApi 的实例对象,提供了多种与聊天平台交互的接口,此处用到的 sendG丨groupMsgsendPrivateMsg 分别是发送群聊消息与发送私聊消息,第一个参数为消息内容,第二个参数分别为群聊 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.apiadapter.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.elsession.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.BKi_1qAf.lean.js b/assets/guide_base_events.md.BKi_1qAf.lean.js new file mode 100644 index 00000000..de0faa7a --- /dev/null +++ b/assets/guide_base_events.md.BKi_1qAf.lean.js @@ -0,0 +1,63 @@ +import{_ as i,c as a,a0 as h,o as n}from"./chunks/framework.P9qPzDnn.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.apiApi 的实例对象,提供了多种与聊天平台交互的接口,此处用到的 sendG丨groupMsgsendPrivateMsg 分别是发送群聊消息与发送私聊消息,第一个参数为消息内容,第二个参数分别为群聊 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.apiadapter.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.elsession.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.Do_XwOya.js b/assets/guide_base_middleware.md.Do_XwOya.js new file mode 100644 index 00000000..bb797538 --- /dev/null +++ b/assets/guide_base_middleware.md.Do_XwOya.js @@ -0,0 +1,59 @@ +import{_ as i,c as a,a0 as n,o as h}from"./chunks/framework.P9qPzDnn.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() 注册一个中间件,该方法接受两个参数:

  1. 中间件回调函数
  2. 可选的中间件优先级,默认为 50

优先级数字越小(但不能为负数)则优先级越高,如果两个中间件的优先级相同,则按照注册顺序执行,先注册的中间件会先执行。

WARNING

如无特殊需求建议请勿更改优先级,否则可能会导致一些意料之外的问题。

typescript
ctx.midware((next, session) => {
+  // 中间件逻辑...
+  next(); // 通过此中间件
+}, 80); // 优先级为 80

中间件回调函数接收两个参数:

  1. next 函数,调用它将执行下一个中间件
  2. 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 的各个环节进行拦截和处理,实现更加灵活和可控的消息处理逻辑。

限制命令使用频率

WARNING

以下内容由 Claude3 生成,不保证可用性。

有时我们需要限制某些命令的使用频率,以防止被滥用。这时可以使用中间件来实现这一功能。

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); // 设置较高优先级

上述代码定义了一个中间件,用于限制命令的使用频率。具体逻辑如下:

  1. 首先检查收到的消息是否以 / 开头,如果不是则直接调用 next()通过该中间件。
  2. 获取命令名和发送者 ID。
  3. 检查是否存在该命令的使用记录,如果没有则新建一个记录。
  4. 获取该用户对此命令的最后使用时间,如果不存在则认为是第一次使用,最后使用时间设为 0。
  5. 计算距离上次使用的时间间隔(单位为秒)。
  6. 如果时间间隔小于 10 秒,则拒绝执行该命令,发送 '命令使用过于频繁,请稍后再试'
  7. 如果时间间隔大于等于 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.Do_XwOya.lean.js b/assets/guide_base_middleware.md.Do_XwOya.lean.js new file mode 100644 index 00000000..bb797538 --- /dev/null +++ b/assets/guide_base_middleware.md.Do_XwOya.lean.js @@ -0,0 +1,59 @@ +import{_ as i,c as a,a0 as n,o as h}from"./chunks/framework.P9qPzDnn.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() 注册一个中间件,该方法接受两个参数:

  1. 中间件回调函数
  2. 可选的中间件优先级,默认为 50

优先级数字越小(但不能为负数)则优先级越高,如果两个中间件的优先级相同,则按照注册顺序执行,先注册的中间件会先执行。

WARNING

如无特殊需求建议请勿更改优先级,否则可能会导致一些意料之外的问题。

typescript
ctx.midware((next, session) => {
+  // 中间件逻辑...
+  next(); // 通过此中间件
+}, 80); // 优先级为 80

中间件回调函数接收两个参数:

  1. next 函数,调用它将执行下一个中间件
  2. 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 的各个环节进行拦截和处理,实现更加灵活和可控的消息处理逻辑。

限制命令使用频率

WARNING

以下内容由 Claude3 生成,不保证可用性。

有时我们需要限制某些命令的使用频率,以防止被滥用。这时可以使用中间件来实现这一功能。

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); // 设置较高优先级

上述代码定义了一个中间件,用于限制命令的使用频率。具体逻辑如下:

  1. 首先检查收到的消息是否以 / 开头,如果不是则直接调用 next()通过该中间件。
  2. 获取命令名和发送者 ID。
  3. 检查是否存在该命令的使用记录,如果没有则新建一个记录。
  4. 获取该用户对此命令的最后使用时间,如果不存在则认为是第一次使用,最后使用时间设为 0。
  5. 计算距离上次使用的时间间隔(单位为秒)。
  6. 如果时间间隔小于 10 秒,则拒绝执行该命令,发送 '命令使用过于频繁,请稍后再试'
  7. 如果时间间隔大于等于 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.kd1jlc-g.js b/assets/guide_base_regexp.md.kd1jlc-g.js new file mode 100644 index 00000000..7bdfbf34 --- /dev/null +++ b/assets/guide_base_regexp.md.kd1jlc-g.js @@ -0,0 +1,48 @@ +import{_ as i,c as a,a0 as h,o as k}from"./chunks/framework.P9qPzDnn.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() 注册一个正则匹配,该方法接受两个参数:

  1. match: 用于匹配消息内容的正则表达式
  2. callback: 当正则匹配成功时执行的回调函数
typescript
ctx.regexp(/^\\/start$/, (match, session) => {
+  session.send('游戏开始!');
+});

上述代码注册了一个正则匹配,当收到消息内容为 /start 时,它会执行回调函数,并向发送者发送 '游戏开始!' 消息。

callback 函数接收两个参数:

  1. match: 正则匹配结果,是一个数组,第一项为完整匹配结果,后续项为各个捕获组的内容
  2. 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.kd1jlc-g.lean.js b/assets/guide_base_regexp.md.kd1jlc-g.lean.js new file mode 100644 index 00000000..7bdfbf34 --- /dev/null +++ b/assets/guide_base_regexp.md.kd1jlc-g.lean.js @@ -0,0 +1,48 @@ +import{_ as i,c as a,a0 as h,o as k}from"./chunks/framework.P9qPzDnn.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() 注册一个正则匹配,该方法接受两个参数:

  1. match: 用于匹配消息内容的正则表达式
  2. callback: 当正则匹配成功时执行的回调函数
typescript
ctx.regexp(/^\\/start$/, (match, session) => {
+  session.send('游戏开始!');
+});

上述代码注册了一个正则匹配,当收到消息内容为 /start 时,它会执行回调函数,并向发送者发送 '游戏开始!' 消息。

callback 函数接收两个参数:

  1. match: 正则匹配结果,是一个数组,第一项为完整匹配结果,后续项为各个捕获组的内容
  2. 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.BkEYU-ym.js b/assets/guide_components_adapter.md.BkEYU-ym.js new file mode 100644 index 00000000..0ef84080 --- /dev/null +++ b/assets/guide_components_adapter.md.BkEYU-ym.js @@ -0,0 +1 @@ +import{_ as t,c as r,j as a,a as o,o as n}from"./chunks/framework.P9qPzDnn.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.BkEYU-ym.lean.js b/assets/guide_components_adapter.md.BkEYU-ym.lean.js new file mode 100644 index 00000000..0ef84080 --- /dev/null +++ b/assets/guide_components_adapter.md.BkEYU-ym.lean.js @@ -0,0 +1 @@ +import{_ as t,c as r,j as a,a as o,o as n}from"./chunks/framework.P9qPzDnn.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.CDibfftx.js b/assets/guide_components_api.md.CDibfftx.js new file mode 100644 index 00000000..e6ed07e4 --- /dev/null +++ b/assets/guide_components_api.md.CDibfftx.js @@ -0,0 +1 @@ +import{_ as t,c as o,j as a,a as n,o as r}from"./chunks/framework.P9qPzDnn.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.CDibfftx.lean.js b/assets/guide_components_api.md.CDibfftx.lean.js new file mode 100644 index 00000000..e6ed07e4 --- /dev/null +++ b/assets/guide_components_api.md.CDibfftx.lean.js @@ -0,0 +1 @@ +import{_ as t,c as o,j as a,a as n,o as r}from"./chunks/framework.P9qPzDnn.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.BNIS-43m.js b/assets/guide_components_custom.md.BNIS-43m.js new file mode 100644 index 00000000..62ac9548 --- /dev/null +++ b/assets/guide_components_custom.md.BNIS-43m.js @@ -0,0 +1 @@ +import{_ as a,c as o,j as t,a as s,o as n}from"./chunks/framework.P9qPzDnn.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.BNIS-43m.lean.js b/assets/guide_components_custom.md.BNIS-43m.lean.js new file mode 100644 index 00000000..62ac9548 --- /dev/null +++ b/assets/guide_components_custom.md.BNIS-43m.lean.js @@ -0,0 +1 @@ +import{_ as a,c as o,j as t,a as s,o as n}from"./chunks/framework.P9qPzDnn.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.BEJCY6Yx.js b/assets/guide_components_elements.md.BEJCY6Yx.js new file mode 100644 index 00000000..b6c36108 --- /dev/null +++ b/assets/guide_components_elements.md.BEJCY6Yx.js @@ -0,0 +1 @@ +import{_ as a,c as n,j as t,a as s,o}from"./chunks/framework.P9qPzDnn.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.BEJCY6Yx.lean.js b/assets/guide_components_elements.md.BEJCY6Yx.lean.js new file mode 100644 index 00000000..b6c36108 --- /dev/null +++ b/assets/guide_components_elements.md.BEJCY6Yx.lean.js @@ -0,0 +1 @@ +import{_ as a,c as n,j as t,a as s,o}from"./chunks/framework.P9qPzDnn.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.C1RkeOP0.js b/assets/guide_extend_tools.md.C1RkeOP0.js new file mode 100644 index 00000000..77d86a04 --- /dev/null +++ b/assets/guide_extend_tools.md.C1RkeOP0.js @@ -0,0 +1 @@ +import{_ as a,c as o,j as t,a as s,o as r}from"./chunks/framework.P9qPzDnn.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.C1RkeOP0.lean.js b/assets/guide_extend_tools.md.C1RkeOP0.lean.js new file mode 100644 index 00000000..77d86a04 --- /dev/null +++ b/assets/guide_extend_tools.md.C1RkeOP0.lean.js @@ -0,0 +1 @@ +import{_ as a,c as o,j as t,a as s,o as r}from"./chunks/framework.P9qPzDnn.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.CUmZPOqd.js b/assets/guide_index.md.CUmZPOqd.js new file mode 100644 index 00000000..452eeeb9 --- /dev/null +++ b/assets/guide_index.md.CUmZPOqd.js @@ -0,0 +1 @@ +import{_ as t,c as e,a0 as r,o as i}from"./chunks/framework.P9qPzDnn.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.CUmZPOqd.lean.js b/assets/guide_index.md.CUmZPOqd.lean.js new file mode 100644 index 00000000..452eeeb9 --- /dev/null +++ b/assets/guide_index.md.CUmZPOqd.lean.js @@ -0,0 +1 @@ +import{_ as t,c as e,a0 as r,o as i}from"./chunks/framework.P9qPzDnn.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.CfNIj0Wx.js b/assets/guide_modules_context.md.CfNIj0Wx.js new file mode 100644 index 00000000..35a1e2db --- /dev/null +++ b/assets/guide_modules_context.md.CfNIj0Wx.js @@ -0,0 +1,200 @@ +import{_ as i,c as a,a0 as h,o as n}from"./chunks/framework.P9qPzDnn.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 属性可获取当前上下文的根上下文实例,无论是子级上下文 ctxChild1ctxChild2,还是继承了 ctxChild1 的孙级上下文 ctxChild3,其根上下文实例均指向 ctx,而根上下午实例的 ctx.root 指向自身。

事件系统

以上内容均由最初的 Context 类定义,通过类原生的继承方式和 ctx.inject()ctx.mixin() 等方法对 Context 进行装饰或扩展。而在 Context 类内部,它本身就已为自己注册并注入了两个实例对象,其一便是事件系统,这也是在第二章中介绍事件系统时说道「事件订阅者模式与事件系统共同构成了 Kotori 的基础」的原由,只不过 Context 类本身并未使用事件系统功能,且仅直接定义了 readydispose 事件,两者被作为整个程序生命周期的重要一环,其余事件由另一实例对象(见下文)或 Kotori 核心类定义。

插件系统

其二便是插件系统,它定义了 ready_moduledispose_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.CfNIj0Wx.lean.js b/assets/guide_modules_context.md.CfNIj0Wx.lean.js new file mode 100644 index 00000000..35a1e2db --- /dev/null +++ b/assets/guide_modules_context.md.CfNIj0Wx.lean.js @@ -0,0 +1,200 @@ +import{_ as i,c as a,a0 as h,o as n}from"./chunks/framework.P9qPzDnn.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 属性可获取当前上下文的根上下文实例,无论是子级上下文 ctxChild1ctxChild2,还是继承了 ctxChild1 的孙级上下文 ctxChild3,其根上下文实例均指向 ctx,而根上下午实例的 ctx.root 指向自身。

事件系统

以上内容均由最初的 Context 类定义,通过类原生的继承方式和 ctx.inject()ctx.mixin() 等方法对 Context 进行装饰或扩展。而在 Context 类内部,它本身就已为自己注册并注入了两个实例对象,其一便是事件系统,这也是在第二章中介绍事件系统时说道「事件订阅者模式与事件系统共同构成了 Kotori 的基础」的原由,只不过 Context 类本身并未使用事件系统功能,且仅直接定义了 readydispose 事件,两者被作为整个程序生命周期的重要一环,其余事件由另一实例对象(见下文)或 Kotori 核心类定义。

插件系统

其二便是插件系统,它定义了 ready_moduledispose_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.DZm9Jfnd.js b/assets/guide_modules_decorator.md.DZm9Jfnd.js new file mode 100644 index 00000000..db0c03fe --- /dev/null +++ b/assets/guide_modules_decorator.md.DZm9Jfnd.js @@ -0,0 +1 @@ +import{_ as t,c as r,j as a,a as o,o as d}from"./chunks/framework.P9qPzDnn.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.DZm9Jfnd.lean.js b/assets/guide_modules_decorator.md.DZm9Jfnd.lean.js new file mode 100644 index 00000000..db0c03fe --- /dev/null +++ b/assets/guide_modules_decorator.md.DZm9Jfnd.lean.js @@ -0,0 +1 @@ +import{_ as t,c as r,j as a,a as o,o as d}from"./chunks/framework.P9qPzDnn.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.B6ZA5MSX.js b/assets/guide_modules_filter.md.B6ZA5MSX.js new file mode 100644 index 00000000..fadea90b --- /dev/null +++ b/assets/guide_modules_filter.md.B6ZA5MSX.js @@ -0,0 +1 @@ +import{_ as a,c as r,j as t,a as s,o}from"./chunks/framework.P9qPzDnn.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.B6ZA5MSX.lean.js b/assets/guide_modules_filter.md.B6ZA5MSX.lean.js new file mode 100644 index 00000000..fadea90b --- /dev/null +++ b/assets/guide_modules_filter.md.B6ZA5MSX.lean.js @@ -0,0 +1 @@ +import{_ as a,c as r,j as t,a as s,o}from"./chunks/framework.P9qPzDnn.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.DN93rR3l.js b/assets/guide_modules_i18n.md.DN93rR3l.js new file mode 100644 index 00000000..21e13fee --- /dev/null +++ b/assets/guide_modules_i18n.md.DN93rR3l.js @@ -0,0 +1 @@ +import{_ as t,c as r,j as a,a as s,o}from"./chunks/framework.P9qPzDnn.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.DN93rR3l.lean.js b/assets/guide_modules_i18n.md.DN93rR3l.lean.js new file mode 100644 index 00000000..21e13fee --- /dev/null +++ b/assets/guide_modules_i18n.md.DN93rR3l.lean.js @@ -0,0 +1 @@ +import{_ as t,c as r,j as a,a as s,o}from"./chunks/framework.P9qPzDnn.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.F2YmqKkM.js b/assets/guide_modules_plugin.md.F2YmqKkM.js new file mode 100644 index 00000000..27eae13a --- /dev/null +++ b/assets/guide_modules_plugin.md.F2YmqKkM.js @@ -0,0 +1,176 @@ +import{_ as i,c as a,a0 as n,o as h}from"./chunks/framework.P9qPzDnn.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>
+  ) {
+    /* ... */
+  }
+}

在导出类式中,可同时在外部导出诸如 configlanginject 属性,也可在类中设置相应的静态属性,一般地,请使用后者。如若两者同时存在,类中的属性将会覆盖外部导出的属性。

诚然,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 的识别顺序(一经识别成功将不再继续识别后续内容):

  1. 适配器类实现
  2. 默认导出类
  3. 默认导出函数
  4. main() 导出函数
  5. 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.F2YmqKkM.lean.js b/assets/guide_modules_plugin.md.F2YmqKkM.lean.js new file mode 100644 index 00000000..27eae13a --- /dev/null +++ b/assets/guide_modules_plugin.md.F2YmqKkM.lean.js @@ -0,0 +1,176 @@ +import{_ as i,c as a,a0 as n,o as h}from"./chunks/framework.P9qPzDnn.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>
+  ) {
+    /* ... */
+  }
+}

在导出类式中,可同时在外部导出诸如 configlanginject 属性,也可在类中设置相应的静态属性,一般地,请使用后者。如若两者同时存在,类中的属性将会覆盖外部导出的属性。

诚然,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 的识别顺序(一经识别成功将不再继续识别后续内容):

  1. 适配器类实现
  2. 默认导出类
  3. 默认导出函数
  4. main() 导出函数
  5. 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.Bl-r_LjA.js b/assets/guide_modules_rescript.md.Bl-r_LjA.js new file mode 100644 index 00000000..975bab73 --- /dev/null +++ b/assets/guide_modules_rescript.md.Bl-r_LjA.js @@ -0,0 +1,8 @@ +import{_ as i,c as a,a0 as t,o as n}from"./chunks/framework.P9qPzDnn.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)。

NOTE

详细信息与入门指南请参考 The ReScript Programming Language

基本使用

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.Bl-r_LjA.lean.js b/assets/guide_modules_rescript.md.Bl-r_LjA.lean.js new file mode 100644 index 00000000..975bab73 --- /dev/null +++ b/assets/guide_modules_rescript.md.Bl-r_LjA.lean.js @@ -0,0 +1,8 @@ +import{_ as i,c as a,a0 as t,o as n}from"./chunks/framework.P9qPzDnn.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)。

NOTE

详细信息与入门指南请参考 The ReScript Programming Language

基本使用

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.D-o7cJAG.js b/assets/guide_modules_schema.md.D-o7cJAG.js new file mode 100644 index 00000000..b0de5375 --- /dev/null +++ b/assets/guide_modules_schema.md.D-o7cJAG.js @@ -0,0 +1,49 @@ +import{_ as i,c as a,a0 as h,o as k}from"./chunks/framework.P9qPzDnn.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)
+}

该方法会返回一个对象,当 valuetrue 时,对象上存在 data 属性,其值即为处理后的结果,当 valuefalse 时,对象上存在 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.D-o7cJAG.lean.js b/assets/guide_modules_schema.md.D-o7cJAG.lean.js new file mode 100644 index 00000000..b0de5375 --- /dev/null +++ b/assets/guide_modules_schema.md.D-o7cJAG.lean.js @@ -0,0 +1,49 @@ +import{_ as i,c as a,a0 as h,o as k}from"./chunks/framework.P9qPzDnn.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)
+}

该方法会返回一个对象,当 valuetrue 时,对象上存在 data 属性,其值即为处理后的结果,当 valuefalse 时,对象上存在 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.DMQ0sFPf.js b/assets/guide_modules_service.md.DMQ0sFPf.js new file mode 100644 index 00000000..53bbda84 --- /dev/null +++ b/assets/guide_modules_service.md.DMQ0sFPf.js @@ -0,0 +1 @@ +import{_ as t,c as r,j as a,a as s,o}from"./chunks/framework.P9qPzDnn.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.DMQ0sFPf.lean.js b/assets/guide_modules_service.md.DMQ0sFPf.lean.js new file mode 100644 index 00000000..53bbda84 --- /dev/null +++ b/assets/guide_modules_service.md.DMQ0sFPf.lean.js @@ -0,0 +1 @@ +import{_ as t,c as r,j as a,a as s,o}from"./chunks/framework.P9qPzDnn.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.X_FtbxVb.js b/assets/guide_start_environment.md.X_FtbxVb.js new file mode 100644 index 00000000..2f0bff7f --- /dev/null +++ b/assets/guide_start_environment.md.X_FtbxVb.js @@ -0,0 +1 @@ +import{_ as e,c as a,a0 as r,o as i}from"./chunks/framework.P9qPzDnn.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 的功能,当然你也可以根据你的喜好选择,如:

  • WebStorm
',9)]))}const c=e(o,[["render",n]]);export{u as __pageData,c as default}; diff --git a/assets/guide_start_environment.md.X_FtbxVb.lean.js b/assets/guide_start_environment.md.X_FtbxVb.lean.js new file mode 100644 index 00000000..2f0bff7f --- /dev/null +++ b/assets/guide_start_environment.md.X_FtbxVb.lean.js @@ -0,0 +1 @@ +import{_ as e,c as a,a0 as r,o as i}from"./chunks/framework.P9qPzDnn.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 的功能,当然你也可以根据你的喜好选择,如:

  • WebStorm
',9)]))}const c=e(o,[["render",n]]);export{u as __pageData,c as default}; diff --git a/assets/guide_start_publish.md.s23QhAsX.js b/assets/guide_start_publish.md.s23QhAsX.js new file mode 100644 index 00000000..ad7f22bc --- /dev/null +++ b/assets/guide_start_publish.md.s23QhAsX.js @@ -0,0 +1,40 @@ +import{_ as i,c as a,a0 as n,o as e}from"./chunks/framework.P9qPzDnn.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 程序),在你的模块根目录中输入以下指令:

bash
pnpm build

一般地,你将会发现在模块根目录出现了一个 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 注册账号,然后根据提示在浏览器内登录:

bash
npm login

当一切就绪时:

bash
npm publish

当没有任何意外问题时,访问 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.s23QhAsX.lean.js b/assets/guide_start_publish.md.s23QhAsX.lean.js new file mode 100644 index 00000000..ad7f22bc --- /dev/null +++ b/assets/guide_start_publish.md.s23QhAsX.lean.js @@ -0,0 +1,40 @@ +import{_ as i,c as a,a0 as n,o as e}from"./chunks/framework.P9qPzDnn.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 程序),在你的模块根目录中输入以下指令:

bash
pnpm build

一般地,你将会发现在模块根目录出现了一个 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 注册账号,然后根据提示在浏览器内登录:

bash
npm login

当一切就绪时:

bash
npm publish

当没有任何意外问题时,访问 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.ZNwpORZ-.js b/assets/guide_start_setup.md.ZNwpORZ-.js new file mode 100644 index 00000000..a20e16f4 --- /dev/null +++ b/assets/guide_start_setup.md.ZNwpORZ-.js @@ -0,0 +1,144 @@ +import{_ as i,c as a,a0 as n,o as h}from"./chunks/framework.P9qPzDnn.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 工程文件夹,代码存放处
    • index.ts 整个模块的入口文件

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:

bash
pnpm dev

在浏览器中打开 http://localhost:2333 即可进入沙盒环境,输入 /echo Hello,Kotori! 以查看效果:

show

`,35)]))}const o=i(k,[["render",t]]);export{y as __pageData,o as default}; diff --git a/assets/guide_start_setup.md.ZNwpORZ-.lean.js b/assets/guide_start_setup.md.ZNwpORZ-.lean.js new file mode 100644 index 00000000..a20e16f4 --- /dev/null +++ b/assets/guide_start_setup.md.ZNwpORZ-.lean.js @@ -0,0 +1,144 @@ +import{_ as i,c as a,a0 as n,o as h}from"./chunks/framework.P9qPzDnn.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 工程文件夹,代码存放处
    • index.ts 整个模块的入口文件

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:

bash
pnpm dev

在浏览器中打开 http://localhost:2333 即可进入沙盒环境,输入 /echo Hello,Kotori! 以查看效果:

show

`,35)]))}const o=i(k,[["render",t]]);export{y as __pageData,o as default}; diff --git a/assets/index.md.P8--M6MF.js b/assets/index.md.P8--M6MF.js new file mode 100644 index 00000000..425ae8a0 --- /dev/null +++ b/assets/index.md.P8--M6MF.js @@ -0,0 +1 @@ +import{_ as t,c as e,o as i}from"./chunks/framework.P9qPzDnn.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.P8--M6MF.lean.js b/assets/index.md.P8--M6MF.lean.js new file mode 100644 index 00000000..425ae8a0 --- /dev/null +++ b/assets/index.md.P8--M6MF.lean.js @@ -0,0 +1 @@ +import{_ as t,c as e,o as i}from"./chunks/framework.P9qPzDnn.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.D1mO3gni.js b/assets/modules_index.md.D1mO3gni.js new file mode 100644 index 00000000..c79d82dd --- /dev/null +++ b/assets/modules_index.md.D1mO3gni.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.P9qPzDnn.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.D1mO3gni.lean.js b/assets/modules_index.md.D1mO3gni.lean.js new file mode 100644 index 00000000..c79d82dd --- /dev/null +++ b/assets/modules_index.md.D1mO3gni.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.P9qPzDnn.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.B8KJTn0Z.css b/assets/style.B8KJTn0Z.css new file mode 100644 index 00000000..9d741a07 --- /dev/null +++ b/assets/style.B8KJTn0Z.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-d220041e]{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-d220041e],.VPBackdrop.fade-leave-to[data-v-d220041e]{opacity:0}.VPBackdrop.fade-leave-active[data-v-d220041e]{transition-duration:.25s}@media (min-width: 1280px){.VPBackdrop[data-v-d220041e]{display:none}}.NotFound[data-v-314d6823]{padding:64px 24px 96px;text-align:center}@media (min-width: 768px){.NotFound[data-v-314d6823]{padding:96px 32px 168px}}.code[data-v-314d6823]{line-height:64px;font-size:64px;font-weight:600}.title[data-v-314d6823]{padding-top:12px;letter-spacing:2px;line-height:20px;font-size:20px;font-weight:700}.divider[data-v-314d6823]{margin:24px auto 18px;width:64px;height:1px;background-color:var(--vp-c-divider)}.quote[data-v-314d6823]{margin:0 auto;max-width:256px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.action[data-v-314d6823]{padding-top:20px}.link[data-v-314d6823]{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-314d6823]:hover{border-color:var(--vp-c-brand-2);color:var(--vp-c-brand-2)}.root[data-v-9e933ec1]{position:relative;z-index:1}.nested[data-v-9e933ec1]{padding-right:16px;padding-left:16px}.outline-link[data-v-9e933ec1]{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-9e933ec1]:hover,.outline-link.active[data-v-9e933ec1]{color:var(--vp-c-text-1);transition:color .25s}.outline-link.nested[data-v-9e933ec1]{padding-left:13px}.VPDocAsideOutline[data-v-d595de97]{display:none}.VPDocAsideOutline.has-outline[data-v-d595de97]{display:block}.content[data-v-d595de97]{position:relative;border-left:1px solid var(--vp-c-divider);padding-left:16px;font-size:13px;font-weight:500}.outline-marker[data-v-d595de97]{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-d595de97]{line-height:32px;font-size:14px;font-weight:600}.VPDocAside[data-v-3de71ef9]{display:flex;flex-direction:column;flex-grow:1}.spacer[data-v-3de71ef9]{flex-grow:1}.VPDocAside[data-v-3de71ef9] .spacer+.VPDocAsideSponsors,.VPDocAside[data-v-3de71ef9] .spacer+.VPDocAsideCarbonAds{margin-top:24px}.VPDocAside[data-v-3de71ef9] .VPDocAsideSponsors+.VPDocAsideCarbonAds{margin-top:16px}.VPLastUpdated[data-v-12f2da6a]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}@media (min-width: 640px){.VPLastUpdated[data-v-12f2da6a]{line-height:32px;font-size:14px;font-weight:500}}.VPDocFooter[data-v-e4c276ab]{margin-top:64px}.edit-info[data-v-e4c276ab]{padding-bottom:18px}@media (min-width: 640px){.edit-info[data-v-e4c276ab]{display:flex;justify-content:space-between;align-items:center;padding-bottom:14px}}.edit-link-button[data-v-e4c276ab]{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-e4c276ab]:hover{color:var(--vp-c-brand-2)}.edit-link-icon[data-v-e4c276ab]{margin-right:8px}.prev-next[data-v-e4c276ab]{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-e4c276ab]{grid-template-columns:repeat(2,1fr);grid-column-gap:16px}}.pager-link[data-v-e4c276ab]{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-e4c276ab]:hover{border-color:var(--vp-c-brand-1)}.pager-link.next[data-v-e4c276ab]{margin-left:auto;text-align:right}.desc[data-v-e4c276ab]{display:block;line-height:20px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.title[data-v-e4c276ab]{display:block;line-height:20px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1);transition:color .25s}.VPDoc[data-v-13b8f1e2]{padding:32px 24px 96px;width:100%}@media (min-width: 768px){.VPDoc[data-v-13b8f1e2]{padding:48px 32px 128px}}@media (min-width: 960px){.VPDoc[data-v-13b8f1e2]{padding:48px 32px 0}.VPDoc:not(.has-sidebar) .container[data-v-13b8f1e2]{display:flex;justify-content:center;max-width:992px}.VPDoc:not(.has-sidebar) .content[data-v-13b8f1e2]{max-width:752px}}@media (min-width: 1280px){.VPDoc .container[data-v-13b8f1e2]{display:flex;justify-content:center}.VPDoc .aside[data-v-13b8f1e2]{display:block}}@media (min-width: 1440px){.VPDoc:not(.has-sidebar) .content[data-v-13b8f1e2]{max-width:784px}.VPDoc:not(.has-sidebar) .container[data-v-13b8f1e2]{max-width:1104px}}.container[data-v-13b8f1e2]{margin:0 auto;width:100%}.aside[data-v-13b8f1e2]{position:relative;display:none;order:2;flex-grow:1;padding-left:32px;width:100%;max-width:256px}.left-aside[data-v-13b8f1e2]{order:1;padding-left:unset;padding-right:32px}.aside-container[data-v-13b8f1e2]{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-13b8f1e2]::-webkit-scrollbar{display:none}.aside-curtain[data-v-13b8f1e2]{position:fixed;bottom:0;z-index:10;width:224px;height:32px;background:linear-gradient(transparent,var(--vp-c-bg) 70%)}.aside-content[data-v-13b8f1e2]{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-13b8f1e2]{position:relative;margin:0 auto;width:100%}@media (min-width: 960px){.content[data-v-13b8f1e2]{padding:0 32px 128px}}@media (min-width: 1280px){.content[data-v-13b8f1e2]{order:1;margin:0;min-width:640px}}.content-container[data-v-13b8f1e2]{margin:0 auto}.VPDoc.has-aside .content-container[data-v-13b8f1e2]{max-width:688px}.VPButton[data-v-df2cf507]{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-df2cf507]:active{transition:color .1s,border-color .1s,background-color .1s}.VPButton.medium[data-v-df2cf507]{border-radius:20px;padding:0 20px;line-height:38px;font-size:14px}.VPButton.big[data-v-df2cf507]{border-radius:24px;padding:0 24px;line-height:46px;font-size:16px}.VPButton.brand[data-v-df2cf507]{border-color:var(--vp-button-brand-border);color:var(--vp-button-brand-text);background-color:var(--vp-button-brand-bg)}.VPButton.brand[data-v-df2cf507]: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-df2cf507]: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-df2cf507]{border-color:var(--vp-button-alt-border);color:var(--vp-button-alt-text);background-color:var(--vp-button-alt-bg)}.VPButton.alt[data-v-df2cf507]: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-df2cf507]: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-df2cf507]{border-color:var(--vp-button-sponsor-border);color:var(--vp-button-sponsor-text);background-color:var(--vp-button-sponsor-bg)}.VPButton.sponsor[data-v-df2cf507]: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-df2cf507]: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-964b1c62]{display:none}.dark .VPImage.light[data-v-964b1c62]{display:none}.VPHero[data-v-8d45dfd5]{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-8d45dfd5]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px}}@media (min-width: 960px){.VPHero[data-v-8d45dfd5]{padding:calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px}}.container[data-v-8d45dfd5]{display:flex;flex-direction:column;margin:0 auto;max-width:1152px}@media (min-width: 960px){.container[data-v-8d45dfd5]{flex-direction:row}}.main[data-v-8d45dfd5]{position:relative;z-index:10;order:2;flex-grow:1;flex-shrink:0}.VPHero.has-image .container[data-v-8d45dfd5]{text-align:center}@media (min-width: 960px){.VPHero.has-image .container[data-v-8d45dfd5]{text-align:left}}@media (min-width: 960px){.main[data-v-8d45dfd5]{order:1;width:calc((100% / 3) * 2)}.VPHero.has-image .main[data-v-8d45dfd5]{max-width:592px}}.name[data-v-8d45dfd5],.text[data-v-8d45dfd5]{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-8d45dfd5],.VPHero.has-image .text[data-v-8d45dfd5]{margin:0 auto}.name[data-v-8d45dfd5]{color:var(--vp-home-hero-name-color)}.clip[data-v-8d45dfd5]{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-8d45dfd5],.text[data-v-8d45dfd5]{max-width:576px;line-height:56px;font-size:48px}}@media (min-width: 960px){.name[data-v-8d45dfd5],.text[data-v-8d45dfd5]{line-height:64px;font-size:56px}.VPHero.has-image .name[data-v-8d45dfd5],.VPHero.has-image .text[data-v-8d45dfd5]{margin:0}}.tagline[data-v-8d45dfd5]{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-8d45dfd5]{margin:0 auto}@media (min-width: 640px){.tagline[data-v-8d45dfd5]{padding-top:12px;max-width:576px;line-height:32px;font-size:20px}}@media (min-width: 960px){.tagline[data-v-8d45dfd5]{line-height:36px;font-size:24px}.VPHero.has-image .tagline[data-v-8d45dfd5]{margin:0}}.actions[data-v-8d45dfd5]{display:flex;flex-wrap:wrap;margin:-6px;padding-top:24px}.VPHero.has-image .actions[data-v-8d45dfd5]{justify-content:center}@media (min-width: 640px){.actions[data-v-8d45dfd5]{padding-top:32px}}@media (min-width: 960px){.VPHero.has-image .actions[data-v-8d45dfd5]{justify-content:flex-start}}.action[data-v-8d45dfd5]{flex-shrink:0;padding:6px}.image[data-v-8d45dfd5]{order:1;margin:-76px -24px -48px}@media (min-width: 640px){.image[data-v-8d45dfd5]{margin:-108px -24px -48px}}@media (min-width: 960px){.image[data-v-8d45dfd5]{flex-grow:1;order:2;margin:0;min-height:100%}}.image-container[data-v-8d45dfd5]{position:relative;margin:0 auto;width:320px;height:320px}@media (min-width: 640px){.image-container[data-v-8d45dfd5]{width:392px;height:392px}}@media (min-width: 960px){.image-container[data-v-8d45dfd5]{display:flex;justify-content:center;align-items:center;width:100%;height:100%;transform:translate(-32px,-32px)}}.image-bg[data-v-8d45dfd5]{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-8d45dfd5]{width:256px;height:256px}}@media (min-width: 960px){.image-bg[data-v-8d45dfd5]{width:320px;height:320px}}[data-v-8d45dfd5] .image-src{position:absolute;top:50%;left:50%;max-width:192px;max-height:192px;transform:translate(-50%,-50%)}@media (min-width: 640px){[data-v-8d45dfd5] .image-src{max-width:256px;max-height:256px}}@media (min-width: 960px){[data-v-8d45dfd5] .image-src{max-width:320px;max-height:320px}}.VPFeature[data-v-810fe4d1]{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-810fe4d1]:hover{border-color:var(--vp-c-brand-1)}.box[data-v-810fe4d1]{display:flex;flex-direction:column;padding:24px;height:100%}.box[data-v-810fe4d1]>.VPImage{margin-bottom:20px}.icon[data-v-810fe4d1]{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-810fe4d1]{line-height:24px;font-size:16px;font-weight:600}.details[data-v-810fe4d1]{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-810fe4d1]{padding-top:8px}.link-text-value[data-v-810fe4d1]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.link-text-icon[data-v-810fe4d1]{margin-left:6px}.VPFeatures[data-v-1d713b99]{position:relative;padding:0 24px}@media (min-width: 640px){.VPFeatures[data-v-1d713b99]{padding:0 48px}}@media (min-width: 960px){.VPFeatures[data-v-1d713b99]{padding:0 64px}}.container[data-v-1d713b99]{margin:0 auto;max-width:1152px}.items[data-v-1d713b99]{display:flex;flex-wrap:wrap;margin:-8px}.item[data-v-1d713b99]{padding:8px;width:100%}@media (min-width: 640px){.item.grid-2[data-v-1d713b99],.item.grid-4[data-v-1d713b99],.item.grid-6[data-v-1d713b99]{width:50%}}@media (min-width: 768px){.item.grid-2[data-v-1d713b99],.item.grid-4[data-v-1d713b99]{width:50%}.item.grid-3[data-v-1d713b99],.item.grid-6[data-v-1d713b99]{width:calc(100% / 3)}}@media (min-width: 960px){.item.grid-4[data-v-1d713b99]{width:25%}}.container[data-v-c6e93e4e]{margin:auto;width:100%;max-width:1280px;padding:0 24px}@media (min-width: 640px){.container[data-v-c6e93e4e]{padding:0 48px}}@media (min-width: 960px){.container[data-v-c6e93e4e]{width:100%;padding:0 64px}}.vp-doc[data-v-c6e93e4e] .VPHomeSponsors,.vp-doc[data-v-c6e93e4e] .VPTeamPage{margin-left:var(--vp-offset, calc(50% - 50vw) );margin-right:var(--vp-offset, calc(50% - 50vw) )}.vp-doc[data-v-c6e93e4e] .VPHomeSponsors h2{border-top:none;letter-spacing:normal}.vp-doc[data-v-c6e93e4e] .VPHomeSponsors a,.vp-doc[data-v-c6e93e4e] .VPTeamPage a{text-decoration:none}.VPHome[data-v-f253ebe2]{margin-bottom:96px}@media (min-width: 768px){.VPHome[data-v-f253ebe2]{margin-bottom:128px}}.VPContent[data-v-7eb770b7]{flex-grow:1;flex-shrink:0;margin:var(--vp-layout-top-height, 0px) auto 0;width:100%}.VPContent.is-home[data-v-7eb770b7]{width:100%;max-width:100%}.VPContent.has-sidebar[data-v-7eb770b7]{margin:0}@media (min-width: 960px){.VPContent[data-v-7eb770b7]{padding-top:var(--vp-nav-height)}.VPContent.has-sidebar[data-v-7eb770b7]{margin:var(--vp-layout-top-height, 0px) 0 0;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPContent.has-sidebar[data-v-7eb770b7]{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-8c586fe8]{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-8c586fe8]{display:none}.VPFooter[data-v-8c586fe8] a{text-decoration-line:underline;text-underline-offset:2px;transition:color .25s}.VPFooter[data-v-8c586fe8] a:hover{color:var(--vp-c-text-1)}@media (min-width: 768px){.VPFooter[data-v-8c586fe8]{padding:32px}}.container[data-v-8c586fe8]{margin:0 auto;max-width:var(--vp-layout-max-width);text-align:center}.message[data-v-8c586fe8],.copyright[data-v-8c586fe8]{line-height:24px;font-size:14px;font-weight:500;color:var(--vp-c-text-2)}.VPLocalNavOutlineDropdown[data-v-5bcce627]{padding:12px 20px 11px}@media (min-width: 960px){.VPLocalNavOutlineDropdown[data-v-5bcce627]{padding:12px 36px 11px}}.VPLocalNavOutlineDropdown button[data-v-5bcce627]{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-5bcce627]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPLocalNavOutlineDropdown button.open[data-v-5bcce627]{color:var(--vp-c-text-1)}.icon[data-v-5bcce627]{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-5bcce627]{font-size:14px}.icon[data-v-5bcce627]{font-size:16px}}.open>.icon[data-v-5bcce627]{transform:rotate(90deg)}.items[data-v-5bcce627]{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-5bcce627]{right:auto;left:calc(var(--vp-sidebar-width) + 32px);width:320px}}.header[data-v-5bcce627]{background-color:var(--vp-c-bg-soft)}.top-link[data-v-5bcce627]{display:block;padding:0 16px;line-height:48px;font-size:14px;font-weight:500;color:var(--vp-c-brand-1)}.outline[data-v-5bcce627]{padding:8px 0;background-color:var(--vp-c-bg-soft)}.flyout-enter-active[data-v-5bcce627]{transition:all .2s ease-out}.flyout-leave-active[data-v-5bcce627]{transition:all .15s ease-in}.flyout-enter-from[data-v-5bcce627],.flyout-leave-to[data-v-5bcce627]{opacity:0;transform:translateY(-16px)}.VPLocalNav[data-v-d38f1bb7]{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-d38f1bb7]{position:fixed}@media (min-width: 960px){.VPLocalNav[data-v-d38f1bb7]{top:var(--vp-nav-height)}.VPLocalNav.has-sidebar[data-v-d38f1bb7]{padding-left:var(--vp-sidebar-width)}.VPLocalNav.empty[data-v-d38f1bb7]{display:none}}@media (min-width: 1280px){.VPLocalNav[data-v-d38f1bb7]{display:none}}@media (min-width: 1440px){.VPLocalNav.has-sidebar[data-v-d38f1bb7]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.container[data-v-d38f1bb7]{display:flex;justify-content:space-between;align-items:center}.menu[data-v-d38f1bb7]{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-d38f1bb7]:hover{color:var(--vp-c-text-1);transition:color .25s}@media (min-width: 768px){.menu[data-v-d38f1bb7]{padding:0 32px}}@media (min-width: 960px){.menu[data-v-d38f1bb7]{display:none}}.menu-icon[data-v-d38f1bb7]{margin-right:8px;font-size:14px}.VPOutlineDropdown[data-v-d38f1bb7]{padding:12px 24px 11px}@media (min-width: 768px){.VPOutlineDropdown[data-v-d38f1bb7]{padding:12px 32px 11px}}.VPSwitch[data-v-e23c1592]{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-e23c1592]:hover{border-color:var(--vp-c-brand-1)}.check[data-v-e23c1592]{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-e23c1592]{position:relative;display:block;width:18px;height:18px;border-radius:50%;overflow:hidden}.icon[data-v-e23c1592] [class^=vpi-]{position:absolute;top:3px;left:3px;width:12px;height:12px;color:var(--vp-c-text-2)}.dark .icon[data-v-e23c1592] [class^=vpi-]{color:var(--vp-c-text-1);transition:opacity .25s!important}.sun[data-v-327cf911]{opacity:1}.moon[data-v-327cf911],.dark .sun[data-v-327cf911]{opacity:0}.dark .moon[data-v-327cf911]{opacity:1}.dark .VPSwitchAppearance[data-v-327cf911] .check{transform:translate(18px)}.VPNavBarAppearance[data-v-c319a4c2]{display:none}@media (min-width: 1280px){.VPNavBarAppearance[data-v-c319a4c2]{display:flex;align-items:center}}.VPMenuGroup+.VPMenuLink[data-v-6d9cb10e]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.link[data-v-6d9cb10e]{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-6d9cb10e]:hover{color:var(--vp-c-brand-1);background-color:var(--vp-c-default-soft)}.link.active[data-v-6d9cb10e]{color:var(--vp-c-brand-1)}.VPMenuGroup[data-v-c73fface]{margin:12px -12px 0;border-top:1px solid var(--vp-c-divider);padding:12px 12px 0}.VPMenuGroup[data-v-c73fface]:first-child{margin-top:0;border-top:0;padding-top:0}.VPMenuGroup+.VPMenuGroup[data-v-c73fface]{margin-top:12px;border-top:1px solid var(--vp-c-divider)}.title[data-v-c73fface]{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-4abdfca2]{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-4abdfca2] .group{margin:0 -12px;padding:0 12px 12px}.VPMenu[data-v-4abdfca2] .group+.group{border-top:1px solid var(--vp-c-divider);padding:11px 12px 12px}.VPMenu[data-v-4abdfca2] .group:last-child{padding-bottom:0}.VPMenu[data-v-4abdfca2] .group+.item{border-top:1px solid var(--vp-c-divider);padding:11px 16px 0}.VPMenu[data-v-4abdfca2] .item{padding:0 16px;white-space:nowrap}.VPMenu[data-v-4abdfca2] .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-4abdfca2] .action{padding-left:24px}.VPFlyout[data-v-405682cb]{position:relative}.VPFlyout[data-v-405682cb]:hover{color:var(--vp-c-brand-1);transition:color .25s}.VPFlyout:hover .text[data-v-405682cb]{color:var(--vp-c-text-2)}.VPFlyout:hover .icon[data-v-405682cb]{fill:var(--vp-c-text-2)}.VPFlyout.active .text[data-v-405682cb]{color:var(--vp-c-brand-1)}.VPFlyout.active:hover .text[data-v-405682cb]{color:var(--vp-c-brand-2)}.button[aria-expanded=false]+.menu[data-v-405682cb]{opacity:0;visibility:hidden;transform:translateY(0)}.VPFlyout:hover .menu[data-v-405682cb],.button[aria-expanded=true]+.menu[data-v-405682cb]{opacity:1;visibility:visible;transform:translateY(0)}.button[data-v-405682cb]{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-405682cb]{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-405682cb]{margin-right:0;font-size:16px}.text-icon[data-v-405682cb]{margin-left:4px;font-size:14px}.icon[data-v-405682cb]{font-size:20px;transition:fill .25s}.menu[data-v-405682cb]{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-3edd214b]{display:flex;justify-content:center;align-items:center;width:36px;height:36px;color:var(--vp-c-text-2);transition:color .5s}.VPSocialLink[data-v-3edd214b]:hover{color:var(--vp-c-text-1);transition:color .25s}.VPSocialLink[data-v-3edd214b]>svg,.VPSocialLink[data-v-3edd214b]>[class^=vpi-social-]{width:20px;height:20px;fill:currentColor}.VPSocialLinks[data-v-5e24eb64]{display:flex;justify-content:center}.VPNavBarExtra[data-v-432caf41]{display:none;margin-right:-12px}@media (min-width: 768px){.VPNavBarExtra[data-v-432caf41]{display:block}}@media (min-width: 1280px){.VPNavBarExtra[data-v-432caf41]{display:none}}.trans-title[data-v-432caf41]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.item.appearance[data-v-432caf41],.item.social-links[data-v-432caf41]{display:flex;align-items:center;padding:0 12px}.item.appearance[data-v-432caf41]{min-width:176px}.appearance-action[data-v-432caf41]{margin-right:-2px}.social-links-list[data-v-432caf41]{margin:-4px -8px}.VPNavBarHamburger[data-v-a20145d7]{display:flex;justify-content:center;align-items:center;width:48px;height:var(--vp-nav-height)}@media (min-width: 768px){.VPNavBarHamburger[data-v-a20145d7]{display:none}}.container[data-v-a20145d7]{position:relative;width:16px;height:14px;overflow:hidden}.VPNavBarHamburger:hover .top[data-v-a20145d7]{top:0;left:0;transform:translate(4px)}.VPNavBarHamburger:hover .middle[data-v-a20145d7]{top:6px;left:0;transform:translate(0)}.VPNavBarHamburger:hover .bottom[data-v-a20145d7]{top:12px;left:0;transform:translate(8px)}.VPNavBarHamburger.active .top[data-v-a20145d7]{top:6px;transform:translate(0) rotate(225deg)}.VPNavBarHamburger.active .middle[data-v-a20145d7]{top:6px;transform:translate(16px)}.VPNavBarHamburger.active .bottom[data-v-a20145d7]{top:6px;transform:translate(0) rotate(135deg)}.VPNavBarHamburger.active:hover .top[data-v-a20145d7],.VPNavBarHamburger.active:hover .middle[data-v-a20145d7],.VPNavBarHamburger.active:hover .bottom[data-v-a20145d7]{background-color:var(--vp-c-text-2);transition:top .25s,background-color .25s,transform .25s}.top[data-v-a20145d7],.middle[data-v-a20145d7],.bottom[data-v-a20145d7]{position:absolute;width:16px;height:2px;background-color:var(--vp-c-text-1);transition:top .25s,background-color .5s,transform .25s}.top[data-v-a20145d7]{top:0;left:0;transform:translate(0)}.middle[data-v-a20145d7]{top:6px;left:0;transform:translate(8px)}.bottom[data-v-a20145d7]{top:12px;left:0;transform:translate(4px)}.VPNavBarMenuLink[data-v-22ff4594]{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-22ff4594],.VPNavBarMenuLink[data-v-22ff4594]:hover{color:var(--vp-c-brand-1)}.VPNavBarMenu[data-v-6e009845]{display:none}@media (min-width: 768px){.VPNavBarMenu[data-v-6e009845]{display:flex}}/*! @docsearch/css 3.8.2 | 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-8f8d6fed]{display:none}@media (min-width: 1280px){.VPNavBarSocialLinks[data-v-8f8d6fed]{display:flex;align-items:center}}.title[data-v-34d5de26]{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-34d5de26]{flex-shrink:0}.VPNavBarTitle.has-sidebar .title[data-v-34d5de26]{border-bottom-color:var(--vp-c-divider)}}[data-v-34d5de26] .logo{margin-right:8px;height:var(--vp-nav-logo-height)}.VPNavBarTranslations[data-v-9bfc6a91]{display:none}@media (min-width: 1280px){.VPNavBarTranslations[data-v-9bfc6a91]{display:flex;align-items:center}}.title[data-v-9bfc6a91]{padding:0 24px 0 12px;line-height:32px;font-size:14px;font-weight:700;color:var(--vp-c-text-1)}.VPNavBar[data-v-a01e6a85]{position:relative;height:var(--vp-nav-height);pointer-events:none;white-space:nowrap;transition:background-color .25s}.VPNavBar.screen-open[data-v-a01e6a85]{transition:none;background-color:var(--vp-nav-bg-color);border-bottom:1px solid var(--vp-c-divider)}.VPNavBar[data-v-a01e6a85]:not(.home){background-color:var(--vp-nav-bg-color)}@media (min-width: 960px){.VPNavBar[data-v-a01e6a85]:not(.home){background-color:transparent}.VPNavBar[data-v-a01e6a85]:not(.has-sidebar):not(.home.top){background-color:var(--vp-nav-bg-color)}}.wrapper[data-v-a01e6a85]{padding:0 8px 0 24px}@media (min-width: 768px){.wrapper[data-v-a01e6a85]{padding:0 32px}}@media (min-width: 960px){.VPNavBar.has-sidebar .wrapper[data-v-a01e6a85]{padding:0}}.container[data-v-a01e6a85]{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-a01e6a85],.container>.content[data-v-a01e6a85]{pointer-events:none}.container[data-v-a01e6a85] *{pointer-events:auto}@media (min-width: 960px){.VPNavBar.has-sidebar .container[data-v-a01e6a85]{max-width:100%}}.title[data-v-a01e6a85]{flex-shrink:0;height:calc(var(--vp-nav-height) - 1px);transition:background-color .5s}@media (min-width: 960px){.VPNavBar.has-sidebar .title[data-v-a01e6a85]{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-a01e6a85]{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-a01e6a85]{flex-grow:1}@media (min-width: 960px){.VPNavBar.has-sidebar .content[data-v-a01e6a85]{position:relative;z-index:1;padding-right:32px;padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .content[data-v-a01e6a85]{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-a01e6a85]{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-a01e6a85]{position:relative;background-color:var(--vp-nav-bg-color)}.VPNavBar:not(.has-sidebar):not(.home.top) .content-body[data-v-a01e6a85]{background-color:transparent}}@media (max-width: 767px){.content-body[data-v-a01e6a85]{column-gap:.5rem}}.menu+.translations[data-v-a01e6a85]:before,.menu+.appearance[data-v-a01e6a85]:before,.menu+.social-links[data-v-a01e6a85]:before,.translations+.appearance[data-v-a01e6a85]:before,.appearance+.social-links[data-v-a01e6a85]:before{margin-right:8px;margin-left:8px;width:1px;height:24px;background-color:var(--vp-c-divider);content:""}.menu+.appearance[data-v-a01e6a85]:before,.translations+.appearance[data-v-a01e6a85]:before{margin-right:16px}.appearance+.social-links[data-v-a01e6a85]:before{margin-left:16px}.social-links[data-v-a01e6a85]{margin-right:-8px}.divider[data-v-a01e6a85]{width:100%;height:1px}@media (min-width: 960px){.VPNavBar.has-sidebar .divider[data-v-a01e6a85]{padding-left:var(--vp-sidebar-width)}}@media (min-width: 1440px){.VPNavBar.has-sidebar .divider[data-v-a01e6a85]{padding-left:calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width))}}.divider-line[data-v-a01e6a85]{width:100%;height:1px;transition:background-color .5s}.VPNavBar:not(.home) .divider-line[data-v-a01e6a85]{background-color:var(--vp-c-gutter)}@media (min-width: 960px){.VPNavBar:not(.home.top) .divider-line[data-v-a01e6a85]{background-color:var(--vp-c-gutter)}.VPNavBar:not(.has-sidebar):not(.home.top) .divider[data-v-a01e6a85]{background-color:var(--vp-c-gutter)}}.VPNavScreenAppearance[data-v-d75f9d29]{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-d75f9d29]{line-height:24px;font-size:12px;font-weight:500;color:var(--vp-c-text-2)}.VPNavScreenMenuLink[data-v-a0e8f5e2]{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-a0e8f5e2]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupLink[data-v-774c2599]{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-774c2599]:hover{color:var(--vp-c-brand-1)}.VPNavScreenMenuGroupSection[data-v-104421fc]{display:block}.title[data-v-104421fc]{line-height:32px;font-size:13px;font-weight:700;color:var(--vp-c-text-2);transition:color .25s}.VPNavScreenMenuGroup[data-v-dc123f70]{border-bottom:1px solid var(--vp-c-divider);height:48px;overflow:hidden;transition:border-color .5s}.VPNavScreenMenuGroup .items[data-v-dc123f70]{visibility:hidden}.VPNavScreenMenuGroup.open .items[data-v-dc123f70]{visibility:visible}.VPNavScreenMenuGroup.open[data-v-dc123f70]{padding-bottom:10px;height:auto}.VPNavScreenMenuGroup.open .button[data-v-dc123f70]{padding-bottom:6px;color:var(--vp-c-brand-1)}.VPNavScreenMenuGroup.open .button-icon[data-v-dc123f70]{transform:rotate(45deg)}.button[data-v-dc123f70]{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-dc123f70]:hover{color:var(--vp-c-brand-1)}.button-icon[data-v-dc123f70]{transition:transform .25s}.group[data-v-dc123f70]:first-child{padding-top:0}.group+.group[data-v-dc123f70],.group+.item[data-v-dc123f70]{padding-top:4px}.VPNavScreenTranslations[data-v-82c9b97d]{height:24px;overflow:hidden}.VPNavScreenTranslations.open[data-v-82c9b97d]{height:auto}.title[data-v-82c9b97d]{display:flex;align-items:center;font-size:14px;font-weight:500;color:var(--vp-c-text-1)}.icon[data-v-82c9b97d]{font-size:16px}.icon.lang[data-v-82c9b97d]{margin-right:8px}.icon.chevron[data-v-82c9b97d]{margin-left:4px}.list[data-v-82c9b97d]{padding:4px 0 0 24px}.link[data-v-82c9b97d]{line-height:32px;font-size:13px;color:var(--vp-c-text-1)}.VPNavScreen[data-v-eed74814]{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-eed74814],.VPNavScreen.fade-leave-active[data-v-eed74814]{transition:opacity .25s}.VPNavScreen.fade-enter-active .container[data-v-eed74814],.VPNavScreen.fade-leave-active .container[data-v-eed74814]{transition:transform .25s ease}.VPNavScreen.fade-enter-from[data-v-eed74814],.VPNavScreen.fade-leave-to[data-v-eed74814]{opacity:0}.VPNavScreen.fade-enter-from .container[data-v-eed74814],.VPNavScreen.fade-leave-to .container[data-v-eed74814]{transform:translateY(-8px)}@media (min-width: 768px){.VPNavScreen[data-v-eed74814]{display:none}}.container[data-v-eed74814]{margin:0 auto;padding:24px 0 96px;max-width:288px}.menu+.translations[data-v-eed74814],.menu+.appearance[data-v-eed74814],.translations+.appearance[data-v-eed74814]{margin-top:24px}.menu+.social-links[data-v-eed74814]{margin-top:16px}.appearance+.social-links[data-v-eed74814]{margin-top:16px}.VPNav[data-v-1d136424]{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-1d136424]{position:fixed}}.VPSidebarItem.level-0[data-v-dad44e86]{padding-bottom:24px}.VPSidebarItem.collapsed.level-0[data-v-dad44e86]{padding-bottom:10px}.item[data-v-dad44e86]{position:relative;display:flex;width:100%}.VPSidebarItem.collapsible>.item[data-v-dad44e86]{cursor:pointer}.indicator[data-v-dad44e86]{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-dad44e86],.VPSidebarItem.level-3.is-active>.item>.indicator[data-v-dad44e86],.VPSidebarItem.level-4.is-active>.item>.indicator[data-v-dad44e86],.VPSidebarItem.level-5.is-active>.item>.indicator[data-v-dad44e86]{background-color:var(--vp-c-brand-1)}.link[data-v-dad44e86]{display:flex;align-items:center;flex-grow:1}.text[data-v-dad44e86]{flex-grow:1;padding:4px 0;line-height:24px;font-size:14px;transition:color .25s}.VPSidebarItem.level-0 .text[data-v-dad44e86]{font-weight:700;color:var(--vp-c-text-1)}.VPSidebarItem.level-1 .text[data-v-dad44e86],.VPSidebarItem.level-2 .text[data-v-dad44e86],.VPSidebarItem.level-3 .text[data-v-dad44e86],.VPSidebarItem.level-4 .text[data-v-dad44e86],.VPSidebarItem.level-5 .text[data-v-dad44e86]{font-weight:500;color:var(--vp-c-text-2)}.VPSidebarItem.level-0.is-link>.item>.link:hover .text[data-v-dad44e86],.VPSidebarItem.level-1.is-link>.item>.link:hover .text[data-v-dad44e86],.VPSidebarItem.level-2.is-link>.item>.link:hover .text[data-v-dad44e86],.VPSidebarItem.level-3.is-link>.item>.link:hover .text[data-v-dad44e86],.VPSidebarItem.level-4.is-link>.item>.link:hover .text[data-v-dad44e86],.VPSidebarItem.level-5.is-link>.item>.link:hover .text[data-v-dad44e86]{color:var(--vp-c-brand-1)}.VPSidebarItem.level-0.has-active>.item>.text[data-v-dad44e86],.VPSidebarItem.level-1.has-active>.item>.text[data-v-dad44e86],.VPSidebarItem.level-2.has-active>.item>.text[data-v-dad44e86],.VPSidebarItem.level-3.has-active>.item>.text[data-v-dad44e86],.VPSidebarItem.level-4.has-active>.item>.text[data-v-dad44e86],.VPSidebarItem.level-5.has-active>.item>.text[data-v-dad44e86],.VPSidebarItem.level-0.has-active>.item>.link>.text[data-v-dad44e86],.VPSidebarItem.level-1.has-active>.item>.link>.text[data-v-dad44e86],.VPSidebarItem.level-2.has-active>.item>.link>.text[data-v-dad44e86],.VPSidebarItem.level-3.has-active>.item>.link>.text[data-v-dad44e86],.VPSidebarItem.level-4.has-active>.item>.link>.text[data-v-dad44e86],.VPSidebarItem.level-5.has-active>.item>.link>.text[data-v-dad44e86]{color:var(--vp-c-text-1)}.VPSidebarItem.level-0.is-active>.item .link>.text[data-v-dad44e86],.VPSidebarItem.level-1.is-active>.item .link>.text[data-v-dad44e86],.VPSidebarItem.level-2.is-active>.item .link>.text[data-v-dad44e86],.VPSidebarItem.level-3.is-active>.item .link>.text[data-v-dad44e86],.VPSidebarItem.level-4.is-active>.item .link>.text[data-v-dad44e86],.VPSidebarItem.level-5.is-active>.item .link>.text[data-v-dad44e86]{color:var(--vp-c-brand-1)}.caret[data-v-dad44e86]{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-dad44e86]{color:var(--vp-c-text-2)}.item:hover .caret[data-v-dad44e86]:hover{color:var(--vp-c-text-1)}.caret-icon[data-v-dad44e86]{font-size:18px;transform:rotate(90deg);transition:transform .25s}.VPSidebarItem.collapsed .caret-icon[data-v-dad44e86]{transform:rotate(0)}.VPSidebarItem.level-1 .items[data-v-dad44e86],.VPSidebarItem.level-2 .items[data-v-dad44e86],.VPSidebarItem.level-3 .items[data-v-dad44e86],.VPSidebarItem.level-4 .items[data-v-dad44e86],.VPSidebarItem.level-5 .items[data-v-dad44e86]{border-left:1px solid var(--vp-c-divider);padding-left:16px}.VPSidebarItem.collapsed .items[data-v-dad44e86]{display:none}.no-transition[data-v-994dc249] .caret-icon{transition:none}.group+.group[data-v-994dc249]{border-top:1px solid var(--vp-c-divider);padding-top:10px}@media (min-width: 960px){.group[data-v-994dc249]{padding-top:10px;width:calc(var(--vp-sidebar-width) - 64px)}}.VPSidebar[data-v-3746e67f]{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-3746e67f]{opacity:1;visibility:visible;transform:translate(0);transition:opacity .25s,transform .5s cubic-bezier(.19,1,.22,1)}.dark .VPSidebar[data-v-3746e67f]{box-shadow:var(--vp-shadow-1)}@media (min-width: 960px){.VPSidebar[data-v-3746e67f]{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-3746e67f]{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-3746e67f]{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-3746e67f]{outline:0}.VPSkipLink[data-v-474ecce8]{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-474ecce8]:focus{height:auto;width:auto;clip:auto;clip-path:none}@media (min-width: 1280px){.VPSkipLink[data-v-474ecce8]{top:14px;left:16px}}.Layout[data-v-793c26c5]{display:flex;flex-direction:column;min-height:100vh}.VPHomeSponsors[data-v-8ddd972b]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPHomeSponsors[data-v-8ddd972b]{margin:96px 0}@media (min-width: 768px){.VPHomeSponsors[data-v-8ddd972b]{margin:128px 0}}.VPHomeSponsors[data-v-8ddd972b]{padding:0 24px}@media (min-width: 768px){.VPHomeSponsors[data-v-8ddd972b]{padding:0 48px}}@media (min-width: 960px){.VPHomeSponsors[data-v-8ddd972b]{padding:0 64px}}.container[data-v-8ddd972b]{margin:0 auto;max-width:1152px}.love[data-v-8ddd972b]{margin:0 auto;width:fit-content;font-size:28px;color:var(--vp-c-text-3)}.icon[data-v-8ddd972b]{display:inline-block}.message[data-v-8ddd972b]{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-8ddd972b]{padding-top:32px}.action[data-v-8ddd972b]{padding-top:40px;text-align:center}.VPTeamPage[data-v-c8ed2757]{margin:96px 0}@media (min-width: 768px){.VPTeamPage[data-v-c8ed2757]{margin:128px 0}}.VPHome .VPTeamPageTitle[data-v-c8ed2757-s]{border-top:1px solid var(--vp-c-gutter);padding-top:88px!important}.VPTeamPageSection+.VPTeamPageSection[data-v-c8ed2757-s],.VPTeamMembers+.VPTeamPageSection[data-v-c8ed2757-s]{margin-top:64px}.VPTeamMembers+.VPTeamMembers[data-v-c8ed2757-s]{margin-top:24px}@media (min-width: 768px){.VPTeamPageTitle+.VPTeamPageSection[data-v-c8ed2757-s]{margin-top:16px}.VPTeamPageSection+.VPTeamPageSection[data-v-c8ed2757-s],.VPTeamMembers+.VPTeamPageSection[data-v-c8ed2757-s]{margin-top:96px}}.VPTeamMembers[data-v-c8ed2757-s]{padding:0 24px}@media (min-width: 768px){.VPTeamMembers[data-v-c8ed2757-s]{padding:0 48px}}@media (min-width: 960px){.VPTeamMembers[data-v-c8ed2757-s]{padding:0 64px}}.VPTeamPageTitle[data-v-3d976094]{padding:48px 32px;text-align:center}@media (min-width: 768px){.VPTeamPageTitle[data-v-3d976094]{padding:64px 48px 48px}}@media (min-width: 960px){.VPTeamPageTitle[data-v-3d976094]{padding:80px 64px 48px}}.title[data-v-3d976094]{letter-spacing:0;line-height:44px;font-size:36px;font-weight:500}@media (min-width: 768px){.title[data-v-3d976094]{letter-spacing:-.5px;line-height:56px;font-size:48px}}.lead[data-v-3d976094]{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-3d976094]{max-width:592px;letter-spacing:.15px;line-height:28px;font-size:20px}}.VPTeamPageSection[data-v-9813fdfa]{padding:0 32px}@media (min-width: 768px){.VPTeamPageSection[data-v-9813fdfa]{padding:0 48px}}@media (min-width: 960px){.VPTeamPageSection[data-v-9813fdfa]{padding:0 64px}}.title[data-v-9813fdfa]{position:relative;margin:0 auto;max-width:1152px;text-align:center;color:var(--vp-c-text-2)}.title-line[data-v-9813fdfa]{position:absolute;top:16px;left:0;width:100%;height:1px;background-color:var(--vp-c-divider)}.title-text[data-v-9813fdfa]{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-9813fdfa]{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-9813fdfa]{padding-top:40px}.VPTeamMembersItem[data-v-0949c979]{display:flex;flex-direction:column;gap:2px;border-radius:12px;width:100%;height:100%;overflow:hidden}.VPTeamMembersItem.small .profile[data-v-0949c979]{padding:32px}.VPTeamMembersItem.small .data[data-v-0949c979]{padding-top:20px}.VPTeamMembersItem.small .avatar[data-v-0949c979]{width:64px;height:64px}.VPTeamMembersItem.small .name[data-v-0949c979]{line-height:24px;font-size:16px}.VPTeamMembersItem.small .affiliation[data-v-0949c979]{padding-top:4px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .desc[data-v-0949c979]{padding-top:12px;line-height:20px;font-size:14px}.VPTeamMembersItem.small .links[data-v-0949c979]{margin:0 -16px -20px;padding:10px 0 0}.VPTeamMembersItem.medium .profile[data-v-0949c979]{padding:48px 32px}.VPTeamMembersItem.medium .data[data-v-0949c979]{padding-top:24px;text-align:center}.VPTeamMembersItem.medium .avatar[data-v-0949c979]{width:96px;height:96px}.VPTeamMembersItem.medium .name[data-v-0949c979]{letter-spacing:.15px;line-height:28px;font-size:20px}.VPTeamMembersItem.medium .affiliation[data-v-0949c979]{padding-top:4px;font-size:16px}.VPTeamMembersItem.medium .desc[data-v-0949c979]{padding-top:16px;max-width:288px;font-size:16px}.VPTeamMembersItem.medium .links[data-v-0949c979]{margin:0 -16px -12px;padding:16px 12px 0}.profile[data-v-0949c979]{flex-grow:1;background-color:var(--vp-c-bg-soft)}.data[data-v-0949c979]{text-align:center}.avatar[data-v-0949c979]{position:relative;flex-shrink:0;margin:0 auto;border-radius:50%;box-shadow:var(--vp-shadow-3)}.avatar-img[data-v-0949c979]{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;object-fit:cover}.name[data-v-0949c979]{margin:0;font-weight:600}.affiliation[data-v-0949c979]{margin:0;font-weight:500;color:var(--vp-c-text-2)}.org.link[data-v-0949c979]{color:var(--vp-c-text-2);transition:color .25s}.org.link[data-v-0949c979]:hover{color:var(--vp-c-brand-1)}.desc[data-v-0949c979]{margin:0 auto}.desc[data-v-0949c979] a{font-weight:500;color:var(--vp-c-brand-1);text-decoration-style:dotted;transition:color .25s}.links[data-v-0949c979]{display:flex;justify-content:center;height:56px}.sp-link[data-v-0949c979]{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-0949c979]:hover,.sp .sp-link.link[data-v-0949c979]:focus{outline:none;color:var(--vp-c-white);background-color:var(--vp-c-sponsor)}.sp-icon[data-v-0949c979]{margin-right:8px;font-size:16px}.VPTeamMembers.small .container[data-v-c0f7e16c]{grid-template-columns:repeat(auto-fit,minmax(224px,1fr))}.VPTeamMembers.small.count-1 .container[data-v-c0f7e16c]{max-width:276px}.VPTeamMembers.small.count-2 .container[data-v-c0f7e16c]{max-width:576px}.VPTeamMembers.small.count-3 .container[data-v-c0f7e16c]{max-width:876px}.VPTeamMembers.medium .container[data-v-c0f7e16c]{grid-template-columns:repeat(auto-fit,minmax(256px,1fr))}@media (min-width: 375px){.VPTeamMembers.medium .container[data-v-c0f7e16c]{grid-template-columns:repeat(auto-fit,minmax(288px,1fr))}}.VPTeamMembers.medium.count-1 .container[data-v-c0f7e16c]{max-width:368px}.VPTeamMembers.medium.count-2 .container[data-v-c0f7e16c]{max-width:760px}.container[data-v-c0f7e16c]{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..68a6f9dd --- /dev/null +++ b/basic/config.html @@ -0,0 +1,82 @@ + + + + + + 配置详解 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

配置详解

前面一节已对 kotori.toml 有了大概认识,本节内容将更为全面的介绍它。kotori.toml 是一个 Kotori 程序的核心配置文件,它一般位于 Kotori 根目录,与 package.json 文件同级,使用 TOML 格式。

文件格式

虽然默认使用的是 TOML 格式,但 Kotori 也支持 YAML 格式(.yaml.yml)与 JSON 格式,其它格式使用方法请参考下文

配置项

以下是将先前的配置片段集中在一起的例子(仅作参考请勿直接复制):

toml
[global]
+port = 720
+dbPrefix = "romiChan"
+lang = "en_US"
+commandPrefix = "/"
+noColor = false
+level = 25
+dirs = [
+  "./node_modules/@custom-scope/",
+  "./test_modules"
+]
+
+[adapter.cmd-test]
+extends = "cmd"
+master = 2_333
+nickname = "Kotarou"
+age = 18
+sex = "male"
+self-id = 720
+
+[adapter.kisaki]
+extends = "qq"
+appid = "xxxx"
+secret = "xxxxx"
+master = 2_333
+retry = 10
+
+[plugin.menu]
+alias = "cd"
+keywords = [ "菜单", "功能", "帮助" ]
+content = "菜单 | 小鳥%break%/menu - 查看BOT菜单%break%/hitokoto - 获取一条一言%break%ByHotaru"

global.lang

定义全局使用的语言,目前仅支持英语、日语、台湾语、中文四门语言。

  • 值:LocaleType
  • 默认值:'en_US'
typescript
type LocaleType = 'en_US' | 'ja_JP' | 'zh_TW' | 'zh_CN';

global.commandPrefix

定义全局使用的命令前缀。

  • 值:string
  • 默认值: '/'

global.dirs

定义需要加载的模块目录。

  • 值:string[]
  • 默认值:['./node_modules/', './node_modules/@kotori-bot/']

global.port

定义 Kotori 使用的端口(Http 服务器与 WebSocket 服务器)。

  • 值:number
  • 默认值:720

global.dbPrefix

定义 Kotori 使用的数据库前缀。

  • 值:string
  • 默认值:'romiChan'

global.noColor

定义是否禁用彩色输出。

  • 值:boolean
  • 默认值:false

global.level

定义日志输出级别。

  • 值:LoggerLevel
  • 默认值:25
typescript
export enum LoggerLevel {
+  TRACE = 10,
+  DEBUG = 20,
+  RECORD = 25,
+  INFO = 30,
+  WARN = 40,
+  ERROR = 50,
+  FATAL = 60,
+  SILENT = 70
+}

adapter

定义 Bot。

  • 值:{ [botName: string]: AdapterConfig }
  • 默认值:{}
  • botName:Bot 的唯一标识符,可自定义,建议仅使用小写英语字母、数字、连字符字符([a-z0-9-])
typescript
interface AdapterConfig {
+  extends: string;
+  master?: number;
+  lang?: langType;
+  commandPrefix?: string;
+  [propName: string]?: unknown;
+}

AdapterConfig.extends

定义该 Bot 使用的适配器。

  • 值:string
  • 可选:否

AdapterConfig.master

定义该 Bot 的最高管理员 id(即该用户在平台的 id)。

  • 值:string | number
  • 可选:是

AdapterConfig.lang

定义该 Bot 使用的语言。

  • 值:LocaleType
  • 默认值:继承自 global.lang

AdapterConfig.commandPrefix

定义该 Bot 使用的命令前缀。

  • 值:string
  • 默认值: 继承自 global.commandPrefix

AdapterConfig[propName]

除去以上由 Kotori 内部定义的配置项,extends 中指定的适配器一般会额外定义配置项用于 Bot 内部,这些配置项也可能不存在或为可选,具体请参考该模块的详情页。

plugin

定义插件的配置项。

  • 值:{ [pluginName: string]: PluginConfig }
  • 默认值:{}
  • pluginName:模块的 id,去掉该模块包名中的命名空间与模块前缀(Kotori-plugin-)部分
typescript
interface PluginConfig {
+  filter?: {};
+  [propName: string]?: unknown;
+}

PluginConfig.filter

定义该插件使用的滤器。

  • 值:FilterOption
  • 默认值:{}

关于滤器使用请参考 滤器

PluginConfig[propName]

类似于 AdapterConfig 中的 [propName],该插件也可能会定义一些配置项用于插件内部,具体请参考该模块的详情页。

CLI 参数

Kotori 提供了一些 CLI 参数,用于在启动时修改配置文件中的配置项。

  • --daemon:是否使用守护进程
  • --mode [name]:设置程序运行模式,builddev
  • --dir [path]:设置程序运行根目录
  • --config [name]:设置配置文件名
  • --level [number]:设置日志输出级别
  • --port [number]:设置服务器端口
  • --dbPrefix [string]:设置数据库前缀
  • --noColor:禁用彩色输出

环境变量

Kotori 提供了一些环境变量,用于在启动时修改配置文件中的配置项。

  • NODE_ENV:设置程序运行模式,builddev
  • DIR:设置程序运行根目录
  • CONFIG:设置配置文件名
  • PORT:设置服务器端口
  • DB_PREFIX:设置数据库前缀
  • LEVEL:设置日志输出级别
  • NO_COLOR:禁用彩色输出
  • DAEMON:是否使用守护进程

设置环境变量只需在运行根目录下创建 .env 文件,并以 KEY=VALUE 的形式写入即可,例如:

ini
NODE_ENV=build
+CONFIG=config.toml
+PORT=720
+DB_PREFIX=romiChan
+LEVEL=25
+NO_COLOR=off
+DAEMON=on

NOTE

在环境变量文件中,对于 boolean 值,使用 on 表示 true,使用 off 或其它任何值表示 false

优先级

一般地,CLI 参数 > 环境变量 > 配置文件 > 默认值

+ + + + \ No newline at end of file diff --git a/basic/index.html b/basic/index.html new file mode 100644 index 00000000..834077af --- /dev/null +++ b/basic/index.html @@ -0,0 +1,29 @@ + + + + + + 简介 | Kotori + + + + + + + + + + + + + + + + + + +
Skip to content

简介

kotori-bot

kotori 是一个跨平台、解耦合、现代化于一体的聊天机器人框架,运行于 Node.js 环境,使用 TypeScript 语言开发。

概述

「Kotori」是一个罗马字,在日语中是「ことり」(小鳥)的意思,发音为 /kotolɪ/ ,该名字取自于 Key 公式 的游戏 《Rewrite》 中主要女性角色之一:神户小鸟 (神戸(かんべ) 小鳥(ことり))。 借助 Kotori,可快速搭建一个多平台、功能强大的聊天机器人应用,通过安装不同模块为 Kotori 扩展功能、玩法和个性化配置等。同时,Kotori 为开发者提供了现成的 Cli 用于模块开发与 Kotori 二次开发。

特点

  • 跨平台 得益于模块化支持,通过编写各种模块实现不同的功能与聊天平台接入

  • 解耦合 基于控制反转(IOC)与面向切面编程(AOP)思想,减少代码冗余与复杂度

  • 现代化 使用现代化的 ECMAScript 语法规范与强大的 TypeScript 类型支持

扩展支持

平台

  • QQ(基于腾讯官方接口)
  • QQ(基于 OneBot11 标准,适用于 NapCatgo-cqhttp 等项目)
  • CMD 命令行
  • Slack
  • Telegram
  • Email
  • Discord
  • MinecraftBedrock (基于 WebSocket)

即将支持:

  • Kook/开黑啦
  • WeChat/微信
  • Line
  • What's App
  • DingTalk

数据

  • LevelDb

Kotori 使用极为轻量的 LevelDb 作为数据存储。

你是?

  • 框架用户,使用 Kotori 搭建自己的机器人:快速开始
  • 平台用户,使用官方提供的现成机器人:立即使用
  • 开发者
    • 模块开发者,开发 Kotori 模块:开发指南
    • Node.js 开发者,将 Kotori 作为依赖开发自己的项目:接口参考
+ + + + \ No newline at end of file diff --git a/basic/modules.html b/basic/modules.html new file mode 100644 index 00000000..c184611b --- /dev/null +++ b/basic/modules.html @@ -0,0 +1,59 @@ + + + + + + 模块安装 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

模块安装

介绍

模块(Modules) 是 Kotori 的重要组成部分之一,通过使用模块以扩展各式各样的功能。

模块根据功能与应用范围不同,主要分为以下三大类型:

  • 插件(Plugin):为用户提供多种功能、玩法、扩展,数量最多的类型
  • 服务(Service):用于提供封装好的接口
  • 适配器(Adapter):用于接入各个聊天平台

寻找模块

Kotori 模块中心 内收录了大部分 Kotori 模块。选择所需模块,在详情页中会有插件的基础信息、介绍、使用说明、配置说明等。

下载与安装

此处以「QQ 适配器服务模块」(@kotori-bot/kotori-plugin-adapter-qq)为例。

模块的包名除去 @xxx/ 的部分(如果有),会有一段相似的开始字段,将其称之为「模块前缀」。通过模块前缀可判断模块类型,如「kotori-plugin-adapter-xxx」表示适配器,「kotori-plugin-database-xxx」表示数据库服务,「kotori-plugin-xxx」表示插件自定义服务,详细内容请参考 开发文档 - 插件范式

使用包管理工具

复制模块详情页里中的安装指令,或手动输入对应模块的 npm 包名,在 Kotori 根目录运行:

bash
npm install @kotori-bot/kotori-plugin-adapter-qq

手动下载安装

WARNING

该方法目前仅建议插件开发者在工作区下可适当使用。

在模块详情页里跳转至对应的 npm 地址或 GitHub 地址,下载模块的构建产物。 解压压缩包并移动至 Kotori 根目录下的 ./modules/ 内。

GitHub 仓库中存有模块的源码,在当前阶段,你应下载并使用模块的构建产物而非源码

务必确保解压后的模块文件夹仅有一层文件夹而非多层,否则将无法识别与加载模块。

添加加载目录

模块安装在 ./modules 目录内请忽略该步骤

通过包管理工具安装的模块一般会安装在 Kotori 根目录下的 ./node_modules/ 内,如若插件包名带有 @xxx/ 的前缀,表示为包的命名空间,上述示例模块中的「@kotori-bot/」为 Kotori 官方包的命名空间,表示官方模块,其余的命名空间或无命名空间的模块为社区模块

所有未安装在 ./modules/ 都应配置 kotori.ymlglobal.dirs 项以设置额外的加载根目录,但对于 ./node_modules/@kotori-bot/ 命名空间已经存在于 Kotori.yml 默认配置中,因此无需担心。

对于其它安装目录或命名空间则需手动添加到 Kotori.yml 中,如:

  • 使用包管理工具安装一个命名空间为 @custom-scope/ 的模块
  • 将模块安装在 Kotori 根目录下的 ./test_modules/

对应配置为:

toml
[global]:
+dirs = [
+  "./node_modules/",
+  "./node_modules/@kotori-bot/",
+  # 上面为默认配置的加载目录
+  "./node_modules/@custom-scope/",
+  "- ./test_modules"
+]

配置模块

根据安装的模块类型不同,配置策略也将不同。

插件

插件配置数据应写在 kotori.ymlplugin.<plugin-name> 项下,其中 <plugin-name> 为插件名字,不应含有包的命名空间与模块前缀,值必须是一个对象。插件的配置项由插件本身提供与指定,并非所有插件本身都会提供配置项。一般地,有提供配置项的插件内都会有一套默认配置,因此不配置也可以正常运行插件。插件的配置和说明可参考该插件的详情页,此处以 「菜单插件」(@kotori-bot/kotori-plugin-menu)为例,在详情页查看配置说明后在 kotori.yml 中配置相关内容:

toml
[plugin.menu]
+alias = "cd"
+keywords = [ "菜单", "功能", "帮助" ]
+content = "菜单 | 小鳥%break%/menu - 查看BOT菜单%break%/hitokoto - 获取一条一言%break%ByHotaru"

适配器

适配器配置数据应写在 kotori.ymladapter[instanceName] 项下,其中 instanceName 为适配器实例(以下简称「Bot」)名字应由小写英语字母、数字、连字符([a-z0-9])组成,值必须是一个对象。适配器的配置数据不会作用于适配器模块,Kotori 会根据配置数据创建对应 Bot。对于适配器的配置,必须提供一些必要配置项才能确保实例的正常运行,其中有部分配置项由 Kotori 内部定义,如:

toml
[adapter.cmd-test]
+extends = "cmd"
+master = 2_333

「cmd-test」是该 Bot 的名字也是在 kotori 程序运行中的唯一标识符,不可重复。extends 用于指定该实例使用的适配器,值为适配器模块的包名除去命名空间与适配器服务前缀的字符串,如:使用「@kotori-bot/kotori-plugin-adapter-qq」适配器,则应填入「qq」。master 用于指定该实例的最高管理员(Admin),值类型可为数字或字符串,非必填。 除去由 Kotori 内部定义的配置项以外,一般还需要填入该适配器要求传入的必要配置项。

toml
[adapter.cmd-test]
+extends = "cmd"
+master = 2_333
+nickname = "Kotarou"
+age = 18
+sex = "male"
+self-id = 720

cmd 适配器即「@kotori-bot/kotori-plugin-adapter-cmd」,属于 kotori 预装模块之一,为 kotori 程序当前所在控制台提供聊天交互功能,也是最方便的测试机器人的场所(但并不推荐,因为只支持文字交互,模块开发有更好的测试场所选择,详细内容请参考 开发文档 - 项目构建

不过此处使用的 cmd 适配器定义的配置项均有默认值因此为可选。接着使用「@kotori-bot/kotori-plugin-adapter-qq」适配器再创建一个 Bot:

toml
[adapter.cmd-test]
+extends = "cmd"
+master = 2_333
+nickname = "Kotarou"
+age = 18
+sex = "male"
+self-id = 720
+
+[adapter.kisaki]
+extends = "qq"
+appid = "xxxx"
+secret = "xxxxx"
+master = 2_333
+retry = 10

查看 QQ 适配器的详情页面可知,appidsecret 为其定义的必要配置项,retry 为其定义的可选配置项,关于 QQ 适配器的具体使用与配置项含义请查看其插件详情页。

关于配置文件的详细介绍请参考 配置详解

+ + + + \ No newline at end of file diff --git a/basic/start.html b/basic/start.html new file mode 100644 index 00000000..84329136 --- /dev/null +++ b/basic/start.html @@ -0,0 +1,44 @@ + + + + + + 快速开始 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

快速开始

一键使用仓库

尽管 Kotori 的安装几乎已可以说是开箱即用的地步,只需要简单动用一下包管理器同时写入配置文件即可完成安装,但鉴于这仅仅安装了本体,并未附带一些基础且必要的模块,因此这里有一份来自于社区的 Kotori 手把手安装教程,该仓库提供了一个包含基础模块的 package.json 与 Kotori 配置文件以及启动脚本。

NOTE

如若视频加载不出来请考虑使用 VPN,当然这并不重要。

下载该仓库

  • 方法 1:使用 git 克隆
bash
git clone https://github.com/kotorijs/kotori-app.git
  • 方法 2:直接下载压缩包并解压

下载地址

安装依赖

1.安装 Node.js

2.安装项目依赖

支持 npmyarn 主流包管理器安装,pnpm 安装后可能存在启动问题,cnpmdenobun 未经测试,但理论仍可以进行安装并运行。先进入仓库根目录,输入以下任一命令安装:

bash
npm install

如若出现安装失败问题则强制安装:

bash
npm install --force

强制更新所有包:

bash
npm update --force

配置 kotori.toml

关于 kotori.toml 的详细介绍请参考 配置详解

正式启动

在仓库根目录打开命令行输入:

bash
npm exec kotori

当然,也可以直接使用仓库根目录下提供的两个启动文件:

  • Windows 平台下使用 start.bat
  • Linux/Mac 平台下使用 start.sh

启动完成后,在控制台内输入 /status 查看,输入 /help 查看帮助内容

进入 Webui

启动 Kotori 后,会在控制台看到以下类似信息:

log
7/19 23:59:59 LOG (1372799) : Http server started at http://127.0.0.1:720
+7/20 0:0:0 LOG (1372799) : WebSocket server started at ws://127.0.0.1:720
+KotoriO > 当前未设置 Webui 账号与密码,请输入 /webui 指令以进行初始化

此处的 http://127.0.0.1:720 即为 Kotori 网页控制台,第一次启动会提示设置用户名与密码,通过输入 /webui 命令进行设置:

bash
/webui
+KotoriO > 请输入 Webui 用户名:
+Admin
+KotoriO > 请输入 Webui 密码:
+kotori666
+8/11 10:37:5 LOG (1435470) [cmd/cmd-test]: User 2333 exec command webui successfully
+KotoriO > 配置成功!请再次输入指令以查看运行状态
+/webui
+8/11 10:47:30 LOG (1435470) [cmd/cmd-test]: User 2333 exec command webui successfully
+KotoriO > Webui 服务已启动!运行端口:720

当想重置用户名或密码时也可以输入以下指令:

bash
/webui -R
+8/11 10:35:51 LOG (1372799) [cmd/cmd-test]: User 2333 exec command webui -R successfully
+KotoriO > Webui 账户数据已重置

安装适配器与插件

  • 适配器:用于对接各个聊天平台,比如说你要用 qq 即安装 qq 适配器即可

  • 插件:扩展机器人的各种功能

  • 模块中心

此处收录了大部分的 Kotori 模块,选择自己喜欢的模块打开详情页查看说明,复制包名使用你的包管理器进行安装。以下指令会为你安装一些重要的基础插件:

更新 Kotori 与模块

一般地,在需要更新时使用以下命令即可进行更新:

bash
npm update -f

此外,当变动较大时可重新重复上述安装步骤。

使用 GUI

Kotori 内置了一个简易的交互式命令行功能,用于执行简单的操作,在根目录下输入以下命令:

bash
npm exec kotori ui

ui1

gui2

启用守护进程

程序运行中可能总会发生一些不会如用户所愿的错误和异常,为了避免这种情况,Kotori 提供了守护进程功能,当程序崩溃时,守护进程会自动重启程序,而在默认情况下(即程序处于 build 模式),守护进程是会自动启用的,想改变这一策略可更改环境变量或 CLI 参数,具体参见 配置详解

bash
24/8/11 10:35:56 INFO (1372340) : [Daemon] Starting...

当 Kotori 启动时出现以上消息则表明守护进程成功启用。同时在启用守护进程后,也可以通过指令 /restart(来自「@kotori-bot/kotori-plugin-core」) 手动进行重启:

bash
/restart
+KotoriO > Kotori 正在重启中...
+8/11 10:35:56 LOG (1372799) [cmd/cmd-test]: User 2333 exec command restart successfully
+24/8/11 10:35:56 WARN (1372340) : [Daemon] Restarting...

当然,守护进程在程序崩溃时也有一套判定规则,如若程序在短时间内多次崩溃,则视为存在重大异常问题,此时便会停止自动重启需要人工进行排查原因。。

从源码构建

请参考 作为依赖与二次开发

其它方式

N.A

+ + + + \ No newline at end of file diff --git a/basic/usage.html b/basic/usage.html new file mode 100644 index 00000000..394904ed --- /dev/null +++ b/basic/usage.html @@ -0,0 +1,72 @@ + + + + + + 立即使用 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

立即使用

TIP

本篇适用于想立即体验基于 Kotori 搭建的 Bot 实际效果、或单纯想使用由官方提供的各平台 Bot 服务的平台用户。


服务平台

目前共提供 2 个平台、四个 Bot:

QQ

Kanbe Kotori(小鳥一号)

小鳥二号

基于 OneBot11 标准的第三方 QQ 机器人(@Kotori-bot/kotori-plugin-adapter-onebot)。

立即使用:Kotori 交流群

小鳥三号

基于 Tencent 官方 API(@Kotori-bot/kotori-plugin-adapter-qq)。相比于第三方 QQ 机器人,限制较多(不可发送主动消息、URL 需备案等),其余功能与「小鳥二号」基本一致。

立即使用:Kotori 交流群

Misakura

基于 OneBot11 标准的第三方 QQ 机器人。

立即使用:Kotori 交流群

Telegram

Sena

基于 @kotori-bot/adapter-telegram 的 Telegram 机器人。

立即使用:@Sena0620Bot

Discord

Slack

功能一览

WARNING

以下内容已严重过期

以下展示并非全部功能。

@kotori-bot/kotori-plugin-core

  • /core 查看实例统计信息
  • /bot 查看当前 bot 信息与运行状态
  • /bots 查看所有 bot 信息与运行状态
  • /version 查看版本信息
  • /about 帮助信息
  • /update 检查更新

@kotori-bot/kotori-plugin-help

  • /help [command] 查看指令帮助信息

@kotori-bot/kotori-plugin-menu

/menu 查看 BOT 菜单 别名:cd

kotori-plugin-random-img

  • /sex [tags] Pixiv 图片
  • /sexh HuliImg 图片
  • /bing 必应每日图
  • /day 60s 带你看世界
  • /earth 实时地球
  • /china 实时中国

kotori-plugin-bangumi

  • /bgm <content> [order:number=1] 番组计划搜索游戏/动漫
  • /bgmc 获取番组计划今日放送
bash
/bgm 素晴日
+> 原名:素晴らしき日々~不連続存在~公式ビジュアルアーカイヴ
+中文名:素晴之日 不连续的存在 Official Visual Archive
+介绍:人気アダルトゲームブランド「ケロQ」から、実に6年ぶりに発売された新作『素晴らしき日々 ~不連続存在~』。その魅力をギュッと閉じ込めたファン必携の一冊。描き下ろしイラスト&原作を担当したSCA-自(すかぢ)氏の新作書き下ろしテキスト満載でお届け。
+标签:素晴らしき日々 设定集 电波 神作 素晴日 公式书 2010 百合 FanBook 悬疑 画集 画集・設定資料集 推理 VFB
+详情:https://bgm.tv/subject/8318
+[image]

kotori-plugin-bilibili

  • /bili <bvid> Bilibili 视频信息查询
  • /bilier <uid> Bilibili 用户信息查询

kotori-plugin-hitokoto

  • /hitokotos 随机语录
  • /hitokoto 获取一条一言
bash
/hitokoto
+> 如果人们不相信数学简单,那是因为他们没有意识到人生有多复杂。——冯诺依曼
+类型:俗语

kotori-plugin-mediawiki

  • /wiki <content> [order] 搜索 MediaWiki
  • /wikil 查看 MediaWiki 列表
bash
/wiki 月社妃
+> 标题:月社妃
+内容:月社妃(日语:月社(つきやしろ) 妃(きさき))是由ウグイスカグラ所制作的18禁galgame《纸上的魔法使》及其衍生作品的登场角色。是主人公四条琉璃的同胞妹妹。
+https://mzh.moegirl.org.cn/.php?curid=384932
+来源:萌娘百科

kotori-plugin-github

  • /github <repository> 查询 Github 仓库信息
bash
/github kotorijs/kotori
+> 地址:kotorijs/kotori
+描述:Cross platform, decoupled, and modernized ChatBot framework base on NodeJS
+语言:TypeScript
+所有者:kotorijs
+创建时间:
+2023-06-14T11:45:16Z
+最后更新时间:2023-12-31T15:28:10Z
+最后推送时间:2024-01-14T09:48:13Z
+开源协议:GNU General Public License v3.0

kotori-plugin-music

  • /music <name> [order:number=1] 网易云点歌 序号默认为 1,填 0 显示歌曲列表
bash
/music 夢水の調べ
+> 歌曲ID:2077744375
+歌曲标题:夢水の調べ
+歌曲作者:おはる
+歌曲下载:http://music.163.com/song/media/outer/url?id=2077744375.mp3
+歌曲封面:[image]

kotori-plugin-weather

  • /weather <area> 查询城市天气
bash
/weather 北京
+> 城市:北京市
+日期:周四
+温度:-6~4℃
+天气:晴
+风度:南风-2级
+空气质量:良
+
+日期:周五
+温度:-5~0℃
+天气:阴
+风度:东风-1级
+空气质量:良
+
+日期:周六
+温度:-8~0℃
+天气:阴
+风度:东北风-1级
+空气质量:良
+ + + + \ 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..fe812115 --- /dev/null +++ b/guide/base/command.html @@ -0,0 +1,231 @@ + + + + + + 指令注册 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

指令注册

引入

在上一节中学习了事件系统的使用,现在通过 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,支持的类型有: stringnumberboolean
  • 可选参数中可在参数类型后添加 =值 设置默认参数
  • 参数中可在参数名前添加 ... 设置剩余参数,与 TypeScript 不同的是,剩余参数的类型不需要加上数组表示
  • 在指令模板字符最后添加 - 指令描述 设置指令描述
  • 指令内容为截止到第一个参数出现之前的字符串(不含空格)
  • 参数名应尽量语义化;剩余参数应在所有参数最后面;应仅在可选参数中设置默认参数;必要参数应在可选参数之前;指令模板字符不应包含指令前缀

选项

通过 Command.option() 设置指令选项,接受两个参数:

  1. 该选项的缩写名
  2. 选项模板字符,可设置多个指令选项
typescript
ctx
+  .command('bar')
+  .option('S', 'speaker - 这是选项的描述')
+  .option('G', 'global:boolean - 这是一个布尔类型的选项')
+  .option('T', 'time:number - 这是一个数字类型的选项');
  • 一般地,使用单个大写字母作为缩写名,解析器将把字符串中单个连接符 - 开头内容作为选项缩写名解析
  • 使用多个小写字母作为全名(多个单词使用 单个连接符 - 解析),解析器将把字符串中两个连接符 -- 开头的内容作为选项全名解析
  • 在选项模板字符最后添加 - 指令描述 设置选项描述
  • 类似地,选项模板字符有着与指令模板字符一样的类型注解方式,默认 string,支持的类型有: stringnumberboolean
  • 选项模板字符不支持设置默认值

回调函数

通过 Command.action() 设置指令的回调函数,且每个指令仅可设置一个回调函数,回调函数中接收两个参数:

  1. argsoptions 两个键组成的对象,类型分别为 (string | number | boolean)[]Record<string, string | number | boolean>,分别代表用户输入的参数值与选项值
  2. 会话事件数据

options 中的键为对应选项的全名而非缩写名。

typescript
ctx.command('bar <...args>').action((data) => {
+  ctx.logger.info(data.args); // 输出参数值数组
+  ctx.logger.info(data.options); // 输出选项值对象
+  session.send('这是一条消息');
+});

回调函数中的第二个参数为当前会话事件数据 sessionsession 对象包含了当前指令触发产生的所有上下文信息,比如消息 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_querylist_addlist_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 事件有 messageIdsendermessagegroupId,而 EventDataPrivateMsg 事件没有 groupIdEventDataPrivateRecall 事件其中的仅有 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 中的 「模板字符串」)。接收两个参数:

  1. 源字符串
  2. 模板字符串参数,其类型有两种,分别为 ObjectArgsArrayArgs
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''voidnull0 则不作处理(一般不允许传入这些东西,主要发生在指令处理的回调函数返回值上)
  • 对于 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_mangerno_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,这将在第三章中讲解

+ + + + \ No newline at end of file diff --git a/guide/base/events.html b/guide/base/events.html new file mode 100644 index 00000000..148c5b10 --- /dev/null +++ b/guide/base/events.html @@ -0,0 +1,90 @@ + + + + + + 事件系统 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

事件系统

事件系统(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.apiApi 的实例对象,提供了多种与聊天平台交互的接口,此处用到的 sendG丨groupMsgsendPrivateMsg 分别是发送群聊消息与发送私聊消息,第一个参数为消息内容,第二个参数分别为群聊 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.apiadapter.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.elsession.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

一般地,自定义事件应只用于单个模块内部,用于多个模块间相互通信传输数据时,每个涉及模块应先加载定义自定义事件的模块,以免出现类型定义的问题。

+ + + + \ No newline at end of file diff --git a/guide/base/middleware.html b/guide/base/middleware.html new file mode 100644 index 00000000..7ec063c7 --- /dev/null +++ b/guide/base/middleware.html @@ -0,0 +1,86 @@ + + + + + + 中间件 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

中间件

中间件(Middleware) 是 Kotori 中另一种监听消息事件的语法糖,与指令系统类似,它也是对 on_message 事件的再处理与封装。中间件的主要用途是提前判断或者过滤掉不必要的消息事件,这样后续的指令和正则表达式等位于下游的设施也不会被这些消息事件触发,从而提高效率。

中间件的工作原理与 Express 等后端框架中的中间件概念基本一致。每次收到消息时,Kotori 会依次执行所有已注册的中间件,只有当所有中间件都通过时,该消息事件才会真正被处理。

注册中间件

通过 ctx.midware() 注册一个中间件,该方法接受两个参数:

  1. 中间件回调函数
  2. 可选的中间件优先级,默认为 50

优先级数字越小(但不能为负数)则优先级越高,如果两个中间件的优先级相同,则按照注册顺序执行,先注册的中间件会先执行。

WARNING

如无特殊需求建议请勿更改优先级,否则可能会导致一些意料之外的问题。

typescript
ctx.midware((next, session) => {
+  // 中间件逻辑...
+  next(); // 通过此中间件
+}, 80); // 优先级为 80

中间件回调函数接收两个参数:

  1. next 函数,调用它将执行下一个中间件
  2. 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 的各个环节进行拦截和处理,实现更加灵活和可控的消息处理逻辑。

限制命令使用频率

WARNING

以下内容由 Claude3 生成,不保证可用性。

有时我们需要限制某些命令的使用频率,以防止被滥用。这时可以使用中间件来实现这一功能。

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); // 设置较高优先级

上述代码定义了一个中间件,用于限制命令的使用频率。具体逻辑如下:

  1. 首先检查收到的消息是否以 / 开头,如果不是则直接调用 next()通过该中间件。
  2. 获取命令名和发送者 ID。
  3. 检查是否存在该命令的使用记录,如果没有则新建一个记录。
  4. 获取该用户对此命令的最后使用时间,如果不存在则认为是第一次使用,最后使用时间设为 0。
  5. 计算距离上次使用的时间间隔(单位为秒)。
  6. 如果时间间隔小于 10 秒,则拒绝执行该命令,发送 '命令使用过于频繁,请稍后再试'
  7. 如果时间间隔大于等于 10 秒,则更新该用户对此命令的最后使用时间,并调用 next()通过该中间件。

该中间件的优先级设为 10,这是为了让它能够比大多数命令优先执行。我们使用 Map 来存储命令使用记录,外层 Map 的键为命令名,值为另一个 Map,内层 Map 的键为用户 ID,值为该用户最后一次使用该命令的时间戳。

通过这种方式,我们可以精确地控制每个用户对每个命令的使用频率,并且只对命令消息生效,不会影响到其他普通消息的处理。需要注意的是,这个示例使用了内存来存储命令使用记录,因此在重启 Bot 后记录会被清空。在实际应用中,你可以将记录持久化存储到数据库中。

+ + + + \ No newline at end of file diff --git a/guide/base/regexp.html b/guide/base/regexp.html new file mode 100644 index 00000000..038d4ceb --- /dev/null +++ b/guide/base/regexp.html @@ -0,0 +1,75 @@ + + + + + + 正则匹配 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

正则匹配

正则匹配(RegExp) 同样是 Kotori 中一种监听消息事件的语法糖。它的主要用途是通过正则表达式匹配消息内容,然后执行相应的处理逻辑。值得一提的是,正则匹配位于消息事件的最后一环(在中间件和指令之后执行),这意味着只有通过了所有中间件和指令的消息,才会进入正则匹配的环节。

正则匹配依赖于正则表达式的强大功能,可以实现多种匹配模式,例如完全匹配、模糊匹配等,为消息处理提供了更大的灵活性。

注册正则匹配

通过 ctx.regexp() 注册一个正则匹配,该方法接受两个参数:

  1. match: 用于匹配消息内容的正则表达式
  2. callback: 当正则匹配成功时执行的回调函数
typescript
ctx.regexp(/^\/start$/, (match, session) => {
+  session.send('游戏开始!');
+});

上述代码注册了一个正则匹配,当收到消息内容为 /start 时,它会执行回调函数,并向发送者发送 '游戏开始!' 消息。

callback 函数接收两个参数:

  1. match: 正则匹配结果,是一个数组,第一项为完整匹配结果,后续项为各个捕获组的内容
  2. 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「 这样的算术表达式。

在回调函数中,我们根据匹配的运算符和操作数进行相应的计算,并将结果作为消息发送出去。需要注意的是,这里我们使用了可选的捕获组,因此在处理单个操作数的情况时需要进行判断。

通过正则匹配的强大功能,我们可以灵活地处理各种复杂的消息,实现个性化的交互体验。而将正则匹配与其他功能(如指令系统、数据持久化等)相结合,就能构建出更加强大的应用程序。

+ + + + \ No newline at end of file diff --git a/guide/components/adapter.html b/guide/components/adapter.html new file mode 100644 index 00000000..8fb95a85 --- /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..dc65889c --- /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..0be2d228 --- /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..8bebe8f6 --- /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..9fa2fbf1 --- /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..5d35bf1e --- /dev/null +++ b/guide/index.html @@ -0,0 +1,28 @@ + + + + + + 前言 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

前言

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 的开发历程、版本记录、运行流程、设计构思、设计参考等。
+ + + + \ No newline at end of file diff --git a/guide/modules/context.html b/guide/modules/context.html new file mode 100644 index 00000000..a7efea12 --- /dev/null +++ b/guide/modules/context.html @@ -0,0 +1,227 @@ + + + + + + 上下文 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

上下文

上下文(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 属性可获取当前上下文的根上下文实例,无论是子级上下文 ctxChild1ctxChild2,还是继承了 ctxChild1 的孙级上下文 ctxChild3,其根上下文实例均指向 ctx,而根上下午实例的 ctx.root 指向自身。

事件系统

以上内容均由最初的 Context 类定义,通过类原生的继承方式和 ctx.inject()ctx.mixin() 等方法对 Context 进行装饰或扩展。而在 Context 类内部,它本身就已为自己注册并注入了两个实例对象,其一便是事件系统,这也是在第二章中介绍事件系统时说道「事件订阅者模式与事件系统共同构成了 Kotori 的基础」的原由,只不过 Context 类本身并未使用事件系统功能,且仅直接定义了 readydispose 事件,两者被作为整个程序生命周期的重要一环,其余事件由另一实例对象(见下文)或 Kotori 核心类定义。

插件系统

其二便是插件系统,它定义了 ready_moduledispose_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() 实现。

+ + + + \ No newline at end of file diff --git a/guide/modules/decorator.html b/guide/modules/decorator.html new file mode 100644 index 00000000..4ab7f5cf --- /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..d2dd924c --- /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..8009e58c --- /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..3a4847d0 --- /dev/null +++ b/guide/modules/plugin.html @@ -0,0 +1,203 @@ + + + + + + 模块与插件 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

模块与插件

前言

恭喜你,只要学习完本章你将成为一名合格的「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>
+  ) {
+    /* ... */
+  }
+}

在导出类式中,可同时在外部导出诸如 configlanginject 属性,也可在类中设置相应的静态属性,一般地,请使用后者。如若两者同时存在,类中的属性将会覆盖外部导出的属性。

诚然,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 的识别顺序(一经识别成功将不再继续识别后续内容):

  1. 适配器类实现
  2. 默认导出类
  3. 默认导出函数
  4. main() 导出函数
  5. 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)熏陶的框架。为数不多的缺点是它需要手动声明类型且对新手而言不容易上手,但如若你有足够的基础则强烈推荐使用。

当然,这并不算在此展开详细介绍,它还需要你了解一点其它内容作为基础,因而它被放在本章最后一节进行具体讲述。

+ + + + \ No newline at end of file diff --git a/guide/modules/rescript.html b/guide/modules/rescript.html new file mode 100644 index 00000000..1ff90318 --- /dev/null +++ b/guide/modules/rescript.html @@ -0,0 +1,35 @@ + + + + + + 使用 ReScript 开发 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

使用 ReScript 开发

ReScript 是一门健壮的类型化语言,可以编译成高效易读的 JavaScript。相比于 TypeScript,ReScript 是 JavaScript 的子集,有着远比 TypeScript 更为严格和安全的类型系统。也是 OCaml 的方言之一,结合了大量函数式编程与现代化编程特性,同时保留了 C 系语言的花括号语法风格,这使你不会像面对其它函数式编程一样对其陌生语法感到茫然,变得极易上手和入门。如果你是一名 Rust 开发者将会对 ReScript 很多地方感到亲切(就像是没有所有权和生命周期的 Rust)。

NOTE

详细信息与入门指南请参考 The ReScript Programming Language

基本使用

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
+}
+ + + + \ No newline at end of file diff --git a/guide/modules/schema.html b/guide/modules/schema.html new file mode 100644 index 00000000..f3d6acf0 --- /dev/null +++ b/guide/modules/schema.html @@ -0,0 +1,76 @@ + + + + + + 配置检测 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

配置检测

配置检测(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)
+}

该方法会返回一个对象,当 valuetrue 时,对象上存在 data 属性,其值即为处理后的结果,当 valuefalse 时,对象上存在 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 中的引用

配置文件

数据检验

+ + + + \ No newline at end of file diff --git a/guide/modules/service.html b/guide/modules/service.html new file mode 100644 index 00000000..4fbe460f --- /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..0f0b59f3 --- /dev/null +++ b/guide/start/environment.html @@ -0,0 +1,28 @@ + + + + + + 环境搭建 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

环境搭建

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 的功能,当然你也可以根据你的喜好选择,如:

  • WebStorm
+ + + + \ No newline at end of file diff --git a/guide/start/publish.html b/guide/start/publish.html new file mode 100644 index 00000000..d5c8f0e3 --- /dev/null +++ b/guide/start/publish.html @@ -0,0 +1,67 @@ + + + + + + 模块发布 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

模块发布

当开发完毕模块后,可以将它发布至社区,一个 Kotori 模块一般会同时发布到如下三个平台:

优先级(重要程度):npm > 模块中心 > 开源社区。每一个公开的 Kotori 模块都应发布至 npm 并作为模块的主要获取途径。Kotori 使用 GPL-3.0 协议,该协议要求 Kotori 的所有模块及其二次开发项目也必须使用 GPL-3.0 协议且开源,因此发布到开源社区是必要的,开源行为本身也是一种无私奉献、共享知识和回馈社区的体现。

构建产物

「构建产物」在 JavaScript 生态中指将源码(Kotori 模块开发中一般为 TypeScript 文件)进行处理以适用于生产环境中(处理过程一般有 TypeScript 转为 JavaScript、向下兼容语法、压缩代码等)。JavaScript 生态中构建工具非常多,你可以选择喜欢的构建工具并自习配置,当然如果你对此并不了解也可以使用 Kotori 默认的构建方式(通过TypeScript 自带的 tsc 程序),在你的模块根目录中输入以下指令:

bash
pnpm build

一般地,你将会发现在模块根目录出现了一个 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 注册账号,然后根据提示在浏览器内登录:

bash
npm login

当一切就绪时:

bash
npm publish

当没有任何意外问题时,访问 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 模块中心 查看你的模块。

放在最后

+ + + + \ No newline at end of file diff --git a/guide/start/setup.html b/guide/start/setup.html new file mode 100644 index 00000000..eee707bb --- /dev/null +++ b/guide/start/setup.html @@ -0,0 +1,171 @@ + + + + + + 项目构建 | Kotori + + + + + + + + + + + + + + + + + +
Skip to content

项目构建

在本阶段中开发模块一并通过搭建工作区开发,此外还可通过克隆 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 工程文件夹,代码存放处
    • index.ts 整个模块的入口文件

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:

bash
pnpm dev

在浏览器中打开 http://localhost:2333 即可进入沙盒环境,输入 /echo Hello,Kotori! 以查看效果:

show

+ + + + \ No newline at end of file diff --git a/hashmap.json b/hashmap.json new file mode 100644 index 00000000..3e7c6aa6 --- /dev/null +++ b/hashmap.json @@ -0,0 +1 @@ +{"advanced_architecture.md":"Pljt76DU","advanced_browser.md":"CURKrjcr","advanced_contributing.md":"CcXEnDf1","advanced_develop.md":"CCQONjp6","advanced_history.md":"FDb9Wd5O","advanced_index.md":"epBIxi7X","advanced_testing.md":"BIJx2vTA","advanced_thanks.md":"CK7Fjmcb","api_index.md":"B1SyjzrW","basic_config.md":"CIBSN9d7","basic_index.md":"BT38T7GH","basic_modules.md":"R6u-LxoQ","basic_start.md":"UB_T-Yyu","basic_usage.md":"ggWDU75P","guide_base_command.md":"B1nKVjKI","guide_base_events.md":"BKi_1qAf","guide_base_middleware.md":"Do_XwOya","guide_base_regexp.md":"kd1jlc-g","guide_components_adapter.md":"BkEYU-ym","guide_components_api.md":"CDibfftx","guide_components_custom.md":"BNIS-43m","guide_components_elements.md":"BEJCY6Yx","guide_extend_tools.md":"C1RkeOP0","guide_index.md":"CUmZPOqd","guide_modules_context.md":"CfNIj0Wx","guide_modules_decorator.md":"DZm9Jfnd","guide_modules_filter.md":"B6ZA5MSX","guide_modules_i18n.md":"DN93rR3l","guide_modules_plugin.md":"F2YmqKkM","guide_modules_rescript.md":"Bl-r_LjA","guide_modules_schema.md":"D-o7cJAG","guide_modules_service.md":"DMQ0sFPf","guide_start_environment.md":"X_FtbxVb","guide_start_publish.md":"s23QhAsX","guide_start_setup.md":"ZNwpORZ-","index.md":"P8--M6MF","modules_index.md":"D1mO3gni"} diff --git a/index.html b/index.html new file mode 100644 index 00000000..dcca5940 --- /dev/null +++ b/index.html @@ -0,0 +1,28 @@ + + + + + + Kotori + + + + + + + + + + + + + + + + + +
Skip to content

小鳥 · KotoriBot

基于 Node.js + TypeScript 的跨平台聊天机器人框架

KotoriBot
🚀
跨平台

得益于模块化支持,通过编写各种模块实现不同的功能与聊天平台接入

🧩
解耦合

基于控制反转与面向切面编程思想,减少代码冗余与复杂度

🛠️
现代化

使用现代化的 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..e8e0b259 --- /dev/null +++ b/modules/index.html @@ -0,0 +1,28 @@ + + + + + + Kotori + + + + + + + + + + + + + + + + + +
Skip to content

Last updated:

+ + + + \ 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