跨平台
得益于模块化支持,通过编写各种模块实现不同的功能与聊天平台接入
diff --git a/404.html b/404.html new file mode 100644 index 00000000..3c2810f3 --- /dev/null +++ b/404.html @@ -0,0 +1,24 @@ + + +
+ + +Kotori use workspace developing (monorepo) by pnpm and submodules management by git, and linter and formatter by Biome (fucks eslint and prettier).
Kotori use workspace developing (monorepo) by pnpm and submodules management by git, and linter and formatter by Biome (fucks eslint and prettier).
To wait better supports...
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.
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'
+ }
+})
To wait better supports...
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.
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'
+ }
+})
TIP
Here need to improved.
This project is open source and we welcome your contributions!
Fork it!
Create your feature branch: git checkout -b my-new-feature
Commit your changes: git commit -am 'Add some feature'
Push to the branch: git push origin my-new-feature
Submit a pull request 😄
More details please refer to CONTRIBUTING.md
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.B7qtzLy9.lean.js b/assets/advanced_contributing.md.B7qtzLy9.lean.js new file mode 100644 index 00000000..7e7cbb79 --- /dev/null +++ b/assets/advanced_contributing.md.B7qtzLy9.lean.js @@ -0,0 +1 @@ +import{_ as e,c as a,a1 as o,o as r}from"./chunks/framework.C72X4JAr.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('TIP
Here need to improved.
This project is open source and we welcome your contributions!
Fork it!
Create your feature branch: git checkout -b my-new-feature
Commit your changes: git commit -am 'Add some feature'
Push to the branch: git push origin my-new-feature
Submit a pull request 😄
More details please refer to CONTRIBUTING.md
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.etIPthZe.js b/assets/advanced_develop.md.etIPthZe.js new file mode 100644 index 00000000..dcf77851 --- /dev/null +++ b/assets/advanced_develop.md.etIPthZe.js @@ -0,0 +1,5 @@ +import{_ as e,c as a,a1 as i,o as t}from"./chunks/framework.C72X4JAr.js";const k=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(`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.
import { Loader } from 'kotori-bot'
+
+const loader = new Loader()
+
+loader.run(true)
1.Clone the repository
git clone https://github.com/kotorijs/kotori
2.Install dependencies
pnpm install
3.Run
pnpm dev
Other scripts:
build
Build all packages at the workspacebuild: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
accesstest
Run all unit tests at the workspaceversion
Generate CHANGELOG.md
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.
import { Loader } from 'kotori-bot'
+
+const loader = new Loader()
+
+loader.run(true)
1.Clone the repository
git clone https://github.com/kotorijs/kotori
2.Install dependencies
pnpm install
3.Run
pnpm dev
Other scripts:
build
Build all packages at the workspacebuild: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
accesstest
Run all unit tests at the workspaceversion
Generate CHANGELOG.md
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.zK8oUFX9.lean.js b/assets/advanced_history.md.zK8oUFX9.lean.js new file mode 100644 index 00000000..ecb109ec --- /dev/null +++ b/assets/advanced_history.md.zK8oUFX9.lean.js @@ -0,0 +1 @@ +import{_ as e,c as a,a1 as o,o as r}from"./chunks/framework.C72X4JAr.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('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.C88PyWUF.js b/assets/advanced_index.md.C88PyWUF.js new file mode 100644 index 00000000..531c34dd --- /dev/null +++ b/assets/advanced_index.md.C88PyWUF.js @@ -0,0 +1 @@ +import{_ as a,c as o,a1 as r,o as t}from"./chunks/framework.C72X4JAr.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('TIP
Here need to improved.
⚡ 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.
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.
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.
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.C88PyWUF.lean.js b/assets/advanced_index.md.C88PyWUF.lean.js new file mode 100644 index 00000000..531c34dd --- /dev/null +++ b/assets/advanced_index.md.C88PyWUF.lean.js @@ -0,0 +1 @@ +import{_ as a,c as o,a1 as r,o as t}from"./chunks/framework.C72X4JAr.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('TIP
Here need to improved.
⚡ 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.
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.
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.
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.NjKEhMaf.js b/assets/advanced_testing.md.NjKEhMaf.js new file mode 100644 index 00000000..6802cd04 --- /dev/null +++ b/assets/advanced_testing.md.NjKEhMaf.js @@ -0,0 +1 @@ +import{_ as e,c as s,a1 as a,o as n}from"./chunks/framework.C72X4JAr.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('TIP
Here need to improved.
Kotori used Jest to implement unit tests.
To run the tests, run:
pnpm test
TIP
Here need to improved.
Kotori used Jest to implement unit tests.
To run the tests, run:
pnpm test
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.
Thanks, referred more and less them at kotori's whole developing process (the ranking is not in order):
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.rJMWayv0.lean.js b/assets/advanced_thanks.md.rJMWayv0.lean.js new file mode 100644 index 00000000..cad98640 --- /dev/null +++ b/assets/advanced_thanks.md.rJMWayv0.lean.js @@ -0,0 +1 @@ +import{_ as a,c as t,a1 as r,o as n}from"./chunks/framework.C72X4JAr.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('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.
Thanks, referred more and less them at kotori's whole developing process (the ranking is not in order):
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.BONjOQ33.js b/assets/api_index.md.BONjOQ33.js new file mode 100644 index 00000000..4eb5b20d --- /dev/null +++ b/assets/api_index.md.BONjOQ33.js @@ -0,0 +1 @@ +import{_ as r,c as t,a1 as o,o as a}from"./chunks/framework.C72X4JAr.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('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.
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.
前面一节已对 kotori.toml
有了大概认识,本节内容将更为全面的介绍它。kotori.toml
是一个 Kotori 程序的核心配置文件,它一般位于 Kotori 根目录,与 package.json 文件同级,使用 TOML 格式。
虽然默认使用的是 TOML 格式,但 Kotori 也支持 YAML 格式(.yaml
或 .yml
)与 JSON 格式,其它格式使用方法请参考下文
以下是将先前的配置片段集中在一起的例子(仅作参考请勿直接复制):
[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"
定义全局使用的语言,目前仅支持英语、日语、台湾语、中文四门语言。
LocaleType
,'en_US'
type LocaleType = 'en_US' | 'ja_JP' | 'zh_TW' | 'zh_CN';
定义全局使用的命令前缀。
'/'
定义需要加载的模块目录。
['./node_modules/', './node_modules/@kotori-bot/']
定义 Kotori 使用的端口(Http 服务器与 WebSocket 服务器)。
720
定义 Kotori 使用的数据库前缀。
'romiChan'
定义是否禁用彩色输出。
false
定义日志输出级别。
25
export enum LoggerLevel {
+ TRACE = 10,
+ DEBUG = 20,
+ RECORD = 25,
+ INFO = 30,
+ WARN = 40,
+ ERROR = 50,
+ FATAL = 60,
+ SILENT = 70
+}
定义 Bot。
{ [botName: string]: AdapterConfig }
{}
interface AdapterConfig {
+ extends: string;
+ master?: number;
+ lang?: langType;
+ commandPrefix?: string;
+ [propName: string]?: unknown;
+}
定义该 Bot 使用的适配器。
定义该 Bot 的最高管理员 id(即该用户在平台的 id)。
定义该 Bot 使用的语言。
LocaleType
,global.lang
定义该 Bot 使用的命令前缀。
global.commandPrefix
除去以上由 Kotori 内部定义的配置项,extends
中指定的适配器一般会额外定义配置项用于 Bot 内部,这些配置项也可能不存在或为可选,具体请参考该模块的详情页。
定义插件的配置项。
{ [pluginName: string]: PluginConfig }
{}
interface PluginConfig {
+ filter?: {};
+ [propName: string]?: unknown;
+}
定义该插件使用的滤器。
FilterOption
关于滤器使用请参考 滤器
类似于 AdapterConfig 中的 [propName]
,该插件也可能会定义一些配置项用于插件内部,具体请参考该模块的详情页。
Kotori 提供了一些 CLI 参数,用于在启动时修改配置文件中的配置项。
--daemon
:是否使用守护进程--mode [name]
:设置程序运行模式,build
或 dev
--dir [path]
:设置程序运行根目录--config [name]
:设置配置文件名--level [number]
:设置日志输出级别--port [number]
:设置服务器端口--dbPrefix [string]
:设置数据库前缀--noColor
:禁用彩色输出Kotori 提供了一些环境变量,用于在启动时修改配置文件中的配置项。
NODE_ENV
:设置程序运行模式,build
或 dev
DIR
:设置程序运行根目录CONFIG
:设置配置文件名PORT
:设置服务器端口DB_PREFIX
:设置数据库前缀LEVEL
:设置日志输出级别NO_COLOR
:禁用彩色输出DAEMON
:是否使用守护进程设置环境变量只需在运行根目录下创建 .env
文件,并以 KEY=VALUE
的形式写入即可,例如:
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=s(e,[["render",t]]);export{g as __pageData,c as default}; diff --git a/assets/basic_config.md.3Bp05E4J.lean.js b/assets/basic_config.md.3Bp05E4J.lean.js new file mode 100644 index 00000000..20ae9dbd --- /dev/null +++ b/assets/basic_config.md.3Bp05E4J.lean.js @@ -0,0 +1,55 @@ +import{_ as s,c as a,a1 as l,o as n}from"./chunks/framework.C72X4JAr.js";const g=JSON.parse('{"title":"配置详解","description":"","frontmatter":{},"headers":[],"relativePath":"basic/config.md","filePath":"basic/config.md","lastUpdated":1723345558000}'),e={name:"basic/config.md"};function t(p,i,h,k,o,d){return n(),a("div",null,i[0]||(i[0]=[l(`前面一节已对 kotori.toml
有了大概认识,本节内容将更为全面的介绍它。kotori.toml
是一个 Kotori 程序的核心配置文件,它一般位于 Kotori 根目录,与 package.json 文件同级,使用 TOML 格式。
虽然默认使用的是 TOML 格式,但 Kotori 也支持 YAML 格式(.yaml
或 .yml
)与 JSON 格式,其它格式使用方法请参考下文
以下是将先前的配置片段集中在一起的例子(仅作参考请勿直接复制):
[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"
定义全局使用的语言,目前仅支持英语、日语、台湾语、中文四门语言。
LocaleType
,'en_US'
type LocaleType = 'en_US' | 'ja_JP' | 'zh_TW' | 'zh_CN';
定义全局使用的命令前缀。
'/'
定义需要加载的模块目录。
['./node_modules/', './node_modules/@kotori-bot/']
定义 Kotori 使用的端口(Http 服务器与 WebSocket 服务器)。
720
定义 Kotori 使用的数据库前缀。
'romiChan'
定义是否禁用彩色输出。
false
定义日志输出级别。
25
export enum LoggerLevel {
+ TRACE = 10,
+ DEBUG = 20,
+ RECORD = 25,
+ INFO = 30,
+ WARN = 40,
+ ERROR = 50,
+ FATAL = 60,
+ SILENT = 70
+}
定义 Bot。
{ [botName: string]: AdapterConfig }
{}
interface AdapterConfig {
+ extends: string;
+ master?: number;
+ lang?: langType;
+ commandPrefix?: string;
+ [propName: string]?: unknown;
+}
定义该 Bot 使用的适配器。
定义该 Bot 的最高管理员 id(即该用户在平台的 id)。
定义该 Bot 使用的语言。
LocaleType
,global.lang
定义该 Bot 使用的命令前缀。
global.commandPrefix
除去以上由 Kotori 内部定义的配置项,extends
中指定的适配器一般会额外定义配置项用于 Bot 内部,这些配置项也可能不存在或为可选,具体请参考该模块的详情页。
定义插件的配置项。
{ [pluginName: string]: PluginConfig }
{}
interface PluginConfig {
+ filter?: {};
+ [propName: string]?: unknown;
+}
定义该插件使用的滤器。
FilterOption
关于滤器使用请参考 滤器
类似于 AdapterConfig 中的 [propName]
,该插件也可能会定义一些配置项用于插件内部,具体请参考该模块的详情页。
Kotori 提供了一些 CLI 参数,用于在启动时修改配置文件中的配置项。
--daemon
:是否使用守护进程--mode [name]
:设置程序运行模式,build
或 dev
--dir [path]
:设置程序运行根目录--config [name]
:设置配置文件名--level [number]
:设置日志输出级别--port [number]
:设置服务器端口--dbPrefix [string]
:设置数据库前缀--noColor
:禁用彩色输出Kotori 提供了一些环境变量,用于在启动时修改配置文件中的配置项。
NODE_ENV
:设置程序运行模式,build
或 dev
DIR
:设置程序运行根目录CONFIG
:设置配置文件名PORT
:设置服务器端口DB_PREFIX
:设置数据库前缀LEVEL
:设置日志输出级别NO_COLOR
:禁用彩色输出DAEMON
:是否使用守护进程设置环境变量只需在运行根目录下创建 .env
文件,并以 KEY=VALUE
的形式写入即可,例如:
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=s(e,[["render",t]]);export{g as __pageData,c as default}; diff --git a/assets/basic_index.md.LKLaNAWr.js b/assets/basic_index.md.LKLaNAWr.js new file mode 100644 index 00000000..a5ece795 --- /dev/null +++ b/assets/basic_index.md.LKLaNAWr.js @@ -0,0 +1 @@ +import{d as p,o,c as n,j as r,a as t,G as i,a1 as d}from"./chunks/framework.C72X4JAr.js";import{N as u}from"./chunks/NpmBadge.UqvZ4rBD.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('跨平台 得益于模块化支持,通过编写各种模块实现不同的功能与聊天平台接入
解耦合 基于控制反转(IOC)与面向切面编程(AOP)思想,减少代码冗余与复杂度
现代化 使用现代化的 ECMAScript 语法规范与强大的 TypeScript 类型支持
即将支持:
Kotori 使用极为轻量的 LevelDb 作为数据存储。
跨平台 得益于模块化支持,通过编写各种模块实现不同的功能与聊天平台接入
解耦合 基于控制反转(IOC)与面向切面编程(AOP)思想,减少代码冗余与复杂度
现代化 使用现代化的 ECMAScript 语法规范与强大的 TypeScript 类型支持
即将支持:
Kotori 使用极为轻量的 LevelDb 作为数据存储。
模块(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 根目录运行:
npm install @kotori-bot/kotori-plugin-adapter-qq
WARNING
该方法目前仅建议插件开发者在工作区下可适当使用。
在模块详情页里跳转至对应的 npm 地址或 GitHub 地址,下载模块的构建产物。 解压压缩包并移动至 Kotori 根目录下的 ./modules/
内。
GitHub 仓库中存有模块的源码,在当前阶段,你应下载并使用模块的构建产物而非源码
务必确保解压后的模块文件夹仅有一层文件夹而非多层,否则将无法识别与加载模块。
模块安装在
./modules
目录内请忽略该步骤
通过包管理工具安装的模块一般会安装在 Kotori 根目录下的 ./node_modules/
内,如若插件包名带有 @xxx/
的前缀,表示为包的命名空间,上述示例模块中的「@kotori-bot/」为 Kotori 官方包的命名空间,表示官方模块,其余的命名空间或无命名空间的模块为社区模块。
所有未安装在 ./modules/
都应配置 kotori.yml
的 global.dirs
项以设置额外的加载根目录,但对于 ./node_modules/
与 @kotori-bot/
命名空间已经存在于 Kotori.yml
默认配置中,因此无需担心。
对于其它安装目录或命名空间则需手动添加到 Kotori.yml
中,如:
@custom-scope/
的模块./test_modules/
内对应配置为:
[global]:
+dirs = [
+ "./node_modules/",
+ "./node_modules/@kotori-bot/",
+ # 上面为默认配置的加载目录
+ "./node_modules/@custom-scope/",
+ "- ./test_modules"
+]
根据安装的模块类型不同,配置策略也将不同。
插件配置数据应写在 kotori.yml
的 plugin.<plugin-name>
项下,其中 <plugin-name>
为插件名字,不应含有包的命名空间与模块前缀,值必须是一个对象。插件的配置项由插件本身提供与指定,并非所有插件本身都会提供配置项。一般地,有提供配置项的插件内都会有一套默认配置,因此不配置也可以正常运行插件。插件的配置和说明可参考该插件的详情页,此处以 「菜单插件」(@kotori-bot/kotori-plugin-menu)为例,在详情页查看配置说明后在 kotori.yml
中配置相关内容:
[plugin.menu]
+alias = "cd"
+keywords = [ "菜单", "功能", "帮助" ]
+content = "菜单 | 小鳥%break%/menu - 查看BOT菜单%break%/hitokoto - 获取一条一言%break%ByHotaru"
适配器配置数据应写在 kotori.yml
的 adapter[instanceName]
项下,其中 instanceName
为适配器实例(以下简称「Bot」)名字应由小写英语字母、数字、连字符([a-z0-9])组成,值必须是一个对象。适配器的配置数据不会作用于适配器模块,Kotori 会根据配置数据创建对应 Bot。对于适配器的配置,必须提供一些必要配置项才能确保实例的正常运行,其中有部分配置项由 Kotori 内部定义,如:
[adapter.cmd-test]
+extends = "cmd"
+master = 2_333
「cmd-test」是该 Bot 的名字也是在 kotori 程序运行中的唯一标识符,不可重复。extends
用于指定该实例使用的适配器,值为适配器模块的包名除去命名空间与适配器服务前缀的字符串,如:使用「@kotori-bot/kotori-plugin-adapter-qq」适配器,则应填入「qq」。master
用于指定该实例的最高管理员(Admin),值类型可为数字或字符串,非必填。 除去由 Kotori 内部定义的配置项以外,一般还需要填入该适配器要求传入的必要配置项。
[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:
[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 适配器的详情页面可知,appid
、 secret
为其定义的必要配置项,retry
为其定义的可选配置项,关于 QQ 适配器的具体使用与配置项含义请查看其插件详情页。
`,41)]))}const E=i(e,[["render",l]]);export{c as __pageData,E as default}; diff --git a/assets/basic_modules.md.DBZMOrr6.lean.js b/assets/basic_modules.md.DBZMOrr6.lean.js new file mode 100644 index 00000000..1beb88ab --- /dev/null +++ b/assets/basic_modules.md.DBZMOrr6.lean.js @@ -0,0 +1,32 @@ +import{_ as i,c as a,a1 as t,o as n}from"./chunks/framework.C72X4JAr.js";const c=JSON.parse('{"title":"模块安装","description":"","frontmatter":{},"headers":[],"relativePath":"basic/modules.md","filePath":"basic/modules.md","lastUpdated":1723293723000}'),e={name:"basic/modules.md"};function l(p,s,o,h,k,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 根目录运行:
npm install @kotori-bot/kotori-plugin-adapter-qq
WARNING
该方法目前仅建议插件开发者在工作区下可适当使用。
在模块详情页里跳转至对应的 npm 地址或 GitHub 地址,下载模块的构建产物。 解压压缩包并移动至 Kotori 根目录下的 ./modules/
内。
GitHub 仓库中存有模块的源码,在当前阶段,你应下载并使用模块的构建产物而非源码
务必确保解压后的模块文件夹仅有一层文件夹而非多层,否则将无法识别与加载模块。
模块安装在
./modules
目录内请忽略该步骤
通过包管理工具安装的模块一般会安装在 Kotori 根目录下的 ./node_modules/
内,如若插件包名带有 @xxx/
的前缀,表示为包的命名空间,上述示例模块中的「@kotori-bot/」为 Kotori 官方包的命名空间,表示官方模块,其余的命名空间或无命名空间的模块为社区模块。
所有未安装在 ./modules/
都应配置 kotori.yml
的 global.dirs
项以设置额外的加载根目录,但对于 ./node_modules/
与 @kotori-bot/
命名空间已经存在于 Kotori.yml
默认配置中,因此无需担心。
对于其它安装目录或命名空间则需手动添加到 Kotori.yml
中,如:
@custom-scope/
的模块./test_modules/
内对应配置为:
[global]:
+dirs = [
+ "./node_modules/",
+ "./node_modules/@kotori-bot/",
+ # 上面为默认配置的加载目录
+ "./node_modules/@custom-scope/",
+ "- ./test_modules"
+]
根据安装的模块类型不同,配置策略也将不同。
插件配置数据应写在 kotori.yml
的 plugin.<plugin-name>
项下,其中 <plugin-name>
为插件名字,不应含有包的命名空间与模块前缀,值必须是一个对象。插件的配置项由插件本身提供与指定,并非所有插件本身都会提供配置项。一般地,有提供配置项的插件内都会有一套默认配置,因此不配置也可以正常运行插件。插件的配置和说明可参考该插件的详情页,此处以 「菜单插件」(@kotori-bot/kotori-plugin-menu)为例,在详情页查看配置说明后在 kotori.yml
中配置相关内容:
[plugin.menu]
+alias = "cd"
+keywords = [ "菜单", "功能", "帮助" ]
+content = "菜单 | 小鳥%break%/menu - 查看BOT菜单%break%/hitokoto - 获取一条一言%break%ByHotaru"
适配器配置数据应写在 kotori.yml
的 adapter[instanceName]
项下,其中 instanceName
为适配器实例(以下简称「Bot」)名字应由小写英语字母、数字、连字符([a-z0-9])组成,值必须是一个对象。适配器的配置数据不会作用于适配器模块,Kotori 会根据配置数据创建对应 Bot。对于适配器的配置,必须提供一些必要配置项才能确保实例的正常运行,其中有部分配置项由 Kotori 内部定义,如:
[adapter.cmd-test]
+extends = "cmd"
+master = 2_333
「cmd-test」是该 Bot 的名字也是在 kotori 程序运行中的唯一标识符,不可重复。extends
用于指定该实例使用的适配器,值为适配器模块的包名除去命名空间与适配器服务前缀的字符串,如:使用「@kotori-bot/kotori-plugin-adapter-qq」适配器,则应填入「qq」。master
用于指定该实例的最高管理员(Admin),值类型可为数字或字符串,非必填。 除去由 Kotori 内部定义的配置项以外,一般还需要填入该适配器要求传入的必要配置项。
[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:
[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 适配器的详情页面可知,appid
、 secret
为其定义的必要配置项,retry
为其定义的可选配置项,关于 QQ 适配器的具体使用与配置项含义请查看其插件详情页。
`,41)]))}const E=i(e,[["render",l]]);export{c as __pageData,E as default}; diff --git a/assets/basic_start.md.C2FsgMGz.js b/assets/basic_start.md.C2FsgMGz.js new file mode 100644 index 00000000..9c817472 --- /dev/null +++ b/assets/basic_start.md.C2FsgMGz.js @@ -0,0 +1,17 @@ +import{_ as i,c as a,a1 as t,o as e}from"./chunks/framework.C72X4JAr.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,o,d){return e(),a("div",null,s[0]||(s[0]=[t(`关于配置文件的详细介绍请参考 配置详解
尽管 Kotori 的安装几乎已可以说是开箱即用的地步,只需要简单动用一下包管理器同时写入配置文件即可完成安装,但鉴于这仅仅安装了本体,并未附带一些基础且必要的模块,因此这里有一份来自于社区的 Kotori 手把手安装教程,该仓库提供了一个包含基础模块的 package.json
与 Kotori 配置文件以及启动脚本。
NOTE
如若视频加载不出来请考虑使用 VPN,当然这并不重要。
git clone https://github.com/kotorijs/kotori-app.git
1.安装 Node.js
2.安装项目依赖
支持 npm
、yarn
主流包管理器安装,pnpm
安装后可能存在启动问题,cnpm
、deno
、bun
未经测试,但理论仍可以进行安装并运行。先进入仓库根目录,输入以下任一命令安装:
npm install
如若出现安装失败问题则强制安装:
npm install --force
强制更新所有包:
npm update --force
kotori.toml
关于 kotori.toml 的详细介绍请参考 配置详解
在仓库根目录打开命令行输入:
npm exec kotori
当然,也可以直接使用仓库根目录下提供的两个启动文件:
start.bat
start.sh
启动完成后,在控制台内输入 /status
查看,输入 /help
查看帮助内容
启动 Kotori 后,会在控制台看到以下类似信息:
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
命令进行设置:
/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
当想重置用户名或密码时也可以输入以下指令:
/webui -R
+8/11 10:35:51 LOG (1372799) [cmd/cmd-test]: User 2333 exec command webui -R successfully
+KotoriO > Webui 账户数据已重置
适配器:用于对接各个聊天平台,比如说你要用 qq 即安装 qq 适配器即可
插件:扩展机器人的各种功能
此处收录了大部分的 Kotori 模块,选择自己喜欢的模块打开详情页查看说明,复制包名使用你的包管理器进行安装。以下指令会为你安装一些重要的基础插件:
一般地,在需要更新时使用以下命令即可进行更新:
npm update -f
此外,当变动较大时可重新重复上述安装步骤。
Kotori 内置了一个简易的交互式命令行功能,用于执行简单的操作,在根目录下输入以下命令:
npm exec kotori ui
程序运行中可能总会发生一些不会如用户所愿的错误和异常,为了避免这种情况,Kotori 提供了守护进程功能,当程序崩溃时,守护进程会自动重启程序,而在默认情况下(即程序处于 build
模式),守护进程是会自动启用的,想改变这一策略可更改环境变量或 CLI 参数,具体参见 配置详解。
24/8/11 10:35:56 INFO (1372340) : [Daemon] Starting...
当 Kotori 启动时出现以上消息则表明守护进程成功启用。同时在启用守护进程后,也可以通过指令 /restart
(来自「@kotori-bot/kotori-plugin-core」) 手动进行重启:
/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.C2FsgMGz.lean.js b/assets/basic_start.md.C2FsgMGz.lean.js new file mode 100644 index 00000000..9c817472 --- /dev/null +++ b/assets/basic_start.md.C2FsgMGz.lean.js @@ -0,0 +1,17 @@ +import{_ as i,c as a,a1 as t,o as e}from"./chunks/framework.C72X4JAr.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,o,d){return e(),a("div",null,s[0]||(s[0]=[t(`尽管 Kotori 的安装几乎已可以说是开箱即用的地步,只需要简单动用一下包管理器同时写入配置文件即可完成安装,但鉴于这仅仅安装了本体,并未附带一些基础且必要的模块,因此这里有一份来自于社区的 Kotori 手把手安装教程,该仓库提供了一个包含基础模块的 package.json
与 Kotori 配置文件以及启动脚本。
NOTE
如若视频加载不出来请考虑使用 VPN,当然这并不重要。
git clone https://github.com/kotorijs/kotori-app.git
1.安装 Node.js
2.安装项目依赖
支持 npm
、yarn
主流包管理器安装,pnpm
安装后可能存在启动问题,cnpm
、deno
、bun
未经测试,但理论仍可以进行安装并运行。先进入仓库根目录,输入以下任一命令安装:
npm install
如若出现安装失败问题则强制安装:
npm install --force
强制更新所有包:
npm update --force
kotori.toml
关于 kotori.toml 的详细介绍请参考 配置详解
在仓库根目录打开命令行输入:
npm exec kotori
当然,也可以直接使用仓库根目录下提供的两个启动文件:
start.bat
start.sh
启动完成后,在控制台内输入 /status
查看,输入 /help
查看帮助内容
启动 Kotori 后,会在控制台看到以下类似信息:
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
命令进行设置:
/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
当想重置用户名或密码时也可以输入以下指令:
/webui -R
+8/11 10:35:51 LOG (1372799) [cmd/cmd-test]: User 2333 exec command webui -R successfully
+KotoriO > Webui 账户数据已重置
适配器:用于对接各个聊天平台,比如说你要用 qq 即安装 qq 适配器即可
插件:扩展机器人的各种功能
此处收录了大部分的 Kotori 模块,选择自己喜欢的模块打开详情页查看说明,复制包名使用你的包管理器进行安装。以下指令会为你安装一些重要的基础插件:
一般地,在需要更新时使用以下命令即可进行更新:
npm update -f
此外,当变动较大时可重新重复上述安装步骤。
Kotori 内置了一个简易的交互式命令行功能,用于执行简单的操作,在根目录下输入以下命令:
npm exec kotori ui
程序运行中可能总会发生一些不会如用户所愿的错误和异常,为了避免这种情况,Kotori 提供了守护进程功能,当程序崩溃时,守护进程会自动重启程序,而在默认情况下(即程序处于 build
模式),守护进程是会自动启用的,想改变这一策略可更改环境变量或 CLI 参数,具体参见 配置详解。
24/8/11 10:35:56 INFO (1372340) : [Daemon] Starting...
当 Kotori 启动时出现以上消息则表明守护进程成功启用。同时在启用守护进程后,也可以通过指令 /restart
(来自「@kotori-bot/kotori-plugin-core」) 手动进行重启:
/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.B7D_3hMk.js b/assets/basic_usage.md.B7D_3hMk.js new file mode 100644 index 00000000..b00ff4f1 --- /dev/null +++ b/assets/basic_usage.md.B7D_3hMk.js @@ -0,0 +1,45 @@ +import{_ as a,c as s,a1 as l,o as t}from"./chunks/framework.C72X4JAr.js";const c=JSON.parse('{"title":"立即使用","description":"","frontmatter":{},"headers":[],"relativePath":"basic/usage.md","filePath":"basic/usage.md","lastUpdated":1723293723000}'),n={name:"basic/usage.md"};function e(o,i,h,p,k,r){return t(),s("div",null,i[0]||(i[0]=[l(`TIP
本篇适用于想立即体验基于 Kotori 搭建的 Bot 实际效果、或单纯想使用由官方提供的各平台 Bot 服务的平台用户。
目前共提供 2 个平台、四个 Bot:
基于 OneBot11 标准的第三方 QQ 机器人(@Kotori-bot/kotori-plugin-adapter-onebot)。
立即使用:Kotori 交流群
基于 Tencent 官方 API(@Kotori-bot/kotori-plugin-adapter-qq)。相比于第三方 QQ 机器人,限制较多(不可发送主动消息、URL 需备案等),其余功能与「小鳥二号」基本一致。
立即使用:Kotori 交流群
基于 OneBot11 标准的第三方 QQ 机器人。
立即使用:Kotori 交流群
基于 @kotori-bot/adapter-telegram 的 Telegram 机器人。
立即使用:@Sena0620Bot
WARNING
以下内容已严重过期
以下展示并非全部功能。
/core
查看实例统计信息/bot
查看当前 bot 信息与运行状态/bots
查看所有 bot 信息与运行状态/version
查看版本信息/about
帮助信息/update
检查更新/help [command]
查看指令帮助信息/menu
查看 BOT 菜单 别名:cd
/sex [tags]
Pixiv 图片/sexh
HuliImg 图片/bing
必应每日图/day
60s 带你看世界/earth
实时地球/china
实时中国/bgm <content> [order:number=1]
番组计划搜索游戏/动漫/bgmc
获取番组计划今日放送/bgm 素晴日
+> 原名:素晴らしき日々~不連続存在~公式ビジュアルアーカイヴ
+中文名:素晴之日 不连续的存在 Official Visual Archive
+介绍:人気アダルトゲームブランド「ケロQ」から、実に6年ぶりに発売された新作『素晴らしき日々 ~不連続存在~』。その魅力をギュッと閉じ込めたファン必携の一冊。描き下ろしイラスト&原作を担当したSCA-自(すかぢ)氏の新作書き下ろしテキスト満載でお届け。
+标签:素晴らしき日々 设定集 电波 神作 素晴日 公式书 2010 百合 FanBook 悬疑 画集 画集・設定資料集 推理 VFB
+详情:https://bgm.tv/subject/8318
+[image]
/bili <bvid>
Bilibili 视频信息查询/bilier <uid>
Bilibili 用户信息查询/hitokotos
随机语录/hitokoto
获取一条一言/hitokoto
+> 如果人们不相信数学简单,那是因为他们没有意识到人生有多复杂。——冯诺依曼
+类型:俗语
/wiki <content> [order]
搜索 MediaWiki/wikil
查看 MediaWiki 列表/wiki 月社妃
+> 标题:月社妃
+内容:月社妃(日语:月社(つきやしろ) 妃(きさき))是由ウグイスカグラ所制作的18禁galgame《纸上的魔法使》及其衍生作品的登场角色。是主人公四条琉璃的同胞妹妹。
+https://mzh.moegirl.org.cn/.php?curid=384932
+来源:萌娘百科
/github <repository>
查询 Github 仓库信息/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
/music <name> [order:number=1]
网易云点歌 序号默认为 1,填 0 显示歌曲列表/music 夢水の調べ
+> 歌曲ID:2077744375
+歌曲标题:夢水の調べ
+歌曲作者:おはる
+歌曲下载:http://music.163.com/song/media/outer/url?id=2077744375.mp3
+歌曲封面:[image]
/weather <area>
查询城市天气/weather 北京
+> 城市:北京市
+日期:周四
+温度:-6~4℃
+天气:晴
+风度:南风-2级
+空气质量:良
+
+日期:周五
+温度:-5~0℃
+天气:阴
+风度:东风-1级
+空气质量:良
+
+日期:周六
+温度:-8~0℃
+天气:阴
+风度:东北风-1级
+空气质量:良
TIP
本篇适用于想立即体验基于 Kotori 搭建的 Bot 实际效果、或单纯想使用由官方提供的各平台 Bot 服务的平台用户。
目前共提供 2 个平台、四个 Bot:
基于 OneBot11 标准的第三方 QQ 机器人(@Kotori-bot/kotori-plugin-adapter-onebot)。
立即使用:Kotori 交流群
基于 Tencent 官方 API(@Kotori-bot/kotori-plugin-adapter-qq)。相比于第三方 QQ 机器人,限制较多(不可发送主动消息、URL 需备案等),其余功能与「小鳥二号」基本一致。
立即使用:Kotori 交流群
基于 OneBot11 标准的第三方 QQ 机器人。
立即使用:Kotori 交流群
基于 @kotori-bot/adapter-telegram 的 Telegram 机器人。
立即使用:@Sena0620Bot
WARNING
以下内容已严重过期
以下展示并非全部功能。
/core
查看实例统计信息/bot
查看当前 bot 信息与运行状态/bots
查看所有 bot 信息与运行状态/version
查看版本信息/about
帮助信息/update
检查更新/help [command]
查看指令帮助信息/menu
查看 BOT 菜单 别名:cd
/sex [tags]
Pixiv 图片/sexh
HuliImg 图片/bing
必应每日图/day
60s 带你看世界/earth
实时地球/china
实时中国/bgm <content> [order:number=1]
番组计划搜索游戏/动漫/bgmc
获取番组计划今日放送/bgm 素晴日
+> 原名:素晴らしき日々~不連続存在~公式ビジュアルアーカイヴ
+中文名:素晴之日 不连续的存在 Official Visual Archive
+介绍:人気アダルトゲームブランド「ケロQ」から、実に6年ぶりに発売された新作『素晴らしき日々 ~不連続存在~』。その魅力をギュッと閉じ込めたファン必携の一冊。描き下ろしイラスト&原作を担当したSCA-自(すかぢ)氏の新作書き下ろしテキスト満載でお届け。
+标签:素晴らしき日々 设定集 电波 神作 素晴日 公式书 2010 百合 FanBook 悬疑 画集 画集・設定資料集 推理 VFB
+详情:https://bgm.tv/subject/8318
+[image]
/bili <bvid>
Bilibili 视频信息查询/bilier <uid>
Bilibili 用户信息查询/hitokotos
随机语录/hitokoto
获取一条一言/hitokoto
+> 如果人们不相信数学简单,那是因为他们没有意识到人生有多复杂。——冯诺依曼
+类型:俗语
/wiki <content> [order]
搜索 MediaWiki/wikil
查看 MediaWiki 列表/wiki 月社妃
+> 标题:月社妃
+内容:月社妃(日语:月社(つきやしろ) 妃(きさき))是由ウグイスカグラ所制作的18禁galgame《纸上的魔法使》及其衍生作品的登场角色。是主人公四条琉璃的同胞妹妹。
+https://mzh.moegirl.org.cn/.php?curid=384932
+来源:萌娘百科
/github <repository>
查询 Github 仓库信息/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
/music <name> [order:number=1]
网易云点歌 序号默认为 1,填 0 显示歌曲列表/music 夢水の調べ
+> 歌曲ID:2077744375
+歌曲标题:夢水の調べ
+歌曲作者:おはる
+歌曲下载:http://music.163.com/song/media/outer/url?id=2077744375.mp3
+歌曲封面:[image]
/weather <area>
查询城市天气/weather 北京
+> 城市:北京市
+日期:周四
+温度:-6~4℃
+天气:晴
+风度:南风-2级
+空气质量:良
+
+日期:周五
+温度:-5~0℃
+天气:阴
+风度:东风-1级
+空气质量:良
+
+日期:周六
+温度:-8~0℃
+天气:阴
+风度:东北风-1级
+空气质量:良
{const{slotScopeIds:I}=p;I&&(j=j?j.concat(I):I);const _=o(g),P=E(i(g),p,_,R,$,j,W);return P&&ln(P)&&P.data==="]"?i(p.anchor=P):(wt(),c(p.anchor=u("]"),_,P),P)},L=(g,p,R,$,j,W)=>{if(cn(g.parentElement,1)||wt(),p.el=null,W){const P=G(g);for(;;){const w=i(g);if(w&&w!==P)l(w);else break}}const I=i(g),_=o(g);return l(g),n(null,p,_,I,R,$,on(_),j),I},G=(g,p="[",R="]")=>{let $=0;for(;g;)if(g=i(g),g&&ln(g)&&(g.data===p&&$++,g.data===R)){if($===0)return i(g);$--}return g},B=(g,p,R)=>{const $=p.parentNode;$&&$.replaceChild(g,p);let j=R;for(;j;)j.vnode.el===p&&(j.vnode.el=j.subTree.el=g),j=j.parent},q=g=>g.nodeType===1&&g.tagName==="TEMPLATE";return[f,h]}const cr="data-allow-mismatch",$l={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function cn(e,t){if(t===0||t===1)for(;e&&!e.hasAttribute(cr);)e=e.parentElement;const n=e&&e.getAttribute(cr);if(n==null)return!1;if(n==="")return!0;{const s=n.split(",");return t===0&&s.includes("children")?!0:n.split(",").includes($l[t])}}const gt=e=>!!e.type.__asyncLoader,Fn=e=>e.type.__isKeepAlive;function Dl(e,t){Ii(e,"a",t)}function jl(e,t){Ii(e,"da",t)}function Ii(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(Hn(t,s,n),n){let r=n.parent;for(;r&&r.parent;)Fn(r.parent.vnode)&&Vl(s,t,n,r),r=r.parent}}function Vl(e,t,n,s){const r=Hn(t,e,s,!0);$n(()=>{Rs(s[t],r)},n)}function Hn(e,t,n=fe,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...o)=>{it();const l=Xt(n),c=Fe(t,n,e,o);return l(),ot(),c});return s?r.unshift(i):r.push(i),i}}const qe=e=>(t,n=fe)=>{(!Un||e==="sp")&&Hn(e,(...s)=>t(...s),n)},Ul=qe("bm"),It=qe("m"),Bl=qe("bu"),kl=qe("u"),Mi=qe("bum"),$n=qe("um"),Wl=qe("sp"),Kl=qe("rtg"),ql=qe("rtc");function Gl(e,t=fe){Hn("ec",e,t)}const Pi="components";function ff(e,t){return Ni(Pi,e,!0,t)||e}const Li=Symbol.for("v-ndc");function uf(e){return re(e)?Ni(Pi,e,!1)||e:e||Li}function Ni(e,t,n=!0,s=!1){const r=ye||fe;if(r){const i=r.type;{const l=Pc(i,!1);if(l&&(l===t||l===Ne(t)||l===An(Ne(t))))return i}const o=ar(r[e]||i[e],t)||ar(r.appContext[e],t);return!o&&s?i:o}}function ar(e,t){return e&&(e[t]||e[Ne(t)]||e[An(Ne(t))])}function df(e,t,n,s){let r;const i=n,o=U(e);if(o||re(e)){const l=o&&pt(e);let c=!1;l&&(c=!Me(e),e=On(e)),r=new Array(e.length);for(let u=0,f=e.length;u 在上一节中学习了事件系统的使用,现在通过 当收到「/echo xxx」消息时将发送「xxx」;当收到「/time」消息时将发送当前时间戳;两者都不是时发送「未知的指令」。然而当结果越来越多后, 指令(Command) 是 Kotori 的核心功能,也是最常见的交互方式,指令实质是 Kotori 内部对 上述演示了指令模板字符的基本格式。 通过 通过 回调函数中的第二个参数为当前会话事件数据 通过 通过 通过 通过 在上述众多演示中,可能你已注意到,与事件系统不同,指令的回调函数可以直接返回一个值作为消息发出,而不必使用 对于返回数组的情况设计国际化相关内容,将在第三章中讲解 试想一下,有一个指令 但是,其需要判断 使用子指令实现 上一节的会话事件部分和本节中均提到了会话事件数据 通过上述代码可知: 在上一节已提到 关于 目前 Kotori 原生提供了两个会话交互方法: WARNING 一次性有多个会话交互(消息、输入、确认...)时请注意不用遗漏 NOTE 目前会话交互功能甚少,内容也不全面,如对 i18n 支持不够完善、需手动进行数据校验、Promise 超时等问题,如有能力欢迎你前来帮助 Kotori 完善。 随着功能的不断增多,不稳定性也随之增多,面对用户传入的各种奇怪数据,虽有着 Kotori 本身的指令参数和数据校验用于防护,但这并不能百分百避免所有错误发生,因此学会自行错误处理至关重要。以下是 Kotori 内置的指令错误类型可供参考: Kotori 中指令指令错误分为两大类: 需要操心的是剩下可能发生在指令执行期间的错误,这些错误无法由 Kotori 处理,全需要你在编写代码时手动处理: 使用 上述代码展示了其非常经典的一个例子,机器人的功能往往部分来自于网络接口请求,确保其第三方内容的稳定性更是必要的,因此对获取的数据进行检查,然后再进行访问属性操作,如若获取的数据与预期不一致则使用 在上一节中学习了事件系统的使用,现在通过 当收到「/echo xxx」消息时将发送「xxx」;当收到「/time」消息时将发送当前时间戳;两者都不是时发送「未知的指令」。然而当结果越来越多后, 指令(Command) 是 Kotori 的核心功能,也是最常见的交互方式,指令实质是 Kotori 内部对 上述演示了指令模板字符的基本格式。 通过 通过 回调函数中的第二个参数为当前会话事件数据 通过 通过 通过 通过 在上述众多演示中,可能你已注意到,与事件系统不同,指令的回调函数可以直接返回一个值作为消息发出,而不必使用 对于返回数组的情况设计国际化相关内容,将在第三章中讲解 试想一下,有一个指令 但是,其需要判断 使用子指令实现 上一节的会话事件部分和本节中均提到了会话事件数据 通过上述代码可知: 在上一节已提到 关于 目前 Kotori 原生提供了两个会话交互方法: WARNING 一次性有多个会话交互(消息、输入、确认...)时请注意不用遗漏 NOTE 目前会话交互功能甚少,内容也不全面,如对 i18n 支持不够完善、需手动进行数据校验、Promise 超时等问题,如有能力欢迎你前来帮助 Kotori 完善。 随着功能的不断增多,不稳定性也随之增多,面对用户传入的各种奇怪数据,虽有着 Kotori 本身的指令参数和数据校验用于防护,但这并不能百分百避免所有错误发生,因此学会自行错误处理至关重要。以下是 Kotori 内置的指令错误类型可供参考: Kotori 中指令指令错误分为两大类: 需要操心的是剩下可能发生在指令执行期间的错误,这些错误无法由 Kotori 处理,全需要你在编写代码时手动处理: 使用 上述代码展示了其非常经典的一个例子,机器人的功能往往部分来自于网络接口请求,确保其第三方内容的稳定性更是必要的,因此对获取的数据进行检查,然后再进行访问属性操作,如若获取的数据与预期不一致则使用 事件系统(Events) 的上游是事件订阅者模式(Events Emiter),该设计模式与事件系统共同构成了 Kotori 的基础,Kotori 内部通过订阅事件保持各部分间的联系和协作任务。同时也有来自各个聊天平台的事件,通过订阅这些事件能实现丰富多样的功能。 事件系统的使用方法与常规的事件订阅者一致,通过 从上述代码中可以看出,当收到消息时,如果不是「你是谁」则立即退出,执行完毕。如果是则判断 id 一般为对应聊天平台提供的 id/uid,叫法不一,值类型为 string 或 number。如当你收到由适配器 @kotori-bot/kotori-plugin-adapter-onebot 发出的消息时, 上面的代码每次都需要判断消息类型再执行相应方法,显得有点繁琐,因此 kotori 提供了一个语法糖: 使用 正如订阅事件是「on」,取消订阅事件则是「off」。 上述代码中,触发事件后会立即取消订阅事件,意味着它只会被触发一次。 使用 工作流程与上面一致,通过 使用 在第三个回调函数中,当收到消息「消失吧!」时将取消订阅所有 Kotori 中事件类型大致分为三类: 常见的系统事件有: 通过 由于 常见的会话事件有: 通过 其中 得益于 TypeScript 有着 声明合并(Declaration Merging) 的特性,在模块中可通过其实现自定义事件的局部声明。 Kotori 中所有事件均定义在 然而,订阅事件后,事件却从来没有发出,因此需要发出事件: TIP 一般地,自定义事件应只用于单个模块内部,用于多个模块间相互通信传输数据时,每个涉及模块应先加载定义自定义事件的模块,以免出现类型定义的问题。 事件系统(Events) 的上游是事件订阅者模式(Events Emiter),该设计模式与事件系统共同构成了 Kotori 的基础,Kotori 内部通过订阅事件保持各部分间的联系和协作任务。同时也有来自各个聊天平台的事件,通过订阅这些事件能实现丰富多样的功能。 事件系统的使用方法与常规的事件订阅者一致,通过 从上述代码中可以看出,当收到消息时,如果不是「你是谁」则立即退出,执行完毕。如果是则判断 id 一般为对应聊天平台提供的 id/uid,叫法不一,值类型为 string 或 number。如当你收到由适配器 @kotori-bot/kotori-plugin-adapter-onebot 发出的消息时, 上面的代码每次都需要判断消息类型再执行相应方法,显得有点繁琐,因此 kotori 提供了一个语法糖: 使用 正如订阅事件是「on」,取消订阅事件则是「off」。 上述代码中,触发事件后会立即取消订阅事件,意味着它只会被触发一次。 使用 工作流程与上面一致,通过 使用 在第三个回调函数中,当收到消息「消失吧!」时将取消订阅所有 Kotori 中事件类型大致分为三类: 常见的系统事件有: 通过 由于 常见的会话事件有: 通过 其中 得益于 TypeScript 有着 声明合并(Declaration Merging) 的特性,在模块中可通过其实现自定义事件的局部声明。 Kotori 中所有事件均定义在 然而,订阅事件后,事件却从来没有发出,因此需要发出事件: TIP 一般地,自定义事件应只用于单个模块内部,用于多个模块间相互通信传输数据时,每个涉及模块应先加载定义自定义事件的模块,以免出现类型定义的问题。 中间件(Middleware) 是 Kotori 中另一种监听消息事件的语法糖,与指令系统类似,它也是对 中间件的工作原理与 Express 等后端框架中的中间件概念基本一致。每次收到消息时,Kotori 会依次执行所有已注册的中间件,只有当所有中间件都通过时,该消息事件才会真正被处理。 通过 优先级数字越小(但不能为负数)则优先级越高,如果两个中间件的优先级相同,则按照注册顺序执行,先注册的中间件会先执行。 WARNING 如无特殊需求建议请勿更改优先级,否则可能会导致一些意料之外的问题。 中间件回调函数接收两个参数: 在中间件内部,你可以根据消息内容或发送者等信息决定是否调用 上述代码注册了两个中间件,每当收到一条消息时,它们都会被执行。第一个中间件只打印日志,第二个则先打印日志,然后发送一条消息。由于两个中间件都调用了 这个示例中的中间件会过滤掉消息内容不是「hello」的消息事件。只有当消息是「hello」时,中间件才会调用 WARNING 以下内容由 Claude3 生成,不保证可用性。 有时我们需要限制某些命令的使用频率,以防止被滥用。这时可以使用中间件来实现这一功能。 上述代码定义了一个中间件,用于限制命令的使用频率。具体逻辑如下: 该中间件的优先级设为 10,这是为了让它能够比大多数命令优先执行。我们使用 通过这种方式,我们可以精确地控制每个用户对每个命令的使用频率,并且只对命令消息生效,不会影响到其他普通消息的处理。需要注意的是,这个示例使用了内存来存储命令使用记录,因此在重启 Bot 后记录会被清空。在实际应用中,你可以将记录持久化存储到数据库中。 中间件(Middleware) 是 Kotori 中另一种监听消息事件的语法糖,与指令系统类似,它也是对 中间件的工作原理与 Express 等后端框架中的中间件概念基本一致。每次收到消息时,Kotori 会依次执行所有已注册的中间件,只有当所有中间件都通过时,该消息事件才会真正被处理。 通过 优先级数字越小(但不能为负数)则优先级越高,如果两个中间件的优先级相同,则按照注册顺序执行,先注册的中间件会先执行。 WARNING 如无特殊需求建议请勿更改优先级,否则可能会导致一些意料之外的问题。 中间件回调函数接收两个参数: 在中间件内部,你可以根据消息内容或发送者等信息决定是否调用 上述代码注册了两个中间件,每当收到一条消息时,它们都会被执行。第一个中间件只打印日志,第二个则先打印日志,然后发送一条消息。由于两个中间件都调用了 这个示例中的中间件会过滤掉消息内容不是「hello」的消息事件。只有当消息是「hello」时,中间件才会调用 WARNING 以下内容由 Claude3 生成,不保证可用性。 有时我们需要限制某些命令的使用频率,以防止被滥用。这时可以使用中间件来实现这一功能。 上述代码定义了一个中间件,用于限制命令的使用频率。具体逻辑如下: 该中间件的优先级设为 10,这是为了让它能够比大多数命令优先执行。我们使用 通过这种方式,我们可以精确地控制每个用户对每个命令的使用频率,并且只对命令消息生效,不会影响到其他普通消息的处理。需要注意的是,这个示例使用了内存来存储命令使用记录,因此在重启 Bot 后记录会被清空。在实际应用中,你可以将记录持久化存储到数据库中。 正则匹配(RegExp) 同样是 Kotori 中一种监听消息事件的语法糖。它的主要用途是通过正则表达式匹配消息内容,然后执行相应的处理逻辑。值得一提的是,正则匹配位于消息事件的最后一环(在中间件和指令之后执行),这意味着只有通过了所有中间件和指令的消息,才会进入正则匹配的环节。 正则匹配依赖于正则表达式的强大功能,可以实现多种匹配模式,例如完全匹配、模糊匹配等,为消息处理提供了更大的灵活性。 通过 上述代码注册了一个正则匹配,当收到消息内容为 在回调函数中,你可以根据匹配结果执行相应的逻辑,回调函数的返回值将作为消息发送的内容。 上述代码注册了一个正则匹配,用于实现 「/echo」 命令。当收到类似 「/echo 你好」 的消息时,正则会匹配到 这个例子演示了一个稍微复杂的正则匹配。正则 在回调函数中,我们通过数组解构拿到匹配的学科和分数,然后根据不同的学科返回对应的消息内容。 这个例子展示了如何使用正则进行模糊匹配。正则 这个例子展示了如何在一个正则匹配中处理多个匹配情况。正则 在回调函数中,我们根据匹配的运算符和操作数进行相应的计算,并将结果作为消息发送出去。需要注意的是,这里我们使用了可选的捕获组,因此在处理单个操作数的情况时需要进行判断。 通过正则匹配的强大功能,我们可以灵活地处理各种复杂的消息,实现个性化的交互体验。而将正则匹配与其他功能(如指令系统、数据持久化等)相结合,就能构建出更加强大的应用程序。 正则匹配(RegExp) 同样是 Kotori 中一种监听消息事件的语法糖。它的主要用途是通过正则表达式匹配消息内容,然后执行相应的处理逻辑。值得一提的是,正则匹配位于消息事件的最后一环(在中间件和指令之后执行),这意味着只有通过了所有中间件和指令的消息,才会进入正则匹配的环节。 正则匹配依赖于正则表达式的强大功能,可以实现多种匹配模式,例如完全匹配、模糊匹配等,为消息处理提供了更大的灵活性。 通过 上述代码注册了一个正则匹配,当收到消息内容为 在回调函数中,你可以根据匹配结果执行相应的逻辑,回调函数的返回值将作为消息发送的内容。 上述代码注册了一个正则匹配,用于实现 「/echo」 命令。当收到类似 「/echo 你好」 的消息时,正则会匹配到 这个例子演示了一个稍微复杂的正则匹配。正则 在回调函数中,我们通过数组解构拿到匹配的学科和分数,然后根据不同的学科返回对应的消息内容。 这个例子展示了如何使用正则进行模糊匹配。正则 这个例子展示了如何在一个正则匹配中处理多个匹配情况。正则 在回调函数中,我们根据匹配的运算符和操作数进行相应的计算,并将结果作为消息发送出去。需要注意的是,这里我们使用了可选的捕获组,因此在处理单个操作数的情况时需要进行判断。 通过正则匹配的强大功能,我们可以灵活地处理各种复杂的消息,实现个性化的交互体验。而将正则匹配与其他功能(如指令系统、数据持久化等)相结合,就能构建出更加强大的应用程序。 IMPORTANT 阅读本章前请确保你已阅读完毕 入门教程。 WARNING 虽然目前开发文档已涵盖大部分基础内容,但在 v1.6 版本中刚加入的不少新特性并未在文档中更新。 Kotori 运行于 Node.js 环境,因此开发 Kotori 模块前掌握 JavaScript 与 Node.js 基础内容是必然的。此处推荐几个文档: 基于 TypeScript 与现代化 ECMAScript 开发。 TypeScript 是 JavaScript 的超集,TypeScript 在继承了 JavaScript 全部特性的同时,为弱类型动态语言的 JavaScript 提供了一个独立且强大的类型系统。同时,使用 TypeScript 基本意味着使用 ESModule 与现代化的 JavaScript 语法与规范,这是 Kotori 三大特点之一。理论上在 Kotori 程序的生产环境中可正常运行由 JavaScript 直接编写的模块,但 Kotori 本身便使用 TypeScript 开发,因此更推荐你使用 TypeScript 用于你的模块开发,尽管这并不是必须的。 IMPORTANT 阅读本章前请确保你已阅读完毕 入门教程。 WARNING 虽然目前开发文档已涵盖大部分基础内容,但在 v1.6 版本中刚加入的不少新特性并未在文档中更新。 Kotori 运行于 Node.js 环境,因此开发 Kotori 模块前掌握 JavaScript 与 Node.js 基础内容是必然的。此处推荐几个文档: 基于 TypeScript 与现代化 ECMAScript 开发。 TypeScript 是 JavaScript 的超集,TypeScript 在继承了 JavaScript 全部特性的同时,为弱类型动态语言的 JavaScript 提供了一个独立且强大的类型系统。同时,使用 TypeScript 基本意味着使用 ESModule 与现代化的 JavaScript 语法与规范,这是 Kotori 三大特点之一。理论上在 Kotori 程序的生产环境中可正常运行由 JavaScript 直接编写的模块,但 Kotori 本身便使用 TypeScript 开发,因此更推荐你使用 TypeScript 用于你的模块开发,尽管这并不是必须的。 上下文(Context) 是整个 Kotori 的核心机制,不仅是 Kotori 模块围绕着上下文实例实现一系列功能,即便是在 Kotori 内部也依赖于上下文实现各组件之间的通信与解耦合,同时也为 Kotori 的扩展提供了可能。犹如一个树根,Kotori 本身在内的各种内容均为其枝干,并通过不同的组合丰富枝干上枝叶的内容,上下文机制充分体现了**依赖注入(Dependency Injection)和面向切面编程(Aspect Oriented Programming)**的思想。 上下文实例中包含诸多属性和方法,但绝大部分功能并非来源于上下文本身,而是来源于 Kotori 内部的其它组件。通过 无论是对象字面量还是实例对象,都可以作为上下文实例的提供者。请注意,此处所有对象均是直接引用并未进行深拷贝。 使用 除了注入外,当只期望目标对象的部分属性或方法被装饰到上下文实例中时,可使用 相比注入,混合更加颗粒化同时减去不必要的属性访问。无论是注入还是混合,都并非直接对对象进行复制或建立新引用,其通过代理控制对象的每个属性或方法的操作,以便解决在混合后,原对象中 this 指向等问题。 对于上面的演示代码,还可以进一步做一些对开发者友好的工作,凭借 TypeScript 中声明合并的特性,为开发者提供良好的代码补全提示。 此外,通过代理得以实现父子级上下文的概念。如,Kotori 直接给与每个模块的执行主体的上下文实例均为独一无二,它是 Kotori 内部中根上下文实例的子级上下文实例,此外也有部分上下文实例是孙级上下文实例或更深。当访问上下文中的属性或方法时,若当前上下文实例中不存在,则会沿着继承链向上查找,直到根上下文为止,这点与 JavaScript 中原型链的查找方式类似,但原理不同。使用 可见,上下文继承后具有相对隔离性,对于子级上下文来说,只能访问自己父级上下文中注册的对象(即便是在自己被继承后注册的),而不能访问非自己父级上下文和其它子级上下文中注册的对象。而父级上下文也只能往上获取,无法往下获取自己子级上下文单独注册的对象。 在继承时,可传入两个可选参数用于标记新的子级上下文实例,第一个参数类型为对象,作用效果类似于将对象注册后并将对象上所有属性执行 通过 以上内容均由最初的 其二便是插件系统,它定义了 通过 导出对象形式与模块入口文件的导出是一致的。在 Kotori 内部,由加载器自动加载所有的模块入口文件进行预处理,然后转接给此处的 子插件与当前模块的上下文实例完全独立,具有隔离性,由此可通过这一点做一些需要隔离的操作: 上述代码加载了一个依赖 当然你也可以指定多个函数主体,这将会验证上一节所讲的执行主体的识别顺序,因此这只会执行其中一个: 此外,也可以外层调用 CommonJS 规范的 [!WARN] 请慎重并正确使用该操作,绝对不可直接导入 因 Kotori 运行模式不同,直接导入带后缀的路径并不可取。在开发模式中,Kotori v1.5.0 及以上版本通过 tsx 运行,同时支持 TS/JS 文件,在 v1.5.0 以下版本通过 ts-node 运行,仅支持 TS 文件;在生产模式中,通过 Node.js 运行,仅支持 JS 文件。因此,为使你的模块更加坚固,考虑并适配不同情况是必要的。在上述代码中,通过上下文实例获取到当前运行模式以返回不同的文件扩展名动态导入,但这并不完全可靠和优雅。 在这一版中,通过改变文件目录结构并利用入口文件特性,以直接减少代码中多余的判断逻辑,并且通过 上下文(Context) 是整个 Kotori 的核心机制,不仅是 Kotori 模块围绕着上下文实例实现一系列功能,即便是在 Kotori 内部也依赖于上下文实现各组件之间的通信与解耦合,同时也为 Kotori 的扩展提供了可能。犹如一个树根,Kotori 本身在内的各种内容均为其枝干,并通过不同的组合丰富枝干上枝叶的内容,上下文机制充分体现了**依赖注入(Dependency Injection)和面向切面编程(Aspect Oriented Programming)**的思想。 上下文实例中包含诸多属性和方法,但绝大部分功能并非来源于上下文本身,而是来源于 Kotori 内部的其它组件。通过 无论是对象字面量还是实例对象,都可以作为上下文实例的提供者。请注意,此处所有对象均是直接引用并未进行深拷贝。 使用 除了注入外,当只期望目标对象的部分属性或方法被装饰到上下文实例中时,可使用 相比注入,混合更加颗粒化同时减去不必要的属性访问。无论是注入还是混合,都并非直接对对象进行复制或建立新引用,其通过代理控制对象的每个属性或方法的操作,以便解决在混合后,原对象中 this 指向等问题。 对于上面的演示代码,还可以进一步做一些对开发者友好的工作,凭借 TypeScript 中声明合并的特性,为开发者提供良好的代码补全提示。 此外,通过代理得以实现父子级上下文的概念。如,Kotori 直接给与每个模块的执行主体的上下文实例均为独一无二,它是 Kotori 内部中根上下文实例的子级上下文实例,此外也有部分上下文实例是孙级上下文实例或更深。当访问上下文中的属性或方法时,若当前上下文实例中不存在,则会沿着继承链向上查找,直到根上下文为止,这点与 JavaScript 中原型链的查找方式类似,但原理不同。使用 可见,上下文继承后具有相对隔离性,对于子级上下文来说,只能访问自己父级上下文中注册的对象(即便是在自己被继承后注册的),而不能访问非自己父级上下文和其它子级上下文中注册的对象。而父级上下文也只能往上获取,无法往下获取自己子级上下文单独注册的对象。 在继承时,可传入两个可选参数用于标记新的子级上下文实例,第一个参数类型为对象,作用效果类似于将对象注册后并将对象上所有属性执行 通过 以上内容均由最初的 其二便是插件系统,它定义了 通过 导出对象形式与模块入口文件的导出是一致的。在 Kotori 内部,由加载器自动加载所有的模块入口文件进行预处理,然后转接给此处的 子插件与当前模块的上下文实例完全独立,具有隔离性,由此可通过这一点做一些需要隔离的操作: 上述代码加载了一个依赖 当然你也可以指定多个函数主体,这将会验证上一节所讲的执行主体的识别顺序,因此这只会执行其中一个: 此外,也可以外层调用 CommonJS 规范的 [!WARN] 请慎重并正确使用该操作,绝对不可直接导入 因 Kotori 运行模式不同,直接导入带后缀的路径并不可取。在开发模式中,Kotori v1.5.0 及以上版本通过 tsx 运行,同时支持 TS/JS 文件,在 v1.5.0 以下版本通过 ts-node 运行,仅支持 TS 文件;在生产模式中,通过 Node.js 运行,仅支持 JS 文件。因此,为使你的模块更加坚固,考虑并适配不同情况是必要的。在上述代码中,通过上下文实例获取到当前运行模式以返回不同的文件扩展名动态导入,但这并不完全可靠和优雅。 在这一版中,通过改变文件目录结构并利用入口文件特性,以直接减少代码中多余的判断逻辑,并且通过 恭喜你,只要学习完本章你将成为一名合格的「Kotori Developer」!在本章将围绕 Kotori 中最重要的概念「上下文」为你讲解一系列模块化内容。 插件(Plugin) 是 Kotori 中的最小运行实例,它是模块的真子集,在真正学习到上下文之前,可暂且默认插件等同于模块。在第一章里你已通过 Cli 初步创建了一个 Kotori 模块工程,但那并不是最小的有效模块,现在,让一切重零开始。 这是一个最小且有效的 package.json 例子: TIP 请不要模仿,package.json 应附有更详尽的包信息。 一个对于 Kotori 而言合法的 package.json 的类型信息大概是这样子: 但仅以 TypeScript 形式展现并不够全面,因为除此之外 Kotori 对合法的 package.json 有以下特殊要求: 对于包名,除去普通模块以外,往往会有一些非强制性规范的特殊值: 在上面例子中,可能你已注意到除了常规的属性以外,还有一个为 一般地,使用 入口文件一般导出一个名为 国际化文件目录(一般为 此处在 在入口文件中导出一个 通过 通过 通过 Kotori 中大体上提供了三种额风格的模块范式: 整合一下上面写的所有代码: 你会发现,无论是当前还是以往的所有演示代码都使用的导出式风格,或许称不上是 Kotori 官方推荐的模块风格,但它一定是在 Web 生态中最经典的一种风格,无论是 Vue、React 等前端响应式框架还是 Webpack、Rollup、eslint、Vite 这种工具链的插件系统都清一色的使用类似的导出式风格。就新人而言,是很推荐使用这种方式的,因为它很容易上手。 导出式可细分成导出函数式和导出类式(这里的「导出」特指模块的执行主体),导出函数式相信你已见过太多演示就不再赘述。这里是一个与上面完全一致的导出类式示例: 在导出类式中,可同时在外部导出诸如 诚然,Kotori 目前对导出类式的支持并不全面,它看起来仅仅是将原本的导出函数替换成导出类后调用其构造函数,并未充分发挥类的特性,但如果你很喜欢面向对象编程,这或许还是很适合你的。不过有一点注意,为与函数区分,导出函数式的函数名使用 无论是导出函数还是导出类,均将其称之为「模块的执行主体」,当入口文件中需要导出的只有执行主体本身时,你大可使用默认导出,此时函数名或类名都无关紧要,如: 又或者是默认导出一个类: 对于执行主体的各种导出形式,以下是 Kotori 的识别顺序(一经识别成功将不再继续识别后续内容): 通过直接访问 将 Kotori 作为依赖开发请参考 深入了解 以上是一个简单的装饰器式示例,与导出式相比,它的风格截然不同,语法上它足够的优雅。模块自己主动创造全局唯一的实例对象 当然,这并不算在此展开详细介绍,它还需要你了解一点其它内容作为基础,因而它被放在本章最后一节进行具体讲述。 恭喜你,只要学习完本章你将成为一名合格的「Kotori Developer」!在本章将围绕 Kotori 中最重要的概念「上下文」为你讲解一系列模块化内容。 插件(Plugin) 是 Kotori 中的最小运行实例,它是模块的真子集,在真正学习到上下文之前,可暂且默认插件等同于模块。在第一章里你已通过 Cli 初步创建了一个 Kotori 模块工程,但那并不是最小的有效模块,现在,让一切重零开始。 这是一个最小且有效的 package.json 例子: TIP 请不要模仿,package.json 应附有更详尽的包信息。 一个对于 Kotori 而言合法的 package.json 的类型信息大概是这样子: 但仅以 TypeScript 形式展现并不够全面,因为除此之外 Kotori 对合法的 package.json 有以下特殊要求: 对于包名,除去普通模块以外,往往会有一些非强制性规范的特殊值: 在上面例子中,可能你已注意到除了常规的属性以外,还有一个为 一般地,使用 入口文件一般导出一个名为 国际化文件目录(一般为 此处在 在入口文件中导出一个 通过 通过 通过 Kotori 中大体上提供了三种额风格的模块范式: 整合一下上面写的所有代码: 你会发现,无论是当前还是以往的所有演示代码都使用的导出式风格,或许称不上是 Kotori 官方推荐的模块风格,但它一定是在 Web 生态中最经典的一种风格,无论是 Vue、React 等前端响应式框架还是 Webpack、Rollup、eslint、Vite 这种工具链的插件系统都清一色的使用类似的导出式风格。就新人而言,是很推荐使用这种方式的,因为它很容易上手。 导出式可细分成导出函数式和导出类式(这里的「导出」特指模块的执行主体),导出函数式相信你已见过太多演示就不再赘述。这里是一个与上面完全一致的导出类式示例: 在导出类式中,可同时在外部导出诸如 诚然,Kotori 目前对导出类式的支持并不全面,它看起来仅仅是将原本的导出函数替换成导出类后调用其构造函数,并未充分发挥类的特性,但如果你很喜欢面向对象编程,这或许还是很适合你的。不过有一点注意,为与函数区分,导出函数式的函数名使用 无论是导出函数还是导出类,均将其称之为「模块的执行主体」,当入口文件中需要导出的只有执行主体本身时,你大可使用默认导出,此时函数名或类名都无关紧要,如: 又或者是默认导出一个类: 对于执行主体的各种导出形式,以下是 Kotori 的识别顺序(一经识别成功将不再继续识别后续内容): 通过直接访问 将 Kotori 作为依赖开发请参考 深入了解 以上是一个简单的装饰器式示例,与导出式相比,它的风格截然不同,语法上它足够的优雅。模块自己主动创造全局唯一的实例对象 当然,这并不算在此展开详细介绍,它还需要你了解一点其它内容作为基础,因而它被放在本章最后一节进行具体讲述。 配置检测(Schema) 是 Kotori 中的一个重要概念和功能,其相关的所有实现均来源于 Tsukiko 库。Kotori 对 Tsukiko 进行了重新导出,因此可直接在 Kotori 中使用。 Tsukiko 是一个基于 TypeScript 开发的运行时下动态类型检查库,最初作为 kotori 开发中的副产物诞生,其作用与应用场景类似于 io-ts 之类的库,常用于 JSON/YAML/TOML 文件数据格式检验、第三方 HTTP API 数据格式检验、数据库返回数据格式检验(尽管此处推荐直接用更为成熟的 ORM 框架)等。 视频介绍与演示:哔哩哔哩 项目名字取自于轻小说《変態王子と笑わない猫。》中的女主角——筒隠月子(Tsukakushi Tsukiko) Tsukiko 中带有多种类型解析器,通过不同的解析器可以实现对未知值的类型校验与处理: 该方法会返回一个对象,当 上面有提到, 在不同的解析器下也有一定的体现,如: 最典型的修饰方法为 同样是出于兼容性考虑,解析器默认会允许 除去以上所有解析器共有方法以外,每个解析器也有自己专门的修饰方法,详情可查看下文。可以看到,在 JSON Schema 是用于验证 JSON 数据结构的强大工具。在必要时可通过 配置检测(Schema) 是 Kotori 中的一个重要概念和功能,其相关的所有实现均来源于 Tsukiko 库。Kotori 对 Tsukiko 进行了重新导出,因此可直接在 Kotori 中使用。 Tsukiko 是一个基于 TypeScript 开发的运行时下动态类型检查库,最初作为 kotori 开发中的副产物诞生,其作用与应用场景类似于 io-ts 之类的库,常用于 JSON/YAML/TOML 文件数据格式检验、第三方 HTTP API 数据格式检验、数据库返回数据格式检验(尽管此处推荐直接用更为成熟的 ORM 框架)等。 视频介绍与演示:哔哩哔哩 项目名字取自于轻小说《変態王子と笑わない猫。》中的女主角——筒隠月子(Tsukakushi Tsukiko) Tsukiko 中带有多种类型解析器,通过不同的解析器可以实现对未知值的类型校验与处理: 该方法会返回一个对象,当 上面有提到, 在不同的解析器下也有一定的体现,如: 最典型的修饰方法为 同样是出于兼容性考虑,解析器默认会允许 除去以上所有解析器共有方法以外,每个解析器也有自己专门的修饰方法,详情可查看下文。可以看到,在 JSON Schema 是用于验证 JSON 数据结构的强大工具。在必要时可通过 在 使用指南 中你已安装并部署了 Node.js 环境与 pnpm,此处不再赘述。 Git 是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。版本控制可方便的实现协作开发、版本回退等,其重要性对每一位开发者都是不言而喻。GitHub 是一个面向开源及私有软件项目的托管平台,拥有着全球最大的开源社区,使用 Git 可轻松将你的项目推送至 GitHub 远程仓库,你与你的项目也将成为开源社区的一份子。Git 与 GitHub 具体使用流程此处不逐一赘述。 显然 Kotori 并不属于 Web 前端的范畴,但依旧隶属于 JavaScript 生态,因此推荐 在 使用指南 中你已安装并部署了 Node.js 环境与 pnpm,此处不再赘述。 Git 是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。版本控制可方便的实现协作开发、版本回退等,其重要性对每一位开发者都是不言而喻。GitHub 是一个面向开源及私有软件项目的托管平台,拥有着全球最大的开源社区,使用 Git 可轻松将你的项目推送至 GitHub 远程仓库,你与你的项目也将成为开源社区的一份子。Git 与 GitHub 具体使用流程此处不逐一赘述。 显然 Kotori 并不属于 Web 前端的范畴,但依旧隶属于 JavaScript 生态,因此推荐 当开发完毕模块后,可以将它发布至社区,一个 Kotori 模块一般会同时发布到如下三个平台: 优先级(重要程度):npm > 模块中心 > 开源社区。每一个公开的 Kotori 模块都应发布至 npm 并作为模块的主要获取途径。Kotori 使用 GPL-3.0 协议,该协议要求 Kotori 的所有模块及其二次开发项目也必须使用 GPL-3.0 协议且开源,因此发布到开源社区是必要的,开源行为本身也是一种无私奉献、共享知识和回馈社区的体现。 「构建产物」在 JavaScript 生态中指将源码(Kotori 模块开发中一般为 TypeScript 文件)进行处理以适用于生产环境中(处理过程一般有 TypeScript 转为 JavaScript、向下兼容语法、压缩代码等)。JavaScript 生态中构建工具非常多,你可以选择喜欢的构建工具并自习配置,当然如果你对此并不了解也可以使用 Kotori 默认的构建方式(通过TypeScript 自带的 tsc 程序),在你的模块根目录中输入以下指令: 一般地,你将会发现在模块根目录出现了一个 关于 对于模块发布主要分为发布构建产物(publish)与推送源码(push),两种情况下需要发布的文件内容会有些许不同,因此便引入了「文件忽略」。 用于指定在发布构建产物时忽略的文件与文件夹,在模块根目录创建一个 实际上在发布构建产物时只需要附带少数文件即可,而 在上一节的 不同于前两者, 使用工作区开发时,需确保当前为待发布模块根目录,而非工作区根目录。首先检查 npm 源是否为 前往 npmjs.org 注册账号,然后根据提示在浏览器内登录: 当一切就绪时: 当没有任何意外问题时,访问 npm 个人页即可查看刚才发布的插件: kotori-plugin-my-project。 使用 Git 前务必先配置好你的账号、邮箱和与 GitHub 通信的 ssh,可参考 手把手教你配置 git 和 git 仓库。使用工作区开发时,可选择发布整个工作区也可仅发布单个模块,切换到相应目录即可。首先在 GitHub New 页面创建一个远程仓库,接着在本地仓库中关联到该远程仓库: 提交并推送至远程仓库 当然,你也可以为本次提交添加一个 tag: 前往 Kotori Docs 仓库将其 fork 到你的账号名下,修改 fork 的仓库中的 完成文件更改后向源仓库发起 pull request 等待审定。所有新的 pull request 一般会在十二小时内审定完毕,当上述注意事项均无误时将会被合并到源仓库,届时你可在 Kotori 模块中心 查看你的模块。 当开发完毕模块后,可以将它发布至社区,一个 Kotori 模块一般会同时发布到如下三个平台: 优先级(重要程度):npm > 模块中心 > 开源社区。每一个公开的 Kotori 模块都应发布至 npm 并作为模块的主要获取途径。Kotori 使用 GPL-3.0 协议,该协议要求 Kotori 的所有模块及其二次开发项目也必须使用 GPL-3.0 协议且开源,因此发布到开源社区是必要的,开源行为本身也是一种无私奉献、共享知识和回馈社区的体现。 「构建产物」在 JavaScript 生态中指将源码(Kotori 模块开发中一般为 TypeScript 文件)进行处理以适用于生产环境中(处理过程一般有 TypeScript 转为 JavaScript、向下兼容语法、压缩代码等)。JavaScript 生态中构建工具非常多,你可以选择喜欢的构建工具并自习配置,当然如果你对此并不了解也可以使用 Kotori 默认的构建方式(通过TypeScript 自带的 tsc 程序),在你的模块根目录中输入以下指令: 一般地,你将会发现在模块根目录出现了一个 关于 对于模块发布主要分为发布构建产物(publish)与推送源码(push),两种情况下需要发布的文件内容会有些许不同,因此便引入了「文件忽略」。 用于指定在发布构建产物时忽略的文件与文件夹,在模块根目录创建一个 实际上在发布构建产物时只需要附带少数文件即可,而 在上一节的 不同于前两者, 使用工作区开发时,需确保当前为待发布模块根目录,而非工作区根目录。首先检查 npm 源是否为 前往 npmjs.org 注册账号,然后根据提示在浏览器内登录: 当一切就绪时: 当没有任何意外问题时,访问 npm 个人页即可查看刚才发布的插件: kotori-plugin-my-project。 使用 Git 前务必先配置好你的账号、邮箱和与 GitHub 通信的 ssh,可参考 手把手教你配置 git 和 git 仓库。使用工作区开发时,可选择发布整个工作区也可仅发布单个模块,切换到相应目录即可。首先在 GitHub New 页面创建一个远程仓库,接着在本地仓库中关联到该远程仓库: 提交并推送至远程仓库 当然,你也可以为本次提交添加一个 tag: 前往 Kotori Docs 仓库将其 fork 到你的账号名下,修改 fork 的仓库中的 完成文件更改后向源仓库发起 pull request 等待审定。所有新的 pull request 一般会在十二小时内审定完毕,当上述注意事项均无误时将会被合并到源仓库,届时你可在 Kotori 模块中心 查看你的模块。 在本阶段中开发模块一并通过搭建工作区开发,此外还可通过克隆 Kotori 源码或单独创建包进行开发,对于前者请参考 深入了解,对于后者则无需赘述。 「create-kotori」是专用于构建 Kotori 模块的 Cli 工具。 除此之外,也可以将其安装在全局使用: 以下为默认创建的 添加一些非必要配置项以完善包信息: 添加用于传给 Kotori 的元数据: 一个合法的 Kotori 模块其 关于 以下为默认创建的 index.ts,当前你还无需理解其具体含义: 在入门教程中提到过使用「@kotori-bot/kotori-plugin-adapter-cmd」适配器可以在命令行中测试指令,但命令行本身仅支持纯文字交互因此并不友好也不便于开发者调试。同样的,Kotori 已默认安装「@kotori-bot/kotori-plugin-adapter-sandbox」适配器,它提供了一个极为方便、全面的机器人沙盒测试环境,只需在 [!WARN] 以下内容有待更新 运行模式分为 「生产模式(Build)」与「开发模式(Dev)」两种: 从 Dev 模式下启动 Kotori: 在浏览器中打开 在本阶段中开发模块一并通过搭建工作区开发,此外还可通过克隆 Kotori 源码或单独创建包进行开发,对于前者请参考 深入了解,对于后者则无需赘述。 「create-kotori」是专用于构建 Kotori 模块的 Cli 工具。 除此之外,也可以将其安装在全局使用: 以下为默认创建的 添加一些非必要配置项以完善包信息: 添加用于传给 Kotori 的元数据: 一个合法的 Kotori 模块其 关于 以下为默认创建的 index.ts,当前你还无需理解其具体含义: 在入门教程中提到过使用「@kotori-bot/kotori-plugin-adapter-cmd」适配器可以在命令行中测试指令,但命令行本身仅支持纯文字交互因此并不友好也不便于开发者调试。同样的,Kotori 已默认安装「@kotori-bot/kotori-plugin-adapter-sandbox」适配器,它提供了一个极为方便、全面的机器人沙盒测试环境,只需在 [!WARN] 以下内容有待更新 运行模式分为 「生产模式(Build)」与「开发模式(Dev)」两种: 从 Dev 模式下启动 Kotori: 在浏览器中打开 指令注册
引入
on_message
事件实现一个小功能: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('未知的指令');
+ }
+});
if...else
语句也会越来越多,显然,这是十分糟糕的。尽管可以考虑将条件内容作为键、结果处理包装成回调函数作为值,以键值对形式装进一个对象或者 Map 中,然后遍历执行。但是当条件越来越复杂时,字符串的键远无法满足需求,同时也可能有相当一部分内容仅在私聊或者群聊下可用,其次,参数的处理也需要在结果处理内部中完成,这是十分复杂与繁琐的,因此便有入了本节内容。基本使用
on_message
事件的再处理与封装,这点与后续将学习的中间件和正则匹配是一致的,因此也可以看作是一个事件处理的语法糖。通过 ctx.command()
可注册一条指令,参数为指令模板字符,返回 Command
实例对象,实例上有着若干方法用于装饰该指令,其返回值同样为当前指令的实例对象。ctx.command('echo <...content>').action((data) => data.args.join(' '));
+
+ctx.command('time').action(() => {
+ const time = new Date().getTime();
+ return time;
+});
指令模板字符
ctx.command('bar');
+ctx.command('car <arg1> <arg2>');
+ctx.command('dar <arg1> [arg2] [arg3=value]');
+ctx.command('ear [arg1:number=1] [...args:string] - 指令描述');
<>
表示必要参数,方括号 []
为可选参数参数名:参数类型
,参数名应为小写字母与数字([a-z0-9])组成,参数类型可省略,默认 string
,支持的类型有: string
、number
、boolean
=值
设置默认参数...
设置剩余参数,与 TypeScript 不同的是,剩余参数的类型不需要加上数组表示- 指令描述
设置指令描述选项
Command.option()
设置指令选项,接受两个参数:ctx
+ .command('bar')
+ .option('S', 'speaker - 这是选项的描述')
+ .option('G', 'global:boolean - 这是一个布尔类型的选项')
+ .option('T', 'time:number - 这是一个数字类型的选项');
-
开头内容作为选项缩写名解析-
解析),解析器将把字符串中两个连接符 --
开头的内容作为选项全名解析- 指令描述
设置选项描述string
,支持的类型有: string
、number
、boolean
回调函数
Command.action()
设置指令的回调函数,且每个指令仅可设置一个回调函数,回调函数中接收两个参数:args
与 options
两个键组成的对象,类型分别为 (string | number | boolean)[]
与 Record<string, string | number | boolean>
,分别代表用户输入的参数值与选项值options
中的键为对应选项的全名而非缩写名。ctx.command('bar <...args>').action((data) => {
+ ctx.logger.info(data.args); // 输出参数值数组
+ ctx.logger.info(data.options); // 输出选项值对象
+ session.send('这是一条消息');
+});
session
。session
对象包含了当前指令触发产生的所有上下文信息,比如消息 id、消息类型、触发指令的账号、Bot 实例等,在处理函数中可以方便地与 Bot 进行交互。还可以从 session
中获取诸如发送消息等实用工具函数。下文将详细讲解 session
对象相关内容,此处仅做演示。ctx.command('at').action((_, session) => {
+ session.send(\`你好,\${session.el.at(session.userId)},你的名字是 \${session.sender.nickname}\`);
+});
作用域
Command.scope()
设置指令作用域,值类型为 MessageScope
,如若不设置则默认所有场景均可使用。export enum MessageScope {
+ PRIVATE, // 私聊
+ GROUP // 群聊
+}
ctx
+ .command('test')
+ .scope(MessageScope.PRIVATE)
+ .action(() => '这是一条仅私聊可用的消息');
+
+ctx
+ .command('hello')
+ .scope(MessageScope.GROUP)
+ .action((_, session) => {
+ session.send(\`这是一条仅群聊可用的消息\`);
+ });
别名
Command.alias()
设置指令别名,参数为 string | string[]
。ctx
+ .command('original')
+ .alias('o') // 别名可以是单个字符串
+ .alias(['ori', 'org']) // 也可以是字符串数组
+ .action(() => '这是原版指令');
权限
Command.access()
设置指令权限,值类型为 CommandAccess
。export const enum CommandAccess {
+ MEMBER, // 所有成员可用,默认值,权限最低
+ MANGER, // 管理员(群管理员/群主或 Bot 管理员)及以上权限可用
+ ADMIN // 仅该 Bot 最高管理员可用
+}
CommandAccess.ADMIN
对应 kotori.yml
中的 AdapterConfig.master
选项ctx
+ .command('op')
+ .access(CommandAccess.ADMIN)
+ .action(() => '这是一条特殊指令');
帮助信息
Command.help()
设置指令帮助信息,相对于指令模板字符中的指令描述,其提供更为详尽全面的信息。ctx.command('bar').help('这里是指令的帮助信息');
返回值处理
session.send()
方法。其本质上是自动将回调函数返回值作为参数传入 session.quick()
方法,具体处理逻辑请参考下文。ctx.command('concat <str1> <str2>').action(({ args: [str1, str2] }) => str1 + str2);
+
+ctx.command('render').action(() => ['template.text', { content: '这是模板内容' }]);
+
+ctx.command('fetch').action(async () => {
+ const res = await ctx.http.get('https://api.example.com');
+ return ['template.status', { status: res.status }];
+});
子指令
list
有着多个操作,如查询、添加、删除列表等,大可以使用多个完全独立的指令如 list_query
、list_add
、list_remove
,但这并不优雅。此处通过注册一个指令并判断其第一个参数的值执行相应操作/* 错误示例不要抄 */
+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
。因此,当同一指令有多个操作(即多个指令回调函数)且各个操作间相对独立时可使用子指令。基础用法: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
指令: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 中使用最广泛的功能且当前你已掌握事件系统的概念,会话事件数据的内容得以放在此处进行详细讲解。重要属性
export interface EventDataApiBase {
+ type?: MessageScope;
+ api: Api;
+ el: Elements;
+ userId: EventDataTargetId;
+ groupId?: EventDataTargetId;
+ operatorId?: EventDataTargetId;
+ i18n: I18n;
+ send(message: MessageRaw): void;
+ format(template: string, data: Record<string, unknown> | CommandArgType[]): string;
+ quick(message: MessageQuick): void;
+ prompt(message?: MessageRaw): Promise<MessageRaw>;
+ confirm(options?: { message: MessageRaw; sure: MessageRaw }): Promise<boolean>;
+ error<T extends Exclude<keyof CommandResult, CommandResultNoArgs>>(
+ type: T,
+ data: CommandResult[T] extends object ? CommandResult[T] : never
+ ): CommandError;
+ error<T extends CommandResultNoArgs>(type: T): CommandError;
+ extra?: unknown;
+}
session
对象本质上就是一个事件数据对象(即会话事件),上述是会话事件的共有属性,不同会话事件中有着不同的额外属性,如 EventDataGroupMsg
事件有 messageId
、sender
、message
、groupId
,而 EventDataPrivateMsg
事件没有 groupId
,EventDataPrivateRecall
事件其中的仅有 messageId
,这些额外属性均不在当前讨论范围内,具体内容参考接口文档。对于上述的共有属性在当前阶段也不必全部掌握。Api
实例对象,提供多个与当前聊天平台的交互接口Elements
实例对象,api.adapter.elements
属性的语法糖字符串处理
export type CommandArgType = string | number | boolean;
+type ObjectArgs = Record<string, CommandArgType>;
+type ArrayArgs = CommandArgType[];
session.format()
方法是一个简单的模板字符串替换工具(此处请区别于 JavaScript 中的 「模板字符串」)。接收两个参数:ObjectArgs
、ArrayArgs
。ctx.command('himeki').action((_, { format }) => {
+ format('名字:%name%\\n身高:%height%cm\\n口头禅:%msg%', {
+ name: 'Ichinose Himeki',
+ height: 153,
+ msg: '最喜欢你了,欧尼酱'
+ });
+ // 等同于:
+ format('名字:{0}\\n身高:{1}cm\\n口头禅:{2}', ['Ichinose Himeki', 153, '最喜欢你了,欧尼酱']);
+ // 最终输出:名字:Ichinose Himeki\\n年龄:153\\n口头禅:最喜欢你了,欧尼酱
+});
%key%
的形式进行替换,与对象键值一一对应,其更具有语义性,适合文本长且参数较多使用,但使用过多易造成代码冗余{index}
的形式进行替换,与数组索引一一对应,缺少语义性但更简洁,适合短文本使用,不易造成代码冗余session.format()
可实现较为复杂的动态数据展示,但不宜过多消息发送
session.send()
方法是对 session.api
上发送消息方法的封装,而 session.quick()
方法则是对 session.send()
的封装。一般地,在有会话事件数据可使用且无特殊需求下,均推荐使用 session.quick()
,后续所有代码演示无特殊情况也默认使用该方法。string
将调用 i18n.locale()
方法实现国际化[string, ObjectArgs | ArrayArgs]
参数,将先遍历数组中第二个值下的所有属性并调用 i18n.locale()
进行替换,然后将其传入 session.format()
方法undefined
、''
、void
、null
、0
则不作处理(一般不允许传入这些东西,主要发生在指令处理的回调函数返回值上)Error
则另作处理(一般不允许传入这些东西,主要发生在指令处理的回调函数返回值上)Promise
则等待 Promise 完成后再做上述处理i18n.locale()
方法当前可粗略理解为:传入一个已预定好且唯一的字符串值,根据当前使用语言返回相应语言文本。当然,不理解并不妨碍你使用 session.quick()
方法// locales/zh_CN.json
+{
+ "test.msg.himeki.hitokoto": "最喜欢你了,欧尼酱",
+ "test.msg.himeki": "名字:{0}\\n身高:{1}cm\\n口头禅:{2}"
+}
// 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']];
+ });
+}
会话交互
session.prompt()
与 session.confirm()
,它们和浏览器中的 prompt()
与 confirm()
类似,分别对应为输入框和提示框。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 '谢谢你的喜欢哦~';
+});
await
关键词,否则可能会有一些意料之外的效果。session.prompt()
参数为 string
,对应提示消息,返回 Promise<string>
session.confirm()
参数为 { message: string, sure: string }
,分别对应提示消息和确认消息(只有用户发送消息与确认消息完全一致时返回 true
反之 false
),返回 Promise<boolean>
错误处理
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'];
+}
CommandParseResult
,这些在指令系统不需要你操心,因为它们已全部交由上游的 Kotori 内置中间件进行处理,在解析指令时就会被发现Omit<CommandResult, keyof CommandParseResult>
,它们有的发生在指令执行前(如 no_access_manger
、no_access_admin
),又或者 error
这种错误之外的错误(执行回调函数时捕获的错误),这两者也不需要你操心data_error
参数错误(不同于参数类型错误)res_error
资源错误(主要是指网络请求第三方 Api 时返回数据类型有误)num_error
序号错误(主要是指需要用户传入数字进行选择的情况)exists
目标已存在(如添加目标到名单里但目标已存在于名单)no_exists
目标不存在(如删除目标从名单里但目标不存在于名单)session.error()
方法即可在运行时阶段抛出错误,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()
抛出错误
`,86)]))}const y=i(k,[["render",t]]);export{g as __pageData,y as default};
diff --git a/assets/guide_base_command.md.D7VFBgLc.lean.js b/assets/guide_base_command.md.D7VFBgLc.lean.js
new file mode 100644
index 00000000..588beb68
--- /dev/null
+++ b/assets/guide_base_command.md.D7VFBgLc.lean.js
@@ -0,0 +1,204 @@
+import{_ as i,c as a,a1 as n,o as h}from"./chunks/framework.C72X4JAr.js";const g=JSON.parse('{"title":"指令注册","description":"","frontmatter":{},"headers":[],"relativePath":"guide/base/command.md","filePath":"guide/base/command.md","lastUpdated":1723293723000}'),k={name:"guide/base/command.md"};function t(l,s,p,e,E,d){return h(),a("div",null,s[0]||(s[0]=[n(`ctx.http
是一个网络请求工具,基于 Axios 封装,具体内容参考接口文档;此处的「检查数据的操作」实际上指 Schema,这将在第三章中讲解指令注册
引入
on_message
事件实现一个小功能: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('未知的指令');
+ }
+});
if...else
语句也会越来越多,显然,这是十分糟糕的。尽管可以考虑将条件内容作为键、结果处理包装成回调函数作为值,以键值对形式装进一个对象或者 Map 中,然后遍历执行。但是当条件越来越复杂时,字符串的键远无法满足需求,同时也可能有相当一部分内容仅在私聊或者群聊下可用,其次,参数的处理也需要在结果处理内部中完成,这是十分复杂与繁琐的,因此便有入了本节内容。基本使用
on_message
事件的再处理与封装,这点与后续将学习的中间件和正则匹配是一致的,因此也可以看作是一个事件处理的语法糖。通过 ctx.command()
可注册一条指令,参数为指令模板字符,返回 Command
实例对象,实例上有着若干方法用于装饰该指令,其返回值同样为当前指令的实例对象。ctx.command('echo <...content>').action((data) => data.args.join(' '));
+
+ctx.command('time').action(() => {
+ const time = new Date().getTime();
+ return time;
+});
指令模板字符
ctx.command('bar');
+ctx.command('car <arg1> <arg2>');
+ctx.command('dar <arg1> [arg2] [arg3=value]');
+ctx.command('ear [arg1:number=1] [...args:string] - 指令描述');
<>
表示必要参数,方括号 []
为可选参数参数名:参数类型
,参数名应为小写字母与数字([a-z0-9])组成,参数类型可省略,默认 string
,支持的类型有: string
、number
、boolean
=值
设置默认参数...
设置剩余参数,与 TypeScript 不同的是,剩余参数的类型不需要加上数组表示- 指令描述
设置指令描述选项
Command.option()
设置指令选项,接受两个参数:ctx
+ .command('bar')
+ .option('S', 'speaker - 这是选项的描述')
+ .option('G', 'global:boolean - 这是一个布尔类型的选项')
+ .option('T', 'time:number - 这是一个数字类型的选项');
-
开头内容作为选项缩写名解析-
解析),解析器将把字符串中两个连接符 --
开头的内容作为选项全名解析- 指令描述
设置选项描述string
,支持的类型有: string
、number
、boolean
回调函数
Command.action()
设置指令的回调函数,且每个指令仅可设置一个回调函数,回调函数中接收两个参数:args
与 options
两个键组成的对象,类型分别为 (string | number | boolean)[]
与 Record<string, string | number | boolean>
,分别代表用户输入的参数值与选项值options
中的键为对应选项的全名而非缩写名。ctx.command('bar <...args>').action((data) => {
+ ctx.logger.info(data.args); // 输出参数值数组
+ ctx.logger.info(data.options); // 输出选项值对象
+ session.send('这是一条消息');
+});
session
。session
对象包含了当前指令触发产生的所有上下文信息,比如消息 id、消息类型、触发指令的账号、Bot 实例等,在处理函数中可以方便地与 Bot 进行交互。还可以从 session
中获取诸如发送消息等实用工具函数。下文将详细讲解 session
对象相关内容,此处仅做演示。ctx.command('at').action((_, session) => {
+ session.send(\`你好,\${session.el.at(session.userId)},你的名字是 \${session.sender.nickname}\`);
+});
作用域
Command.scope()
设置指令作用域,值类型为 MessageScope
,如若不设置则默认所有场景均可使用。export enum MessageScope {
+ PRIVATE, // 私聊
+ GROUP // 群聊
+}
ctx
+ .command('test')
+ .scope(MessageScope.PRIVATE)
+ .action(() => '这是一条仅私聊可用的消息');
+
+ctx
+ .command('hello')
+ .scope(MessageScope.GROUP)
+ .action((_, session) => {
+ session.send(\`这是一条仅群聊可用的消息\`);
+ });
别名
Command.alias()
设置指令别名,参数为 string | string[]
。ctx
+ .command('original')
+ .alias('o') // 别名可以是单个字符串
+ .alias(['ori', 'org']) // 也可以是字符串数组
+ .action(() => '这是原版指令');
权限
Command.access()
设置指令权限,值类型为 CommandAccess
。export const enum CommandAccess {
+ MEMBER, // 所有成员可用,默认值,权限最低
+ MANGER, // 管理员(群管理员/群主或 Bot 管理员)及以上权限可用
+ ADMIN // 仅该 Bot 最高管理员可用
+}
CommandAccess.ADMIN
对应 kotori.yml
中的 AdapterConfig.master
选项ctx
+ .command('op')
+ .access(CommandAccess.ADMIN)
+ .action(() => '这是一条特殊指令');
帮助信息
Command.help()
设置指令帮助信息,相对于指令模板字符中的指令描述,其提供更为详尽全面的信息。ctx.command('bar').help('这里是指令的帮助信息');
返回值处理
session.send()
方法。其本质上是自动将回调函数返回值作为参数传入 session.quick()
方法,具体处理逻辑请参考下文。ctx.command('concat <str1> <str2>').action(({ args: [str1, str2] }) => str1 + str2);
+
+ctx.command('render').action(() => ['template.text', { content: '这是模板内容' }]);
+
+ctx.command('fetch').action(async () => {
+ const res = await ctx.http.get('https://api.example.com');
+ return ['template.status', { status: res.status }];
+});
子指令
list
有着多个操作,如查询、添加、删除列表等,大可以使用多个完全独立的指令如 list_query
、list_add
、list_remove
,但这并不优雅。此处通过注册一个指令并判断其第一个参数的值执行相应操作/* 错误示例不要抄 */
+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
。因此,当同一指令有多个操作(即多个指令回调函数)且各个操作间相对独立时可使用子指令。基础用法: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
指令: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 中使用最广泛的功能且当前你已掌握事件系统的概念,会话事件数据的内容得以放在此处进行详细讲解。重要属性
export interface EventDataApiBase {
+ type?: MessageScope;
+ api: Api;
+ el: Elements;
+ userId: EventDataTargetId;
+ groupId?: EventDataTargetId;
+ operatorId?: EventDataTargetId;
+ i18n: I18n;
+ send(message: MessageRaw): void;
+ format(template: string, data: Record<string, unknown> | CommandArgType[]): string;
+ quick(message: MessageQuick): void;
+ prompt(message?: MessageRaw): Promise<MessageRaw>;
+ confirm(options?: { message: MessageRaw; sure: MessageRaw }): Promise<boolean>;
+ error<T extends Exclude<keyof CommandResult, CommandResultNoArgs>>(
+ type: T,
+ data: CommandResult[T] extends object ? CommandResult[T] : never
+ ): CommandError;
+ error<T extends CommandResultNoArgs>(type: T): CommandError;
+ extra?: unknown;
+}
session
对象本质上就是一个事件数据对象(即会话事件),上述是会话事件的共有属性,不同会话事件中有着不同的额外属性,如 EventDataGroupMsg
事件有 messageId
、sender
、message
、groupId
,而 EventDataPrivateMsg
事件没有 groupId
,EventDataPrivateRecall
事件其中的仅有 messageId
,这些额外属性均不在当前讨论范围内,具体内容参考接口文档。对于上述的共有属性在当前阶段也不必全部掌握。Api
实例对象,提供多个与当前聊天平台的交互接口Elements
实例对象,api.adapter.elements
属性的语法糖字符串处理
export type CommandArgType = string | number | boolean;
+type ObjectArgs = Record<string, CommandArgType>;
+type ArrayArgs = CommandArgType[];
session.format()
方法是一个简单的模板字符串替换工具(此处请区别于 JavaScript 中的 「模板字符串」)。接收两个参数:ObjectArgs
、ArrayArgs
。ctx.command('himeki').action((_, { format }) => {
+ format('名字:%name%\\n身高:%height%cm\\n口头禅:%msg%', {
+ name: 'Ichinose Himeki',
+ height: 153,
+ msg: '最喜欢你了,欧尼酱'
+ });
+ // 等同于:
+ format('名字:{0}\\n身高:{1}cm\\n口头禅:{2}', ['Ichinose Himeki', 153, '最喜欢你了,欧尼酱']);
+ // 最终输出:名字:Ichinose Himeki\\n年龄:153\\n口头禅:最喜欢你了,欧尼酱
+});
%key%
的形式进行替换,与对象键值一一对应,其更具有语义性,适合文本长且参数较多使用,但使用过多易造成代码冗余{index}
的形式进行替换,与数组索引一一对应,缺少语义性但更简洁,适合短文本使用,不易造成代码冗余session.format()
可实现较为复杂的动态数据展示,但不宜过多消息发送
session.send()
方法是对 session.api
上发送消息方法的封装,而 session.quick()
方法则是对 session.send()
的封装。一般地,在有会话事件数据可使用且无特殊需求下,均推荐使用 session.quick()
,后续所有代码演示无特殊情况也默认使用该方法。string
将调用 i18n.locale()
方法实现国际化[string, ObjectArgs | ArrayArgs]
参数,将先遍历数组中第二个值下的所有属性并调用 i18n.locale()
进行替换,然后将其传入 session.format()
方法undefined
、''
、void
、null
、0
则不作处理(一般不允许传入这些东西,主要发生在指令处理的回调函数返回值上)Error
则另作处理(一般不允许传入这些东西,主要发生在指令处理的回调函数返回值上)Promise
则等待 Promise 完成后再做上述处理i18n.locale()
方法当前可粗略理解为:传入一个已预定好且唯一的字符串值,根据当前使用语言返回相应语言文本。当然,不理解并不妨碍你使用 session.quick()
方法// locales/zh_CN.json
+{
+ "test.msg.himeki.hitokoto": "最喜欢你了,欧尼酱",
+ "test.msg.himeki": "名字:{0}\\n身高:{1}cm\\n口头禅:{2}"
+}
// 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']];
+ });
+}
会话交互
session.prompt()
与 session.confirm()
,它们和浏览器中的 prompt()
与 confirm()
类似,分别对应为输入框和提示框。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 '谢谢你的喜欢哦~';
+});
await
关键词,否则可能会有一些意料之外的效果。session.prompt()
参数为 string
,对应提示消息,返回 Promise<string>
session.confirm()
参数为 { message: string, sure: string }
,分别对应提示消息和确认消息(只有用户发送消息与确认消息完全一致时返回 true
反之 false
),返回 Promise<boolean>
错误处理
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'];
+}
CommandParseResult
,这些在指令系统不需要你操心,因为它们已全部交由上游的 Kotori 内置中间件进行处理,在解析指令时就会被发现Omit<CommandResult, keyof CommandParseResult>
,它们有的发生在指令执行前(如 no_access_manger
、no_access_admin
),又或者 error
这种错误之外的错误(执行回调函数时捕获的错误),这两者也不需要你操心data_error
参数错误(不同于参数类型错误)res_error
资源错误(主要是指网络请求第三方 Api 时返回数据类型有误)num_error
序号错误(主要是指需要用户传入数字进行选择的情况)exists
目标已存在(如添加目标到名单里但目标已存在于名单)no_exists
目标不存在(如删除目标从名单里但目标不存在于名单)session.error()
方法即可在运行时阶段抛出错误,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()
抛出错误
`,86)]))}const y=i(k,[["render",t]]);export{g as __pageData,y as default};
diff --git a/assets/guide_base_events.md.CklH1j6g.js b/assets/guide_base_events.md.CklH1j6g.js
new file mode 100644
index 00000000..67f585ae
--- /dev/null
+++ b/assets/guide_base_events.md.CklH1j6g.js
@@ -0,0 +1,63 @@
+import{_ as i,c as a,a1 as n,o as h}from"./chunks/framework.C72X4JAr.js";const g=JSON.parse('{"title":"事件系统","description":"","frontmatter":{},"headers":[],"relativePath":"guide/base/events.md","filePath":"guide/base/events.md","lastUpdated":1712229374000}'),t={name:"guide/base/events.md"};function p(k,s,l,e,E,d){return h(),a("div",null,s[0]||(s[0]=[n(`ctx.http
是一个网络请求工具,基于 Axios 封装,具体内容参考接口文档;此处的「检查数据的操作」实际上指 Schema,这将在第三章中讲解事件系统
订阅事件
ctx.on()
订阅一个事件,第一个参数为事件名,第二个参数为回调函数,事件被触发时事件数据将作为实际参数传给回调函数。import { MessageScope } from 'kotori-bot';
+
+// ...
+
+ctx.on('on_message', (session) => {
+ if (session.message !== '你是谁') return;
+ if (session.type === MessageScope.GROUP) {
+ session.api.sendGroupMsg('是 Kotori!', session.groupId);
+ } else {
+ session.api.sendPrivateMsg('是 Kotori!', session.userId);
+ }
+});
session.type
的值,调用相应的发送消息接口发送「是 Kotori!」。根据语义化命名可知:session.type
为消息类型,值是一个 MessageScope
枚举值,分为 「GROUP」(群聊)和「PRIVATE」(私聊);session.api
是 Api
的实例对象,提供了多种与聊天平台交互的接口,此处用到的 sendG丨groupMsg
与 sendPrivateMsg
分别是发送群聊消息与发送私聊消息,第一个参数为消息内容,第二个参数分别为群聊 id 与用户 id。groupId
为 QQ 群号,userId
为 QQ 号。ctx.on('on_message', (session) => {
+ if (session.message !== '你是谁') return;
+ session.send('是 Kotori!');
+ }
+});
session.send()
只需要传入消息内容即可,消息类型判断和传入相应 id 的工作已在该方法内部完成。session
上还有不少与之类似的语法糖,将在后面章节中逐一提到,也因如此,session.send()
在实际开发中使用率并不高,因为它对你后面将了解的内容而言依旧很繁琐。取消订阅事件
ctx.off()
的使用方法与 ctx.on()
一致。const handle = (session: Session['on_message']) => {
+ ctx.off('on_message', handle);
+ // ...
+};
+
+ctx.on('on_message', handle);
ctx.on()
在执行后会返回取消订阅自己的方法,因此可以这样简化:const off = ctx.on('on_message', (session) => {
+ off();
+ // ...
+});
ctx.once()
再进一步简化:ctx.once('on_message', (session) => {
+ // ...
+});
ctx.once()
订阅事件,在触发后会立即取消订阅。ctx.offAll()
取消订阅指定事件名下所有事件: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
事件。事件类型
data
。session
。系统事件
ready
:当加载完所有模块时触发dispose
:当 Kotori 关闭时触发status
:当 Bot 的在线状态改变时触发status
实现 Bot 上线后自动发送消息给最高管理员:ctx.on('status', (data) => {
+ if (data.status !== 'online') return;
+ const { api, config } = data.adapter;
+ api.sendPrivateMsg('上线了!', config.master);
+});
status
是由适配器发出的系统事件,它并没有类似于会话事件中的 session.send()
,因此只能使用最原始的办法发送消息。status
的事件数据中仅有两个值,一个是 data.status
表示当前在线状态(「online」或「offline」),data.adapter
为目标 Bot,Bot 上有 adapter.api
与 adapter.config
,前者等价于会话事件中的 session.api
,后者为 Bot 配置,来自于 kotori.yml
。会话事件
on_message
:当收到消息时触发on_recall
:当有消息撤回时触发on_group_increase
:当群人数增加时触发on_group_increase
实现群欢迎:ctx.on('on_group_increase', (session) => {
+ session.send(\`因为遇见了\${session.el.at(session.userId)},我的世界才充满颜色!\`);
+});
session.el
与 session.api
类似,是 Elements
的实例对象,它提供了用于转换消息元素的接口,如 session.el.at()
传入用户 id 转换成艾特消息,session.el.image()
传入图片 URL 转换成图片消息。当然,并不是所有聊天平台都支持所有的消息元素,应以具体聊天平台为准。自定义事件与发出事件
declare module 'kotori-bot' {
+ interface EventsMapping {
+ custom_event1(data: string): void;
+ }
+}
+
+ctx.on('custom_event1', (data) => {
+ ctx.logger.debug(data);
+});
EventsMapping
接口上。custom_event1
事件触发后将打印事件数据。ctx.logger
是一个日志打印工具,ctx.logger.debug()
意味着打印内容仅在 dev
模式下运行 Kotori 可见,具体内容请参考接口文档// ...
+
+ctx.emit('custom_event1', '这是事件数据');
+ctx.emit('custom_event1', '这里也是事件数据');
ctx.emit()
第一个参数为事件名,然后为剩余参数,剩余参数与该事件参数一一对应。虽然 Kotori 中系统事件与会话事件的参数均只有一个,但是可以在自定义事件中实现任意多个参数: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 });
事件系统
订阅事件
ctx.on()
订阅一个事件,第一个参数为事件名,第二个参数为回调函数,事件被触发时事件数据将作为实际参数传给回调函数。import { MessageScope } from 'kotori-bot';
+
+// ...
+
+ctx.on('on_message', (session) => {
+ if (session.message !== '你是谁') return;
+ if (session.type === MessageScope.GROUP) {
+ session.api.sendGroupMsg('是 Kotori!', session.groupId);
+ } else {
+ session.api.sendPrivateMsg('是 Kotori!', session.userId);
+ }
+});
session.type
的值,调用相应的发送消息接口发送「是 Kotori!」。根据语义化命名可知:session.type
为消息类型,值是一个 MessageScope
枚举值,分为 「GROUP」(群聊)和「PRIVATE」(私聊);session.api
是 Api
的实例对象,提供了多种与聊天平台交互的接口,此处用到的 sendG丨groupMsg
与 sendPrivateMsg
分别是发送群聊消息与发送私聊消息,第一个参数为消息内容,第二个参数分别为群聊 id 与用户 id。groupId
为 QQ 群号,userId
为 QQ 号。ctx.on('on_message', (session) => {
+ if (session.message !== '你是谁') return;
+ session.send('是 Kotori!');
+ }
+});
session.send()
只需要传入消息内容即可,消息类型判断和传入相应 id 的工作已在该方法内部完成。session
上还有不少与之类似的语法糖,将在后面章节中逐一提到,也因如此,session.send()
在实际开发中使用率并不高,因为它对你后面将了解的内容而言依旧很繁琐。取消订阅事件
ctx.off()
的使用方法与 ctx.on()
一致。const handle = (session: Session['on_message']) => {
+ ctx.off('on_message', handle);
+ // ...
+};
+
+ctx.on('on_message', handle);
ctx.on()
在执行后会返回取消订阅自己的方法,因此可以这样简化:const off = ctx.on('on_message', (session) => {
+ off();
+ // ...
+});
ctx.once()
再进一步简化:ctx.once('on_message', (session) => {
+ // ...
+});
ctx.once()
订阅事件,在触发后会立即取消订阅。ctx.offAll()
取消订阅指定事件名下所有事件: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
事件。事件类型
data
。session
。系统事件
ready
:当加载完所有模块时触发dispose
:当 Kotori 关闭时触发status
:当 Bot 的在线状态改变时触发status
实现 Bot 上线后自动发送消息给最高管理员:ctx.on('status', (data) => {
+ if (data.status !== 'online') return;
+ const { api, config } = data.adapter;
+ api.sendPrivateMsg('上线了!', config.master);
+});
status
是由适配器发出的系统事件,它并没有类似于会话事件中的 session.send()
,因此只能使用最原始的办法发送消息。status
的事件数据中仅有两个值,一个是 data.status
表示当前在线状态(「online」或「offline」),data.adapter
为目标 Bot,Bot 上有 adapter.api
与 adapter.config
,前者等价于会话事件中的 session.api
,后者为 Bot 配置,来自于 kotori.yml
。会话事件
on_message
:当收到消息时触发on_recall
:当有消息撤回时触发on_group_increase
:当群人数增加时触发on_group_increase
实现群欢迎:ctx.on('on_group_increase', (session) => {
+ session.send(\`因为遇见了\${session.el.at(session.userId)},我的世界才充满颜色!\`);
+});
session.el
与 session.api
类似,是 Elements
的实例对象,它提供了用于转换消息元素的接口,如 session.el.at()
传入用户 id 转换成艾特消息,session.el.image()
传入图片 URL 转换成图片消息。当然,并不是所有聊天平台都支持所有的消息元素,应以具体聊天平台为准。自定义事件与发出事件
declare module 'kotori-bot' {
+ interface EventsMapping {
+ custom_event1(data: string): void;
+ }
+}
+
+ctx.on('custom_event1', (data) => {
+ ctx.logger.debug(data);
+});
EventsMapping
接口上。custom_event1
事件触发后将打印事件数据。ctx.logger
是一个日志打印工具,ctx.logger.debug()
意味着打印内容仅在 dev
模式下运行 Kotori 可见,具体内容请参考接口文档// ...
+
+ctx.emit('custom_event1', '这是事件数据');
+ctx.emit('custom_event1', '这里也是事件数据');
ctx.emit()
第一个参数为事件名,然后为剩余参数,剩余参数与该事件参数一一对应。虽然 Kotori 中系统事件与会话事件的参数均只有一个,但是可以在自定义事件中实现任意多个参数: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 });
中间件
on_message
事件的再处理与封装。中间件的主要用途是提前判断或者过滤掉不必要的消息事件,这样后续的指令和正则表达式等位于下游的设施也不会被这些消息事件触发,从而提高效率。注册中间件
ctx.midware()
注册一个中间件,该方法接受两个参数:ctx.midware((next, session) => {
+ // 中间件逻辑...
+ next(); // 通过此中间件
+}, 80); // 优先级为 80
next
函数,调用它将执行下一个中间件session
对象,包含当前消息事件的上下文信息next()
函数。如果调用了 next()
则通过此中间件,否则此消息事件将被过滤掉,不再执行后续的中间件和其他处理逻辑。移除中间件
ctx.midware()
方法的返回值是一个可以用于移除该中间件的函数。const dispose = ctx.midware((next) => {
+ // ...
+ next();
+});
+
+// 移除中间件
+dispose();
使用示例
基本使用
ctx.midware((next, session) => {
+ console.log('收到一条消息');
+ next();
+});
+
+ctx.midware((next, session) => {
+ console.log('这是另一个中间件');
+ session.quick('这条消息将被发送');
+ next();
+});
next()
函数,因此该消息事件会继续被处理。过滤消息
ctx.midware((next, session) => {
+ if (session.message !== 'hello') return;
+ next();
+});
+
+ctx.command('hello').action(() => 'Hello World!');
next()
让该消息事件继续被处理。通过使用中间件,我们可以在消息流经 Kotori 的各个环节进行拦截和处理,实现更加灵活和可控的消息处理逻辑。限制命令使用频率
// 用于存储命令使用记录
+const cmdUsageRecord = new Map();
+
+ctx.midware((next, session) => {
+ // 检查是否为命令消息
+ if (!session.message.startsWith('/')) {
+ next(); // 非命令消息,直接通过
+ return;
+ }
+
+ const cmd = session.message.slice(1); // 获取命令名
+ const userId = session.userId; // 获取发送者ID
+
+ // 如果此命令无使用记录,则新建一个记录
+ if (!cmdUsageRecord.has(cmd)) {
+ cmdUsageRecord.set(cmd, new Map());
+ }
+ const userRecord = cmdUsageRecord.get(cmd);
+
+ // 获取该用户对此命令的最后使用时间
+ const lastUsedAt = userRecord.get(userId) || 0;
+
+ // 计算距离最后一次使用的时间间隔(单位:秒)
+ constInterval = (Date.now() - lastUsedAt) / 1000;
+
+ // 如果间隔小于10秒,则拒绝执行该命令
+ if (Interval < 10) {
+ session.quick('命令使用过于频繁,请稍后再试');
+ return;
+ }
+
+ // 更新该用户对此命令的最后使用时间
+ userRecord.set(userId, Date.now());
+
+ next(); // 通过中间件
+}, 10); // 设置较高优先级
/
开头,如果不是则直接调用 next()
通过该中间件。'命令使用过于频繁,请稍后再试'
。next()
通过该中间件。Map
来存储命令使用记录,外层 Map
的键为命令名,值为另一个 Map
,内层 Map
的键为用户 ID,值为该用户最后一次使用该命令的时间戳。中间件
on_message
事件的再处理与封装。中间件的主要用途是提前判断或者过滤掉不必要的消息事件,这样后续的指令和正则表达式等位于下游的设施也不会被这些消息事件触发,从而提高效率。注册中间件
ctx.midware()
注册一个中间件,该方法接受两个参数:ctx.midware((next, session) => {
+ // 中间件逻辑...
+ next(); // 通过此中间件
+}, 80); // 优先级为 80
next
函数,调用它将执行下一个中间件session
对象,包含当前消息事件的上下文信息next()
函数。如果调用了 next()
则通过此中间件,否则此消息事件将被过滤掉,不再执行后续的中间件和其他处理逻辑。移除中间件
ctx.midware()
方法的返回值是一个可以用于移除该中间件的函数。const dispose = ctx.midware((next) => {
+ // ...
+ next();
+});
+
+// 移除中间件
+dispose();
使用示例
基本使用
ctx.midware((next, session) => {
+ console.log('收到一条消息');
+ next();
+});
+
+ctx.midware((next, session) => {
+ console.log('这是另一个中间件');
+ session.quick('这条消息将被发送');
+ next();
+});
next()
函数,因此该消息事件会继续被处理。过滤消息
ctx.midware((next, session) => {
+ if (session.message !== 'hello') return;
+ next();
+});
+
+ctx.command('hello').action(() => 'Hello World!');
next()
让该消息事件继续被处理。通过使用中间件,我们可以在消息流经 Kotori 的各个环节进行拦截和处理,实现更加灵活和可控的消息处理逻辑。限制命令使用频率
// 用于存储命令使用记录
+const cmdUsageRecord = new Map();
+
+ctx.midware((next, session) => {
+ // 检查是否为命令消息
+ if (!session.message.startsWith('/')) {
+ next(); // 非命令消息,直接通过
+ return;
+ }
+
+ const cmd = session.message.slice(1); // 获取命令名
+ const userId = session.userId; // 获取发送者ID
+
+ // 如果此命令无使用记录,则新建一个记录
+ if (!cmdUsageRecord.has(cmd)) {
+ cmdUsageRecord.set(cmd, new Map());
+ }
+ const userRecord = cmdUsageRecord.get(cmd);
+
+ // 获取该用户对此命令的最后使用时间
+ const lastUsedAt = userRecord.get(userId) || 0;
+
+ // 计算距离最后一次使用的时间间隔(单位:秒)
+ constInterval = (Date.now() - lastUsedAt) / 1000;
+
+ // 如果间隔小于10秒,则拒绝执行该命令
+ if (Interval < 10) {
+ session.quick('命令使用过于频繁,请稍后再试');
+ return;
+ }
+
+ // 更新该用户对此命令的最后使用时间
+ userRecord.set(userId, Date.now());
+
+ next(); // 通过中间件
+}, 10); // 设置较高优先级
/
开头,如果不是则直接调用 next()
通过该中间件。'命令使用过于频繁,请稍后再试'
。next()
通过该中间件。Map
来存储命令使用记录,外层 Map
的键为命令名,值为另一个 Map
,内层 Map
的键为用户 ID,值为该用户最后一次使用该命令的时间戳。正则匹配
注册正则匹配
ctx.regexp()
注册一个正则匹配,该方法接受两个参数:match
: 用于匹配消息内容的正则表达式callback
: 当正则匹配成功时执行的回调函数ctx.regexp(/^\\/start$/, (match, session) => {
+ session.send('游戏开始!');
+});
/start
时,它会执行回调函数,并向发送者发送 '游戏开始!'
消息。callback
函数接收两个参数:match
: 正则匹配结果,是一个数组,第一项为完整匹配结果,后续项为各个捕获组的内容session
: 当前消息事件的上下文信息移除正则匹配
ctx.regexp()
方法的返回值是一个可以用于移除该正则匹配的函数。const off = ctx.regexp(/pattern/, () => {
+ /* ... */
+});
+
+// 移除正则匹配
+off();
正则匹配示例
简单匹配
ctx.regexp(/^\\/echo (.+)$/, (match, session) => {
+ const content = match[1]; // 捕获组内容
+ session.send(content); // 回声匹配消息
+});
你好
并将其作为第一个捕获组,然后在回调函数中将捕获组的内容作为消息发送出去。复杂匹配
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 完全支持,因此这里使用了普通的捕获组)。模糊匹配
ctx.regexp(/在\\s*吗?/, (match, session) => {
+ session.send('我在这里');
+});
/在\\s*吗?/
可以匹配 「在吗」、「在 吗」 以及 「在」 这三种情况。使用 ?
可以使前面的字符或字符组成为可选。多个匹配
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「 这样的算术表达式。正则匹配
注册正则匹配
ctx.regexp()
注册一个正则匹配,该方法接受两个参数:match
: 用于匹配消息内容的正则表达式callback
: 当正则匹配成功时执行的回调函数ctx.regexp(/^\\/start$/, (match, session) => {
+ session.send('游戏开始!');
+});
/start
时,它会执行回调函数,并向发送者发送 '游戏开始!'
消息。callback
函数接收两个参数:match
: 正则匹配结果,是一个数组,第一项为完整匹配结果,后续项为各个捕获组的内容session
: 当前消息事件的上下文信息移除正则匹配
ctx.regexp()
方法的返回值是一个可以用于移除该正则匹配的函数。const off = ctx.regexp(/pattern/, () => {
+ /* ... */
+});
+
+// 移除正则匹配
+off();
正则匹配示例
简单匹配
ctx.regexp(/^\\/echo (.+)$/, (match, session) => {
+ const content = match[1]; // 捕获组内容
+ session.send(content); // 回声匹配消息
+});
你好
并将其作为第一个捕获组,然后在回调函数中将捕获组的内容作为消息发送出去。复杂匹配
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 完全支持,因此这里使用了普通的捕获组)。模糊匹配
ctx.regexp(/在\\s*吗?/, (match, session) => {
+ session.send('我在这里');
+});
/在\\s*吗?/
可以匹配 「在吗」、「在 吗」 以及 「在」 这三种情况。使用 ?
可以使前面的字符或字符组成为可选。多个匹配
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「 这样的算术表达式。前言
前置要求
读后
',13)]))}const f=t(o,[["render",l]]);export{h as __pageData,f as default};
diff --git a/assets/guide_index.md.DTAEBya5.lean.js b/assets/guide_index.md.DTAEBya5.lean.js
new file mode 100644
index 00000000..abe8591a
--- /dev/null
+++ b/assets/guide_index.md.DTAEBya5.lean.js
@@ -0,0 +1 @@
+import{_ as t,c as e,a1 as r,o as i}from"./chunks/framework.C72X4JAr.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('前言
前置要求
读后
',13)]))}const f=t(o,[["render",l]]);export{h as __pageData,f as default};
diff --git a/assets/guide_modules_context.md.DbpmHeoC.js b/assets/guide_modules_context.md.DbpmHeoC.js
new file mode 100644
index 00000000..c2ff8028
--- /dev/null
+++ b/assets/guide_modules_context.md.DbpmHeoC.js
@@ -0,0 +1,200 @@
+import{_ as i,c as a,a1 as n,o as h}from"./chunks/framework.C72X4JAr.js";const g=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,E,d){return h(),a("div",null,s[0]||(s[0]=[n(`上下文
注册与获取
ctx.provide()
可将指定对象注册到当前上下文实例中,并通过 ctx.get()
获取。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()
获取。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()
。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!
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']);
继承
ctx.extends()
继承当前上下文。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
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'
。const ctx = new Context();
+const ctxChild1 = ctx.extends();
+const ctxChild2 = ctx.extends();
+const ctxChild3 = ctxChild1.extends();
+
+ctx.root === ctx; // true
+ctxChild1.root === ctxChild1 || ctxChild1.root === ctxChild2; // false
+ctxChild1.root === ctx && ctxChild2.root === ctx; // true
+ctxChild3.root === ctxChild1; // false
+ctxChild3.root === ctx; // true
ctx.root
属性可获取当前上下文的根上下文实例,无论是子级上下文 ctxChild1
、ctxChild2
,还是继承了 ctxChild1
的孙级上下文 ctxChild3
,其根上下文实例均指向 ctx
,而根上下午实例的 ctx.root
指向自身。事件系统
Context
类定义,通过类原生的继承方式和 ctx.inject()
、ctx.mixin()
等方法对 Context
进行装饰或扩展。而在 Context
类内部,它本身就已为自己注册并注入了两个实例对象,其一便是事件系统,这也是在第二章中介绍事件系统时说道「事件订阅者模式与事件系统共同构成了 Kotori 的基础」的原由,只不过 Context
类本身并未使用事件系统功能,且仅直接定义了 ready
与 dispose
事件,两者被作为整个程序生命周期的重要一环,其余事件由另一实例对象(见下文)或 Kotori 核心类定义。插件系统
ready_module
与 dispose_module
事件。在上一节说过「在真正学习到上下文之前,可暂且默认插件等同于模块」,而现在你将会对「插件」有更深的认知。ctx.load()
加载插件并触发 ready_module
事件,且 ready_module
事件在插件加载完毕后触发。/* 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
):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) => {}
+ });
+}
ctx.load()
进行调用执行主体。不同的是,此处可以定义 name
属性用于标记插件的名称,这将作用于该插件的上下文实例的 ctx.identity
中,而模块中的 ctx.identity
由加载器通过 package.json
中的包名自动获取。即便是子插件,它的上下文实例与配置数据也是完全独立,区别在于模块(由加载器加载)的上下文实例继承自 Kotori 内部中的根上下文实例,而子插件的上下文实例继承于当前模块的上下文实例,以此类推。入口文件中导出的 config
是一个配置检测者,加载器会调用它来验证 kotori.toml
中相应的实际配置数据是否符合要求,符合则将替换 config
为实际数据再传入 ctx.load()
作后续处理,在模块中执行 ctx.load()
,其配置数据拥有确定性(指由开发者保证,与 Kotori 无关),因此要求此处直接传入配置数据。[plugin.my-project]
+value = 'here is a string'
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
+ }
+ });
+}
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
属性。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');
+ }
+ });
+}
require()
或 ESModule 规范的 import()
方法,两个方法将会返回动态导入文件的导出对象,区别在于前者是同步执行后者为异步执行,这将间接实现动态导入并加载外部 TypeScript/JavaScript 文件的插件。/** 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));
+}
.ts
或 .js
后缀的路径/** 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()
实现。上下文
注册与获取
ctx.provide()
可将指定对象注册到当前上下文实例中,并通过 ctx.get()
获取。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()
获取。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()
。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!
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']);
继承
ctx.extends()
继承当前上下文。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
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'
。const ctx = new Context();
+const ctxChild1 = ctx.extends();
+const ctxChild2 = ctx.extends();
+const ctxChild3 = ctxChild1.extends();
+
+ctx.root === ctx; // true
+ctxChild1.root === ctxChild1 || ctxChild1.root === ctxChild2; // false
+ctxChild1.root === ctx && ctxChild2.root === ctx; // true
+ctxChild3.root === ctxChild1; // false
+ctxChild3.root === ctx; // true
ctx.root
属性可获取当前上下文的根上下文实例,无论是子级上下文 ctxChild1
、ctxChild2
,还是继承了 ctxChild1
的孙级上下文 ctxChild3
,其根上下文实例均指向 ctx
,而根上下午实例的 ctx.root
指向自身。事件系统
Context
类定义,通过类原生的继承方式和 ctx.inject()
、ctx.mixin()
等方法对 Context
进行装饰或扩展。而在 Context
类内部,它本身就已为自己注册并注入了两个实例对象,其一便是事件系统,这也是在第二章中介绍事件系统时说道「事件订阅者模式与事件系统共同构成了 Kotori 的基础」的原由,只不过 Context
类本身并未使用事件系统功能,且仅直接定义了 ready
与 dispose
事件,两者被作为整个程序生命周期的重要一环,其余事件由另一实例对象(见下文)或 Kotori 核心类定义。插件系统
ready_module
与 dispose_module
事件。在上一节说过「在真正学习到上下文之前,可暂且默认插件等同于模块」,而现在你将会对「插件」有更深的认知。ctx.load()
加载插件并触发 ready_module
事件,且 ready_module
事件在插件加载完毕后触发。/* 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
):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) => {}
+ });
+}
ctx.load()
进行调用执行主体。不同的是,此处可以定义 name
属性用于标记插件的名称,这将作用于该插件的上下文实例的 ctx.identity
中,而模块中的 ctx.identity
由加载器通过 package.json
中的包名自动获取。即便是子插件,它的上下文实例与配置数据也是完全独立,区别在于模块(由加载器加载)的上下文实例继承自 Kotori 内部中的根上下文实例,而子插件的上下文实例继承于当前模块的上下文实例,以此类推。入口文件中导出的 config
是一个配置检测者,加载器会调用它来验证 kotori.toml
中相应的实际配置数据是否符合要求,符合则将替换 config
为实际数据再传入 ctx.load()
作后续处理,在模块中执行 ctx.load()
,其配置数据拥有确定性(指由开发者保证,与 Kotori 无关),因此要求此处直接传入配置数据。[plugin.my-project]
+value = 'here is a string'
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
+ }
+ });
+}
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
属性。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');
+ }
+ });
+}
require()
或 ESModule 规范的 import()
方法,两个方法将会返回动态导入文件的导出对象,区别在于前者是同步执行后者为异步执行,这将间接实现动态导入并加载外部 TypeScript/JavaScript 文件的插件。/** 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));
+}
.ts
或 .js
后缀的路径/** 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()
实现。模块与插件
前言
package.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"
+ }
+}
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';
+ };
+ };
+}
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 Dependencieskotori-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
。以下是一个最基础的入口文件示例:import { Context } from 'kotori-bot';
+
+export function main(ctx: Context) {}
main()
的函数,接收一个 Context
实例作为参数,诸如之前介绍的事件系统、指令、中间件、正则匹配等功能均是在其上进行的操作。除此之外,入口文件还可以导出一些其他的变量,供其他模块调用。注册国际化文件目录
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 为此提供了语法糖: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()
处理成路径字符串。自定义模块配置
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
文件中进行模块配置:# ...
+
+plugin:
+ my-project:
+ key1: value1
+ key2: 0
+ key3: true
main()
函数的第二个参数 config
获取模块的实际配置信息:/* ... */
+
+export function main(ctx: Context, cfg: Tsu.infer<typeof config>) {
+ ctx.logger.debug(cfg.key1, cfg);
+ // 'value1' { key1: 'value1', key2: 0, key3: true }
+}
设置依赖服务
/* ... */
+
+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
事件(当加载完所有模块时)进行数据库初始化操作。模块风格与范式
导出式
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>) {
+ /* ... */
+}
导出类式
import { Context, Tsu } from 'kotori-bot';
+
+/*
+export const lang = [__dirname, '../locales'];
+
+export const config = Tsu.Object({ /* ... */ });
+
+export const inject = ['database'];
+*/
+
+export class Main {
+ public static lang = [__dirname, '../locales'];
+
+ public static config = Tsu.Object({ /* ... */ });
+
+ public static inject = ['database'];
+
+ public constructor(
+ private ctx: Context,
+ private cfg: Tsu.infer<typeof config>
+ ) {
+ /* ... */
+ }
+}
config
、lang
、inject
属性,也可在类中设置相应的静态属性,一般地,请使用后者。如若两者同时存在,类中的属性将会覆盖外部导出的属性。main
而导出类式的类名使用 Main
,如若两者互换将不会被 Kotori 识别为有效的模块。默认导出
import { Context } from 'kotori-bot';
+
+export default function main(ctx: Context) {}
import { Context } from 'kotori-bot';
+
+export default class {
+ public constructor(private ctx: Context) {}
+}
main()
导出函数Main
导出类直接调用式
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 为依赖库开发一个新的库,则推荐使用该方式。装饰器式
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)熏陶的框架。为数不多的缺点是它需要手动声明类型且对新手而言不容易上手,但如若你有足够的基础则强烈推荐使用。模块与插件
前言
package.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"
+ }
+}
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';
+ };
+ };
+}
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 Dependencieskotori-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
。以下是一个最基础的入口文件示例:import { Context } from 'kotori-bot';
+
+export function main(ctx: Context) {}
main()
的函数,接收一个 Context
实例作为参数,诸如之前介绍的事件系统、指令、中间件、正则匹配等功能均是在其上进行的操作。除此之外,入口文件还可以导出一些其他的变量,供其他模块调用。注册国际化文件目录
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 为此提供了语法糖: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()
处理成路径字符串。自定义模块配置
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
文件中进行模块配置:# ...
+
+plugin:
+ my-project:
+ key1: value1
+ key2: 0
+ key3: true
main()
函数的第二个参数 config
获取模块的实际配置信息:/* ... */
+
+export function main(ctx: Context, cfg: Tsu.infer<typeof config>) {
+ ctx.logger.debug(cfg.key1, cfg);
+ // 'value1' { key1: 'value1', key2: 0, key3: true }
+}
设置依赖服务
/* ... */
+
+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
事件(当加载完所有模块时)进行数据库初始化操作。模块风格与范式
导出式
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>) {
+ /* ... */
+}
导出类式
import { Context, Tsu } from 'kotori-bot';
+
+/*
+export const lang = [__dirname, '../locales'];
+
+export const config = Tsu.Object({ /* ... */ });
+
+export const inject = ['database'];
+*/
+
+export class Main {
+ public static lang = [__dirname, '../locales'];
+
+ public static config = Tsu.Object({ /* ... */ });
+
+ public static inject = ['database'];
+
+ public constructor(
+ private ctx: Context,
+ private cfg: Tsu.infer<typeof config>
+ ) {
+ /* ... */
+ }
+}
config
、lang
、inject
属性,也可在类中设置相应的静态属性,一般地,请使用后者。如若两者同时存在,类中的属性将会覆盖外部导出的属性。main
而导出类式的类名使用 Main
,如若两者互换将不会被 Kotori 识别为有效的模块。默认导出
import { Context } from 'kotori-bot';
+
+export default function main(ctx: Context) {}
import { Context } from 'kotori-bot';
+
+export default class {
+ public constructor(private ctx: Context) {}
+}
main()
导出函数Main
导出类直接调用式
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 为依赖库开发一个新的库,则推荐使用该方式。装饰器式
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)熏陶的框架。为数不多的缺点是它需要手动声明类型且对新手而言不容易上手,但如若你有足够的基础则强烈推荐使用。配置检测
Tsukiko 简介
基本使用
类型检验
import { Tsu } from 'kotori-bot'
+
+const strSchema = Tsu.String()
+strSchema.check(233) // false
+strSchema.check('Hello,Tsukiko!') // true
schema.check()
接收一个参数,返回值表示该参数类型是否匹配。此外,与之类似的还有以下多种校验方法:/* ... */
+
+const value = strSchema.parse(raw)
+// if passed the value must be a string
+// if not passrd: throw TsuError
schema.parse()
会处理传入值并判断是否符合要求,如若不符合将抛出错误(TsuError
)并附带详细原因。不过有时并不想直接抛出错误则可以使用 schema.parseSafe()
:/* ... */
+
+const result = strSchema.parseSafe(raw)
+if (result.value) {
+ console.log('Passed:', result.data)
+
+} else {
+ console.log("Error:", result.error.message)
+}
value
为 true
时,对象上存在 data
属性,其值即为处理后的结果,当 value
为 false
时,对象上存在 error
属性,其值即为错误信息。此外,还有一个异步版本 schema.parseAsync
:/* ... */
+
+strSchema.parseAsync(raw)
+ .then((data) => console.log('Passed', data))
+ .catch((error) => console.log('Fatal', error))
schema.parse()
及相关的解析方法,在传入值符合要求时返回的数据会经过一定的处理,其主要体现为默认值处理: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
const strSchema = Tsu.String()
+
+strSchema(233) // Passed '233'
+strSchema(true) // Error
Tsu.String()
解析器默认允许数字传入(出于兼容性考虑),并会将其处理成字符串返回。类型修饰
schema.default()
与 schema.optional()
,前者用于设置默认值,后者用于设置可选类型: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()
:
+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()
)类型导出
schema.schema()
将任意解析器导出成 JSON Schema。不过在此之前,Tsukiko 提供了额外两个关于 JSON Schema 的新方法: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 g=i(t,[["render",e]]);export{c as __pageData,g as default};
diff --git a/assets/guide_modules_schema.md.COWdiWIF.lean.js b/assets/guide_modules_schema.md.COWdiWIF.lean.js
new file mode 100644
index 00000000..e8ae3710
--- /dev/null
+++ b/assets/guide_modules_schema.md.COWdiWIF.lean.js
@@ -0,0 +1,49 @@
+import{_ as i,c as a,a1 as h,o as n}from"./chunks/framework.C72X4JAr.js";const c=JSON.parse('{"title":"配置检测","description":"","frontmatter":{},"headers":[],"relativePath":"guide/modules/schema.md","filePath":"guide/modules/schema.md","lastUpdated":1724729588000}'),t={name:"guide/modules/schema.md"};function e(k,s,l,p,r,d){return n(),a("div",null,s[0]||(s[0]=[h(`配置检测
Tsukiko 简介
基本使用
类型检验
import { Tsu } from 'kotori-bot'
+
+const strSchema = Tsu.String()
+strSchema.check(233) // false
+strSchema.check('Hello,Tsukiko!') // true
schema.check()
接收一个参数,返回值表示该参数类型是否匹配。此外,与之类似的还有以下多种校验方法:/* ... */
+
+const value = strSchema.parse(raw)
+// if passed the value must be a string
+// if not passrd: throw TsuError
schema.parse()
会处理传入值并判断是否符合要求,如若不符合将抛出错误(TsuError
)并附带详细原因。不过有时并不想直接抛出错误则可以使用 schema.parseSafe()
:/* ... */
+
+const result = strSchema.parseSafe(raw)
+if (result.value) {
+ console.log('Passed:', result.data)
+
+} else {
+ console.log("Error:", result.error.message)
+}
value
为 true
时,对象上存在 data
属性,其值即为处理后的结果,当 value
为 false
时,对象上存在 error
属性,其值即为错误信息。此外,还有一个异步版本 schema.parseAsync
:/* ... */
+
+strSchema.parseAsync(raw)
+ .then((data) => console.log('Passed', data))
+ .catch((error) => console.log('Fatal', error))
schema.parse()
及相关的解析方法,在传入值符合要求时返回的数据会经过一定的处理,其主要体现为默认值处理: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
const strSchema = Tsu.String()
+
+strSchema(233) // Passed '233'
+strSchema(true) // Error
Tsu.String()
解析器默认允许数字传入(出于兼容性考虑),并会将其处理成字符串返回。类型修饰
schema.default()
与 schema.optional()
,前者用于设置默认值,后者用于设置可选类型: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()
:
+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()
)类型导出
schema.schema()
将任意解析器导出成 JSON Schema。不过在此之前,Tsukiko 提供了额外两个关于 JSON Schema 的新方法: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 g=i(t,[["render",e]]);export{c as __pageData,g as default};
diff --git a/assets/guide_modules_service.md.NY7jQReF.js b/assets/guide_modules_service.md.NY7jQReF.js
new file mode 100644
index 00000000..028e9f96
--- /dev/null
+++ b/assets/guide_modules_service.md.NY7jQReF.js
@@ -0,0 +1 @@
+import{_ as t,c as r,j as a,a as s,o}from"./chunks/framework.C72X4JAr.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.NY7jQReF.lean.js b/assets/guide_modules_service.md.NY7jQReF.lean.js
new file mode 100644
index 00000000..028e9f96
--- /dev/null
+++ b/assets/guide_modules_service.md.NY7jQReF.lean.js
@@ -0,0 +1 @@
+import{_ as t,c as r,j as a,a as s,o}from"./chunks/framework.C72X4JAr.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.Cq_i6VjS.js b/assets/guide_start_environment.md.Cq_i6VjS.js
new file mode 100644
index 00000000..071d93ae
--- /dev/null
+++ b/assets/guide_start_environment.md.Cq_i6VjS.js
@@ -0,0 +1 @@
+import{_ as e,c as a,a1 as r,o as i}from"./chunks/framework.C72X4JAr.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
Git & GitHub
IDE & Editor
世界上最好的 Web 开发 IDE 「Visual Studio Code」(以下简称「VSC」)。虽然 VSC 本质上只是文本编辑器,但因其强大的扩展商店使其能做到大部分 IDE 的功能,当然你也可以根据你的喜好选择,如:
',9)]))}const c=e(o,[["render",n]]);export{u as __pageData,c as default};
diff --git a/assets/guide_start_environment.md.Cq_i6VjS.lean.js b/assets/guide_start_environment.md.Cq_i6VjS.lean.js
new file mode 100644
index 00000000..071d93ae
--- /dev/null
+++ b/assets/guide_start_environment.md.Cq_i6VjS.lean.js
@@ -0,0 +1 @@
+import{_ as e,c as a,a1 as r,o as i}from"./chunks/framework.C72X4JAr.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
Git & GitHub
IDE & Editor
世界上最好的 Web 开发 IDE 「Visual Studio Code」(以下简称「VSC」)。虽然 VSC 本质上只是文本编辑器,但因其强大的扩展商店使其能做到大部分 IDE 的功能,当然你也可以根据你的喜好选择,如:
',9)]))}const c=e(o,[["render",n]]);export{u as __pageData,c as default};
diff --git a/assets/guide_start_publish.md.BdBbCqiy.js b/assets/guide_start_publish.md.BdBbCqiy.js
new file mode 100644
index 00000000..015010bd
--- /dev/null
+++ b/assets/guide_start_publish.md.BdBbCqiy.js
@@ -0,0 +1,40 @@
+import{_ as i,c as a,a1 as n,o as t}from"./chunks/framework.C72X4JAr.js";const c=JSON.parse('{"title":"模块发布","description":"","frontmatter":{},"headers":[],"relativePath":"guide/start/publish.md","filePath":"guide/start/publish.md","lastUpdated":1716217371000}'),e={name:"guide/start/publish.md"};function p(l,s,h,r,o,k){return t(),a("div",null,s[0]||(s[0]=[n(`模块发布
构建产物
pnpm build
lib
文件夹,这在上一节已有提到,它是构建产物的输出目录,有必要的话可在 tsconfig.json
文件中更改:{
+ // ...
+ "compilerOptions": {
+ "rootDir": "./src", // 输入目录
+ "outDir": "./lib" // 输出目录
+ // ...
+ }
+}
tsconfig.json
的更多内容:TypeScript Documentation文件忽略
.npmignore
.npmignore
文件:node_modules
+src
+test
+
+tsconfig.json
+!README.md
.npmignore
采用的是黑名单机制显得很繁琐,因此 Kotori 模块的默认模板中并未使用该方式,也并不推荐。package.files
package.json
示例中会发现有一个以字符串数组为值的 files
配置项,其用于指定在使用 publish
时需要附带的文件与文件夹。{
+ "files": ["lib", "LICENSE", "README.md"],
+}
files
配置项优先级高于 .npmignore
,其直接写在 package.json
中显得十分简洁也会减少整个模块目录的文件冗余。.gitignore
.gitignore
用于指定在使用 Git 进行版本控制时需要忽略的文件,语法与 .npmignore
类似,同样位于模块根目录:node_modules
+dist
+lib
+.husky/_
+
+.vscode/*
+.vs/*
+!.vscode/extensions.json
+
+*.tgz
+tsconfig.tsbuildinfo
+*.log
+
+kotori.dev.yml
发布构建产物
http://registry.npmjs.org
:npm config get registry
+# If not:
+# npm config set registry=http://registry.npmjs.org
npm login
npm publish
发布源码
git remote add origin git@github.com:kotorijs/kotori-plugin-my-project
git add .
+git commit -m 'feat: create a project'
+git push origin master
git tag v1.0.0
+git push --tags
收录至模块市场
src/public/data.json
文件,在该文件中追加你的模块的包名与描述:{
+ // ...
+ {
+ "name": "kotori-plugin-my-project",
+ "description": "这是一个"
+ }
+ // ...
+}
name
务必与发布到 npm 的包名一致description
不应过长,但需大致概括模块内容放在最后
`,45)]))}const g=i(e,[["render",p]]);export{c as __pageData,g as default};
diff --git a/assets/guide_start_publish.md.BdBbCqiy.lean.js b/assets/guide_start_publish.md.BdBbCqiy.lean.js
new file mode 100644
index 00000000..015010bd
--- /dev/null
+++ b/assets/guide_start_publish.md.BdBbCqiy.lean.js
@@ -0,0 +1,40 @@
+import{_ as i,c as a,a1 as n,o as t}from"./chunks/framework.C72X4JAr.js";const c=JSON.parse('{"title":"模块发布","description":"","frontmatter":{},"headers":[],"relativePath":"guide/start/publish.md","filePath":"guide/start/publish.md","lastUpdated":1716217371000}'),e={name:"guide/start/publish.md"};function p(l,s,h,r,o,k){return t(),a("div",null,s[0]||(s[0]=[n(`模块发布
构建产物
pnpm build
lib
文件夹,这在上一节已有提到,它是构建产物的输出目录,有必要的话可在 tsconfig.json
文件中更改:{
+ // ...
+ "compilerOptions": {
+ "rootDir": "./src", // 输入目录
+ "outDir": "./lib" // 输出目录
+ // ...
+ }
+}
tsconfig.json
的更多内容:TypeScript Documentation文件忽略
.npmignore
.npmignore
文件:node_modules
+src
+test
+
+tsconfig.json
+!README.md
.npmignore
采用的是黑名单机制显得很繁琐,因此 Kotori 模块的默认模板中并未使用该方式,也并不推荐。package.files
package.json
示例中会发现有一个以字符串数组为值的 files
配置项,其用于指定在使用 publish
时需要附带的文件与文件夹。{
+ "files": ["lib", "LICENSE", "README.md"],
+}
files
配置项优先级高于 .npmignore
,其直接写在 package.json
中显得十分简洁也会减少整个模块目录的文件冗余。.gitignore
.gitignore
用于指定在使用 Git 进行版本控制时需要忽略的文件,语法与 .npmignore
类似,同样位于模块根目录:node_modules
+dist
+lib
+.husky/_
+
+.vscode/*
+.vs/*
+!.vscode/extensions.json
+
+*.tgz
+tsconfig.tsbuildinfo
+*.log
+
+kotori.dev.yml
发布构建产物
http://registry.npmjs.org
:npm config get registry
+# If not:
+# npm config set registry=http://registry.npmjs.org
npm login
npm publish
发布源码
git remote add origin git@github.com:kotorijs/kotori-plugin-my-project
git add .
+git commit -m 'feat: create a project'
+git push origin master
git tag v1.0.0
+git push --tags
收录至模块市场
src/public/data.json
文件,在该文件中追加你的模块的包名与描述:{
+ // ...
+ {
+ "name": "kotori-plugin-my-project",
+ "description": "这是一个"
+ }
+ // ...
+}
name
务必与发布到 npm 的包名一致description
不应过长,但需大致概括模块内容放在最后
`,45)]))}const g=i(e,[["render",p]]);export{c as __pageData,g as default};
diff --git a/assets/guide_start_setup.md.CuuAG2B-.js b/assets/guide_start_setup.md.CuuAG2B-.js
new file mode 100644
index 00000000..3913c184
--- /dev/null
+++ b/assets/guide_start_setup.md.CuuAG2B-.js
@@ -0,0 +1,144 @@
+import{_ as i,c as a,a1 as n,o as t}from"./chunks/framework.C72X4JAr.js";const d=JSON.parse('{"title":"项目构建","description":"","frontmatter":{},"headers":[],"relativePath":"guide/start/setup.md","filePath":"guide/start/setup.md","lastUpdated":1723345558000}'),p={name:"guide/start/setup.md"};function l(h,s,k,e,E,o){return t(),a("div",null,s[0]||(s[0]=[n(`项目构建
基于 create-kotori 快速搭建工作区
create-kotori <project-name>
pnpm create kotori@latest
npm install create-kotori -g
+create-kotori my-project
项目结构
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
:{
+ "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"
+ }
+}
{
+ "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
需要满足一系列来自 Kotori 的约定,Kotori 程序只有在其合法时才会加载该模块。不过当前你无需关心这个问题,元数据与 package.json
约定将放在第三章中讲解。以下是该 package.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 Docsindex.ts
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.yml
中设置该适配器即可:adapter:
+ developer:
+ extends: sandbox
+ master: 1
+ port: 2333
运行模式
src/.ts
。kotori.yml
,Dev 模式下读取 kotori.dev.yml
,两者用法与实际效果均一致,旨在区分不同模式下不同配置。pnpm dev
http://localhost:2333
即可进入沙盒环境,输入 /echo Hello,Kotori!
以查看效果:项目构建
基于 create-kotori 快速搭建工作区
create-kotori <project-name>
pnpm create kotori@latest
npm install create-kotori -g
+create-kotori my-project
项目结构
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
:{
+ "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"
+ }
+}
{
+ "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
需要满足一系列来自 Kotori 的约定,Kotori 程序只有在其合法时才会加载该模块。不过当前你无需关心这个问题,元数据与 package.json
约定将放在第三章中讲解。以下是该 package.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 Docsindex.ts
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.yml
中设置该适配器即可:adapter:
+ developer:
+ extends: sandbox
+ master: 1
+ port: 2333
运行模式
src/.ts
。kotori.yml
,Dev 模式下读取 kotori.dev.yml
,两者用法与实际效果均一致,旨在区分不同模式下不同配置。pnpm dev
http://localhost:2333
即可进入沙盒环境,输入 /echo Hello,Kotori!
以查看效果: