Skip to content

Commit

Permalink
feat(webui): support runtime plugin switch
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Apr 17, 2021
1 parent 0e26723 commit 722f343
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 22 deletions.
3 changes: 0 additions & 3 deletions packages/koishi-core/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,6 @@ export class App extends Context {
if (options.selfUrl) options.selfUrl = trimSlash(options.selfUrl)
this.options = merge(options, App.defaultConfig)
this.registry.set(null, {
parent: null,
context: null,
config: null,
children: [],
disposables: [],
})
Expand Down
12 changes: 8 additions & 4 deletions packages/koishi-core/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Logger, defineProperty, remove, segment } from 'koishi-utils'
import { Logger, defineProperty, remove, segment, Random } from 'koishi-utils'
import { Command } from './command'
import { Session } from './session'
import { User, Channel, Database, Assets } from './database'
Expand Down Expand Up @@ -32,9 +32,11 @@ export namespace Plugin {
export type Config<T extends Plugin> = T extends Function<infer U> ? U : T extends Object<infer U> ? U : never

export interface State extends Meta {
parent: State
context: Context
config: Config<Plugin>
id?: string
parent?: State
context?: Context
config?: Config<Plugin>
plugin?: Plugin
children: Plugin[]
disposables: Disposable[]
}
Expand Down Expand Up @@ -207,6 +209,8 @@ export class Context {
const ctx: this = Object.create(this)
defineProperty(ctx, '_plugin', plugin)
this.app.registry.set(plugin, {
plugin,
id: Random.uuid(),
context: this,
config: options,
parent: this.state,
Expand Down
11 changes: 8 additions & 3 deletions packages/plugin-webui/client/views/plugins/plugin-view.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
<i class="fas fa-angle-right" :class="{ show }"/>
{{ data.name }}
</span>
<span class="complexity">{{ data.complexity }}</span>
<span class="complexity">{{ data.disabled ? '-' : data.complexity }}</span>
<span class="operation">
<k-button class="right" frameless :disabled="data.sideEffect" @click="send('switch', data.id)">{{ '停用' }}</k-button>
<k-button class="right" frameless :disabled="data.sideEffect" @click="toggle(data.id)">{{ data.disabled ? '启用' : '停用' }}</k-button>
</span>
</div>
<k-collapse v-if="data.children.length">
Expand All @@ -21,14 +21,19 @@
<script setup lang="ts">
import type { Registry } from '~/server'
import { send } from '~/client'
import { send, user } from '~/client'
import { ref, defineProps } from 'vue'
import PluginView from './plugin-view.vue'
const show = ref(false)
defineProps<{ data: Registry.PluginData }>()
function toggle(plugin: string) {
const { id, token } = user.value
send('switch', { plugin, id, token })
}
</script>

<style lang="scss">
Expand Down
10 changes: 8 additions & 2 deletions packages/plugin-webui/client/views/plugins/plugins.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<k-card class="page-plugins" title="插件列表">
<k-card class="page-plugins" :class="{ authorized: user?.authority >= 4 }" title="插件列表">
<div class="header plugin-item">
<span class="title">插件名</span>
<span class="complexity">复杂度</span>
Expand All @@ -14,7 +14,7 @@
<script setup lang="ts">
import PluginView from './plugin-view.vue'
import { registry } from '~/client'
import { registry, user } from '~/client'
</script>
Expand All @@ -31,4 +31,10 @@ import { registry } from '~/client'
}
}
.page-plugins:not(.authorized) {
.complexity, .operation {
display: none;
}
}
</style>
63 changes: 53 additions & 10 deletions packages/plugin-webui/src/data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Argv, Assets, Bot, Context, Platform, Plugin, Time, noop } from 'koishi-core'
import { Argv, Assets, Bot, Context, noop, Platform, Plugin, Time } from 'koishi-core'
import { cpus } from 'os'
import { mem } from 'systeminformation'

Expand Down Expand Up @@ -182,32 +182,72 @@ export namespace Meta {
export type Extension = () => Promise<Partial<Payload>>
}

const original = Symbol('webui.original-plugin')

function debounce(callback: Function, ms: number) {
let timer: number
return function () {
if (timer) clearTimeout(timer)
timer = setTimeout(callback, ms)
}
}

export class Registry implements DataSource<Registry.Payload> {
payload: Registry.Payload
promise: Promise<void>

constructor(private ctx: Context, public config: Registry.Config) {
ctx.on('plugin-added', this.update)
ctx.on('plugin-removed', this.update)
}

update = async () => {
this.ctx.webui.broadcast('registry', await this.get(true))
get registry() {
return this.ctx.app.registry
}

update = debounce(async () => {
this.ctx.webui.broadcast('registry', await this.get(true))
}, 0)

async get(forced = false) {
if (this.payload && !forced) return this.payload
this.payload = { pluginCount: 0 } as Registry.Payload
this.payload.plugins = this.traverse(null)
this.payload.plugins = this.traverse(null).children
return this.payload
}

traverse = (plugin: Plugin): Registry.PluginData[] => {
const state = this.ctx.app.registry.get(plugin)
const children = state.children.flatMap(this.traverse, 1)
const { name, sideEffect } = state
if (!name) return children
async switch(id: string) {
await this.promise
for (const [plugin, state] of this.registry) {
if (id !== state.id) continue
const replacer = plugin[original] || {
[original]: state.plugin,
name: state.name,
apply: () => {},
}
this.promise = this.ctx.dispose(plugin)
state.context.plugin(replacer, state.config)
}
}

traverse = (plugin: Plugin): Registry.PluginData => {
const state = this.registry.get(plugin)
this.payload.pluginCount += 1
return [{ name, sideEffect, children }]
let complexity = 1 + state.disposables.length
const children: Registry.PluginData[] = []
state.children.forEach((plugin) => {
const data = this.traverse(plugin)
complexity += data.complexity
if (data.name) {
children.push(data)
} else {
children.push(...data.children)
}
})
const { id, name, sideEffect } = state
const disabled = plugin && !!plugin[original]
children.sort((a, b) => a.name > b.name ? 1 : -1)
return { id, name, sideEffect, disabled, children, complexity }
}
}

Expand All @@ -216,7 +256,10 @@ export namespace Registry {
}

export interface PluginData extends Plugin.Meta {
id: string
disabled: boolean
children: PluginData[]
complexity: number
}

export interface Payload {
Expand Down
7 changes: 7 additions & 0 deletions packages/plugin-webui/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,11 @@ export namespace WebServer {
this.send('user', omit(user, ['password']))
this.authority = user.authority
}

listeners.switch = async function ({ id, token, plugin }) {
const user = await this.validate(id, token, ['name', 'authority'])
if (!user) return
if (user.authority < 4) return this.send('unauthorized')
this.webui.sources.registry.switch(plugin)
}
}

0 comments on commit 722f343

Please sign in to comment.