Commit a13f8e41 authored by nanahira's avatar nanahira

add UsingService

parent 4f828a42
......@@ -8,292 +8,6 @@
npm install koishi-thirdeye koishi
```
## 快速入门
可以简单定义类以快速开发 Koishi 插件。
```ts
import { DefinePlugin, SchemaProperty, CommandUsage, PutOption, UseCommand, OnApply, KoaContext, UseMiddleware, UseEvent, Get } from 'koishi-thirdeye';
import { Context, Session } from 'koishi';
export class MyPluginConfig {
@SchemaProperty({ default: 'bar' })
foo: string;
}
@DefinePlugin({ name: 'my-plugin', schema: MyPluginConfig })
export default class MyPlugin implements OnApply {
constructor(private ctx: Context, private config: Partial<MyPluginConfig>) {
}
onApply() {
// 该方法会在插件加载时调用,用于在上下文中注册事件等操作。
}
@UseMiddleware()
simpleMiddleware(session: Session, next: NextFunction) {
return next();
}
@UseEvent('message')
onMessage(session: Session) {
}
@UseCommand('echo', '命令描述')
@CommandUsage('命令说明')
onEcho(@PutOption('content', '-c <content:string> 命令参数') content: string) {
return content;
}
@Get('/ping')
onPing(ctx: KoaContext) {
ctx.body = 'pong';
}
}
```
## 使用
使用 koishi-thirdeye 编写的插件,需要在插件类上使用 `@DefinePlugin(options: DefinePluginRegistrationOptions)` 装饰器。
您可以在参数中指定该插件的基本信息。
* `name` 插件名称。
* `schema` 插件的配置描述模式。可以是 Schema 描述模式,也可以是由 `schemastery-gen` 生成的 Schema 类。
koishi-thirdeye 内建了 `schemastery-gen` 的支持。只需要导入这1个包即可。另外,系统会自动进行 `@RegisterSchema` 的配置描述的注册。
最基本的插件定义方式如下:
```ts
import { DefinePlugin, SchemaProperty, InjectConfig } from 'koishi-thirdeye';
import { Context, Session } from 'koishi';
export class MyPluginConfig {
@SchemaProperty({ default: 'bar' })
foo: string;
}
@DefinePlugin({ name: 'my-plugin', schema: MyPluginConfig })
export default class MyPlugin {
constructor(private ctx: Context, private config: Partial<MyPluginConfig>) {
}
@InjectConfig()
private config: Config;
}
```
### 插件基类
为了简化不必要的代码,在您的类没有其他继承的情况下,可以继承于 `BasePlugin<Config>` 类,以省去不必要的构造函数等声明。
您可以使用 `this.ctx` 以及 `this.config` 进行访问上下文对象以及插件配置。因此上面的例子可以简化为下面的代码:
> `@DefinePlugin` 装饰器不可省略。
```ts
import { DefinePlugin, SchemaProperty, BasePlugin } from 'koishi-thirdeye';
import { Context, Session } from 'koishi';
export class MyPluginConfig {
@SchemaProperty({ default: 'bar' })
foo: string;
}
@DefinePlugin({ name: 'my-plugin', schema: MyPluginConfig })
export default class MyPlugin extends BasePlugin<MyPluginConfig> {
}
```
## API
### 注入
您可以在类成员变量中,使用下列装饰器进行注入成员变量。 **注入的变量在构造函数中无效。** 请在 `onApply` 等生命周期钩子函数中调用。
* `@InjectContext(select?: Selection)` 注入上下文对象。 **注入的上下文对象会受全局选择器影响。**
* `@InjectApp()` 注入 Koishi 实例对象。
* `@InjectLogger(name: string)` 注入 Koishi 日志记录器。
### 声明周期钩子
下列方法是一些生命周期方法。这些方法会在插件特定生命周期中调用,便于注册方法等操作。
要声明钩子,让插件类实现对应的接口,并添加对应的方法即可。
```ts
// 在插件加载时调用
export interface OnApply {
onApply(): void | Promise<void>;
}
// 在 Koishi 实例启动完毕时调用
export interface OnConnect {
onConnect(): void | Promise<void>;
}
// 在插件卸载或 Koishi 实例关闭时调用
export interface OnDisconnect {
onDisconnect(): void | Promise<void>;
}
```
### 选择器
选择器装饰器可以注册在插件类顶部,也可以注册在插件方法函数。
插件类顶部定义的上下文选择器是全局的,会影响使用 `@Inject``@InjectContext` 注入的任何上下文对象,以及构造函数中传入的上下文对象。
选择器的使用请参照 [Koishi 文档](https://koishi.js.org/v4/guide/plugin/context.html#%E4%BD%BF%E7%94%A8%E9%80%89%E6%8B%A9%E5%99%A8)
* `@OnUser(value)` 等价于 `ctx.user(value)`
* `@OnSelf(value)` 等价于 `ctx.self(value)`
* `@OnGuild(value)` 等价于 `ctx.guild(value)`
* `@OnChannel(value)` 等价于 `ctx.channel(value)`
* `@OnPlatform(value)` 等价于 `ctx.platform(value)`
* `@OnPrivate(value)` 等价于 `ctx.private(value)`
* `@OnSelection(value)` 等价于 `ctx.select(value)`
* `@OnContext((ctx: Context) => Context)` 手动指定上下文选择器,用于不支持的选择器。例如,
```ts
@OnContext(ctx => ctx.platform('onebot'))
```
### 注册方法
* `@UseMiddleware(prepend?: boolean)` 注册中间件,等价于 `ctx.middleware((session, next) => { }, prepend)`[参考](https://koishi.js.org/v4/guide/message/message.html#%E6%B3%A8%E5%86%8C%E5%92%8C%E5%8F%96%E6%B6%88%E4%B8%AD%E9%97%B4%E4%BB%B6)
* `@UseEvent(name: EventName, prepend?: boolean)` 注册事件监听器。等价于 `ctx.on(name, (session) => { }, prepend)`[参考](https://koishi.js.org/v4/guide/plugin/lifecycle.html#%E4%BA%8B%E4%BB%B6%E7%B3%BB%E7%BB%9F)
* `@UsePlugin()` 使用该方法注册插件。在 Koishi 实例注册时该方法会自动被调用。该方法需要返回插件定义,可以使用 `PluginDef(plugin, options, select)` 方法生成。 [参考](https://koishi.js.org/v4/guide/plugin/plugin.html#%E5%AE%89%E8%A3%85%E6%8F%92%E4%BB%B6)
* `@UseCommand(def: string, desc?: string, config?: Command.Config)` 注册指令。指令系统可以参考 [Koishi 文档](https://koishi.js.org/guide/command.html) 。指令回调参数位置和类型和 Koishi 指令一致。
* 若指定 `config.empty` 则不会注册当前函数为 action,用于没有 action 的父指令。
* `@Get(path: string)` `@Post(path: string)` 在 Koishi 的 Koa 路由中注册 GET/POST 路径。此外, PUT PATCH DELETE 等方法也有所支持。
### 指令描述装饰器
koishi-thirdeye 使用一组装饰器进行描述指令的行为。这些装饰器需要和 `@UseCommand(def)` 一起使用。
* `@CommandDescription(text: string)` 指令描述。等价于 `ctx.command(def, desc)` 中的描述。
* `@CommandUsage(text: string)` 指令介绍。等价于 `cmd.usage(text)`
* `@CommandExample(text: string)` 指令示例。等价于 `cmd.example(text)`
* `@CommandAlias(def: string)` 指令别名。等价于 `cmd.alias(def)`
* `@CommandShortcut(def: string, config?: Command.Shortcut)` 指令快捷方式。等价于 `cmd.shortcut(def, config)`
* `@CommandDef((cmd: Command) => Command)` 手动定义指令信息,用于不支持的指令类型。
### 指令参数
指令参数也使用一组装饰器对指令参数进行注入。下列装饰器应对由 `@UseCommand` 配置的类成员方法参数进行操作。
* `@PutArgv()` 注入 `Argv` 对象。
* `@PutSession(field?: keyof Session)` 注入 `Session` 对象,或 `Session` 对象的指定字段。
* `@PutArg(index: number)` 注入指令的第 n 个参数。
* `@PutOption(name: string, desc: string, config: Argv.OptionConfig = {})` 给指令添加选项并注入到该参数。等价于 `cmd.option(name, desc, config)`
* `@PutUser(fields: string[])` 添加一部分字段用于观测,并将 User 对象注入到该参数。
* `@PutChannel(fields: string[])` 添加一部分字段用于观测,并将 Channel 对象注入到该参数。
关于 Koishi 的观察者概念详见 [Koishi 文档](https://koishi.js.org/v4/guide/database/observer.html#%E8%A7%82%E5%AF%9F%E8%80%85%E5%AF%B9%E8%B1%A1)
* `@PutUserName(useDatabase: boolean = true)` 注入当前用户的用户名。
* `useDatabase` 是否尝试从数据库获取用户名。
### 上下文 Service 交互
您可以使用装饰器与 Koishi 的 Service 系统进行交互。
#### 注入上下文 Service
注入的 Service 通常来自其他 Koishi 插件。
```ts
import { Inject, UseEvent } from 'koishi-thirdeye';
import { Cache } from 'koishi';
@DefinePlugin({ name: 'my-plugin' })
export class MyPlugin {
constructor(private ctx: Context, private config: any) {
}
// 注入 Service
@Inject('cache')
private cache2: Cache;
// 成员变量名与 Service 名称一致时 name 可省略。
@Inject()
private cache: Cache;
// 成员类型是 Context 会自动注入 Koishi 上下文,等效于 `@InjectContext()` 。
@Inject()
private anotherCtx: Context;
@UseEvent('service/cache')
async onCacheAvailable() {
// 建议监听 Service 事件
const user = this.cache.get('user', '111111112');
}
}
```
#### 提供上下文 Service
您也可以直接使用 `@Provide` 方法进行 Koishi 的 Service 提供,供其他插件使用。
```ts
import { Provide } from 'koishi-thirdeye';
// 需要定义 Service 类型
declare module 'koishi' {
namespace Context {
interface Services {
myService: MyDatabasePlugin;
}
}
}
// `@Provide(name)` 装饰器会自动完成 `Context.service(name)` 的声明操作
@Provide('myService')
@DefinePlugin({ name: 'my-database' })
export class MyDatabasePlugin {
// 该类会作为 Koishi 的 Service 供其他 Koishi 插件进行引用
}
```
#### 定义
* `@Inject(name?: string, addUsing?: boolean)` 在插件类某一属性注入特定上下文 Service 。 `name` 若为空则默认为类方法名。
`addUsing` 若为 `true` 则会为插件注册的 Service 。
特别的,为了编写简便,如果成员类型也是 Context 则会注入 Koishi 上下文,等效于 `@InjectContext()`
* `@Provide(name: string, options?: ProvideOptions)` 使用该插件提供 Service 。会自动完成 Koishi 的 `Context.service(name)` 声明操作。
* `immediate` 会在插件加载阶段瞬间完成 Service 注册。
## 文档
https://koishi.js.org/guide/misc/decorator.html
......@@ -13,6 +13,7 @@ import {
KoishiDoRegister,
KoishiDoRegisterKeys,
KoishiOnContextScope,
KoishiPartialUsing,
KoishiRouteDef,
KoishiServiceInjectSym,
KoishiServiceInjectSymKeys,
......@@ -313,7 +314,21 @@ export const InjectLogger = (name?: string) =>
);
export const Caller = () =>
InjectSystem((obj) => {
const ctx = obj.__ctx;
const targetCtx: Context = ctx[Context.current] || ctx;
const targetCtx: Context = obj[Context.current] || obj.__ctx;
return targetCtx;
});
export function UsingService(
...services: (keyof Context.Services)[]
): ClassDecorator & MethodDecorator {
return (obj, key?) => {
for (const service of services) {
if (!key) {
// fallback to KoishiAddUsingList
Metadata.appendUnique(KoishiAddUsingList, service)(obj.constructor);
} else {
Metadata.appendUnique(KoishiPartialUsing, service)(obj, key);
}
}
};
}
......@@ -20,6 +20,7 @@ export const KoishiServiceProvideSym = 'KoishiServiceProvideSym';
export const KoishiSystemInjectSym = 'KoishiSystemInjectSym';
export const KoishiSystemInjectSymKeys = 'KoishiSystemInjectSymKeys';
export const KoishiAddUsingList = 'KoishiAddUsingList';
export const KoishiPartialUsing = 'KoishiPartialUsing';
// metadata map
......@@ -31,6 +32,7 @@ export interface MetadataArrayMap {
KoishiServiceInjectSymKeys: string;
KoishiSystemInjectSymKeys: string;
KoishiAddUsingList: keyof Context.Services;
KoishiPartialUsing: keyof Context.Services;
}
export interface MetadataMap {
......
......@@ -6,6 +6,7 @@ import {
Schema,
User,
WebSocketLayer,
Plugin,
} from 'koishi';
import {
CommandPutConfig,
......@@ -16,6 +17,7 @@ import {
KoishiDoRegisterKeys,
KoishiModulePlugin,
KoishiOnContextScope,
KoishiPartialUsing,
KoishiServiceInjectSym,
KoishiServiceInjectSymKeys,
KoishiServiceProvideSym,
......@@ -89,10 +91,8 @@ export function DefinePlugin<T = any>(
__wsLayers: WebSocketLayer[];
_handleSystemInjections() {
// console.log('Handling system injection');
const injectKeys = reflector.getArray(KoishiSystemInjectSymKeys, this);
for (const key of injectKeys) {
// console.log(`Processing ${key}`);
const valueFunction = reflector.get(KoishiSystemInjectSym, this, key);
Object.defineProperty(this, key, {
configurable: true,
......@@ -103,10 +103,8 @@ export function DefinePlugin<T = any>(
}
_handleServiceInjections() {
// console.log('Handling service injection');
const injectKeys = reflector.getArray(KoishiServiceInjectSymKeys, this);
for (const key of injectKeys) {
// console.log(`Processing ${key}`);
const name = reflector.get(KoishiServiceInjectSym, this, key);
Object.defineProperty(this, key, {
enumerable: true,
......@@ -231,22 +229,13 @@ export function DefinePlugin<T = any>(
}
}
_registerDeclarationsFor(methodKey: keyof C & string) {
// console.log(`Handling declaration for ${methodKey}`);
_registerDeclarationsProcess(methodKey: keyof C & string, ctx: Context) {
const regData = reflector.get(KoishiDoRegister, this, methodKey);
if (!regData) {
return;
}
// console.log(`Type: ${regData.type}`);
const baseContext = getContextFromFilters(
this.__ctx,
reflector.getArray(KoishiOnContextScope, this, methodKey),
);
switch (regData.type) {
case 'middleware':
const { data: midPrepend } =
regData as DoRegisterConfig<'middleware'>;
baseContext.middleware(
ctx.middleware(
(session, next) => this[methodKey](session, next),
midPrepend,
);
......@@ -254,7 +243,7 @@ export function DefinePlugin<T = any>(
case 'onevent':
const { data: eventData } = regData as DoRegisterConfig<'onevent'>;
const eventName = eventData.name;
baseContext.on(
ctx.on(
eventName,
(...args: any[]) => this[methodKey](...args),
eventData.prepend,
......@@ -264,18 +253,18 @@ export function DefinePlugin<T = any>(
const { data: beforeEventData } =
regData as DoRegisterConfig<'beforeEvent'>;
const beforeEventName = beforeEventData.name;
baseContext.before(
ctx.before(
beforeEventName,
(...args: any[]) => this[methodKey](...args),
beforeEventData.prepend,
);
case 'plugin':
this._applyInnerPlugin(baseContext, methodKey);
this._applyInnerPlugin(ctx, methodKey);
break;
case 'command':
const { data: commandData } =
regData as DoRegisterConfig<'command'>;
let command = baseContext.command(
let command = ctx.command(
commandData.def,
commandData.desc,
commandData.config,
......@@ -312,13 +301,13 @@ export function DefinePlugin<T = any>(
const realPath = routeData.path.startsWith('/')
? routeData.path
: `/${routeData.path}`;
baseContext.router[routeData.method](realPath, (ctx, next) =>
ctx.router[routeData.method](realPath, (ctx, next) =>
this[methodKey](ctx, next),
);
break;
case 'ws':
const { data: wsPath } = regData as DoRegisterConfig<'ws'>;
const layer = baseContext.router.ws(wsPath, (socket, req) =>
const layer = ctx.router.ws(wsPath, (socket, req) =>
this[methodKey](socket, req),
);
this.__wsLayers.push(layer);
......@@ -328,19 +317,44 @@ export function DefinePlugin<T = any>(
}
}
_registerDeclarationsFor(methodKey: keyof C & string) {
if (!reflector.get(KoishiDoRegister, this, methodKey)) {
return;
}
const ctx = getContextFromFilters(
this.__ctx,
reflector.getArray(KoishiOnContextScope, this, methodKey),
);
const partialUsing = reflector.getArray(
KoishiPartialUsing,
this,
methodKey,
);
if (partialUsing.length) {
const name = `${options.name || originalClass.name}-${methodKey}`;
const innerPlugin: Plugin.Object = {
name,
using: partialUsing,
apply: (innerCtx) =>
this._registerDeclarationsProcess(methodKey, innerCtx),
};
ctx.plugin(innerPlugin);
} else {
this._registerDeclarationsProcess(methodKey, ctx);
}
}
_registerDeclarations() {
const methodKeys = reflector.getArray(
KoishiDoRegisterKeys,
this,
) as (keyof C & string)[];
// console.log(methodKeys);
methodKeys.forEach((methodKey) =>
this._registerDeclarationsFor(methodKey),
);
}
_handleServiceProvide(immediate: boolean) {
// console.log(`Handling service provide`);
const providingServices = [
...reflector.getArray(KoishiServiceProvideSym, originalClass),
...reflector.getArray(KoishiServiceProvideSym, this),
......@@ -352,7 +366,6 @@ export function DefinePlugin<T = any>(
}
_registerAfterInit() {
// console.log(`Handling after init.`);
this.__ctx.on('ready', async () => {
if (typeof this.onConnect === 'function') {
await this.onConnect();
......
import { DefinePlugin } from '../src/register';
import { BasePlugin } from '../src/base-plugin';
import { Caller, Provide } from '../src/decorators';
import { App } from 'koishi';
declare module 'koishi' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Context {
interface Services {
callerTester: CallerTester;
}
}
}
@Provide('callerTester')
@DefinePlugin()
class CallerTester extends BasePlugin<any> {
@Caller()
caller: string;
}
describe('Caller', () => {
let app: App;
beforeEach(async () => {
app = new App();
app.plugin(CallerTester);
await app.start();
});
it('should put caller with correct values', async () => {
const ctx1 = app.any();
const ctx2 = app.any();
const caller1 = ctx1.callerTester.caller;
const caller2 = ctx2.callerTester.caller;
expect(caller1).toEqual(ctx1);
expect(caller2).toEqual(ctx2);
expect(app.callerTester.caller).toEqual(app);
});
});
import { App, Context } from 'koishi';
import { DefinePlugin } from '../src/register';
import { Inject, Provide, UseEvent } from '../src/decorators';
import { Inject, Provide, UseEvent, UsingService } from '../src/decorators';
import { BasePlugin } from '../src/base-plugin';
declare module 'koishi' {
// eslint-disable-next-line @typescript-eslint/no-namespace
......@@ -10,24 +11,39 @@ declare module 'koishi' {
myEagerProvider: MyEagerProvider;
myConsumer: MyConsumer;
myUsingConsumer: MyUsingConsumer;
myPartialConsumer: MyPartialConsumer;
dummyProvider: any;
}
}
interface EventMap {
'pang'(message: string): Promise<string>;
'pong'(message: string): Promise<string>;
}
}
@Provide('myProvider')
@DefinePlugin()
class MyProvider {
class MyProvider extends BasePlugin<any> {
ping() {
return 'pong';
}
dispose() {
return this.ctx.dispose();
}
}
@Provide('myEagerProvider', { immediate: true })
@DefinePlugin()
class MyEagerProvider {
class MyEagerProvider extends BasePlugin<any> {
ping() {
return 'pong eager';
}
dispose() {
return this.ctx.dispose();
}
}
@Provide('myConsumer', { immediate: true })
......@@ -74,6 +90,32 @@ class MyUsingConsumer {
this.eagerPongResult = this.myEagerProvider.ping();
}
}
emitResult: string;
}
@Provide('myPartialConsumer', { immediate: true })
@DefinePlugin()
class MyPartialConsumer {
@Inject()
dummyProvider: number;
pongResult: string;
@UsingService('dummyProvider')
@UseEvent('pang')
async onPang(content: string) {
const msg = `pang: ${content}`;
console.log(msg);
return msg;
}
@UseEvent('pong')
async onPong(content: string) {
const msg = `pong: ${content}`;
console.log(msg);
return msg;
}
}
describe('On service', () => {
......@@ -111,4 +153,28 @@ describe('On service', () => {
//expect(app.myUsingConsumer.eagerPongResult).toBe('pong eager');
//expect(app.myUsingConsumer.pongResult).toBe('pong');
});
/*
it('Should handle partial using deps', async () => {
Context.service('dummyProvider');
app = new App();
app.on('service', (name) => {
console.log('service', name);
});
await app.start();
app.plugin(MyPartialConsumer);
expect(app.myPartialConsumer).toBeDefined();
expect(await app.waterfall('pang', 'hello')).toEqual('hello');
expect(await app.waterfall('pong', 'hello')).toEqual('pong: hello');
app.dummyProvider = { foo: 'bar' };
expect(await app.waterfall('pang', 'hello')).toEqual('pang: hello');
expect(await app.waterfall('pong', 'hello')).toEqual('pong: hello');
app.dummyProvider = undefined;
expect(await app.waterfall('pang', 'hi')).toEqual('hi');
expect(await app.waterfall('pong', 'hi')).toEqual('pong: hi');
app.dummyProvider = { foo: 'baz' };
expect(await app.waterfall('pang', 'hi')).toEqual('pang: hi');
expect(await app.waterfall('pong', 'hi')).toEqual('pong: hi');
});
*/
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment