mirror of
https://github.com/xfgryujk/blivechat.git
synced 2024-12-25 20:30:28 +08:00
添加插件管理前端
This commit is contained in:
parent
e1d0473627
commit
29ff61748b
@ -24,4 +24,7 @@ data/emoticons/*
|
||||
!data/custom_public/
|
||||
data/custom_public/*
|
||||
!data/custom_public/README.txt
|
||||
!data/plugins/
|
||||
data/plugins/*
|
||||
!data/plugins/.gitkeep
|
||||
log/*
|
||||
|
@ -24,6 +24,8 @@ class _AdminHandlerBase(api.base.ApiHandler):
|
||||
if not cfg.enable_admin_plugins:
|
||||
raise tornado.web.HTTPError(403)
|
||||
|
||||
logger.info('client=%s requesting admin plugin, cls=%s', self.request.remote_ip, type(self).__name__)
|
||||
|
||||
super().prepare()
|
||||
|
||||
def _get_plugin(self):
|
||||
@ -60,16 +62,21 @@ class EnableHandler(_AdminHandlerBase):
|
||||
enabled = bool(self.json_args.get('enabled', False))
|
||||
|
||||
plugin = self._get_plugin()
|
||||
old_enabled = plugin.enabled
|
||||
is_switch_success = True
|
||||
msg = ''
|
||||
try:
|
||||
plugin.enabled = enabled
|
||||
except services.plugin.StartTooFrequently as e:
|
||||
except services.plugin.SwitchTooFrequently as e:
|
||||
is_switch_success = False
|
||||
msg = str(e)
|
||||
plugin.enabled = False
|
||||
except services.plugin.StartPluginError as e:
|
||||
plugin.enabled = old_enabled
|
||||
except services.plugin.SwitchPluginError as e:
|
||||
is_switch_success = False
|
||||
msg = str(e)
|
||||
self.write({
|
||||
'enabled': plugin.enabled,
|
||||
'isSwitchSuccess': is_switch_success,
|
||||
'msg': msg
|
||||
})
|
||||
|
||||
|
16
frontend/src/api/plugins.js
Normal file
16
frontend/src/api/plugins.js
Normal file
@ -0,0 +1,16 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export async function getPlugins() {
|
||||
return (await axios.get('/api/plugin/plugins')).data
|
||||
}
|
||||
|
||||
export async function setEnabled(pluginId, enabled) {
|
||||
return (await axios.post('/api/plugin/enable_plugin', {
|
||||
pluginId,
|
||||
enabled
|
||||
})).data
|
||||
}
|
||||
|
||||
export async function openAdminUi(pluginId) {
|
||||
return (await axios.post('/api/plugin/open_admin_ui', { pluginId })).data
|
||||
}
|
@ -3,6 +3,7 @@ export default {
|
||||
home: 'Home',
|
||||
stylegen: 'Style Generator',
|
||||
help: 'Help',
|
||||
plugins: 'Plugins',
|
||||
links: 'Links',
|
||||
projectAddress: 'Project Address',
|
||||
discussion: 'Discussions',
|
||||
@ -169,5 +170,25 @@ export default {
|
||||
sendGift: 'Sent {giftName}x{num}',
|
||||
membershipTitle: 'New member',
|
||||
tickerMembership: 'Member'
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
plugins: 'Plugins',
|
||||
help: 'Help',
|
||||
helpContent: `\
|
||||
<p>Plugins can add more functionality to blivechat, such as message logging, text to speech, song requests, etc. Plugins may
|
||||
be developed by third-party authors, and the security and quality are the responsibility of the plugin author. You can
|
||||
find some published plugins in <a target="_blank" href="https://github.com/xfgryujk/blivechat/discussions/categories/%E6%8F%92%E4%BB%B6"
|
||||
>GitHub Discussions</a></p>
|
||||
<p>Plugin installation method: Put the extracted plugin directory into the "data/plugins" directory, then restart blivechat</p>
|
||||
<p>Notes: Most plugins require enabling the "Relay messages by the server" option and connecting to the room
|
||||
in order to receive messages</p>
|
||||
<p><a target="_blank" href="https://github.com/xfgryujk/blivechat/wiki/%E6%8F%92%E4%BB%B6%E7%B3%BB%E7%BB%9F">
|
||||
Plugin development documentation</a></p>
|
||||
`,
|
||||
author: 'Author: ',
|
||||
disabledByServer: 'Administration for plugins is disabled by the server',
|
||||
admin: 'Admin',
|
||||
connected: 'Connected',
|
||||
unconnected: 'Unconnected',
|
||||
},
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ export default {
|
||||
home: 'トップページ',
|
||||
stylegen: 'スタイルジェネレータ',
|
||||
help: 'ヘルプ',
|
||||
plugins: 'プラグイン',
|
||||
links: 'リンク',
|
||||
projectAddress: 'プロジェクトアドレス',
|
||||
discussion: '議論',
|
||||
@ -169,5 +170,25 @@ export default {
|
||||
sendGift: '{giftName}x{num} を贈りました',
|
||||
membershipTitle: '新規メンバー',
|
||||
tickerMembership: 'メンバー'
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
plugins: 'プラグイン',
|
||||
help: 'ヘルプ',
|
||||
helpContent: `\
|
||||
<p>プラグインは、メッセージの記録、テキスト読み上げ、曲リクエストなど、blivechatにさらなる機能を追加できます。
|
||||
プラグインはサードパーティの作者によって開発される場合があり、セキュリティと品質はプラグイン作者の責任です。
|
||||
いくつかの公開されたプラグインは<a target="_blank" href="https://github.com/xfgryujk/blivechat/discussions/categories/%E6%8F%92%E4%BB%B6"
|
||||
>GitHub Discussions</a>で見つけることができます</p>
|
||||
<p>プラグインのインストール方法:抽出されたプラグインディレクトリを「data/plugins」ディレクトリに配置し、blivechatを再起動します</p>
|
||||
<p>注意:ほとんどのプラグインは、「サーバを介してメッセージを転送する」オプションを有効にし、メッセージを受信するために
|
||||
ルームに接続する必要があります</p>
|
||||
<p><a target="_blank" href="https://github.com/xfgryujk/blivechat/wiki/%E6%8F%92%E4%BB%B6%E7%B3%BB%E7%BB%9F"
|
||||
>プラグイン開発ドキュメント</a></p>
|
||||
`,
|
||||
author: '作者:',
|
||||
disabledByServer: 'プラグインの管理は、サーバーによって無効にされています',
|
||||
admin: '管理',
|
||||
connected: '接続済み',
|
||||
unconnected: '未接続',
|
||||
},
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ export default {
|
||||
home: '首页',
|
||||
stylegen: '样式生成器',
|
||||
help: '帮助',
|
||||
plugins: '插件',
|
||||
links: '常用链接',
|
||||
projectAddress: '项目地址',
|
||||
discussion: '反馈 / 交流',
|
||||
@ -169,5 +170,22 @@ export default {
|
||||
sendGift: '赠送 {giftName}x{num}',
|
||||
membershipTitle: '新会员',
|
||||
tickerMembership: '会员'
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
plugins: '插件',
|
||||
help: '帮助',
|
||||
helpContent: `\
|
||||
<p>插件可以给blivechat添加更多功能,比如消息日志、语音播报、点歌等。插件可能由第三方作者开发,其安全性和质量由插件作者负责。
|
||||
你可以在<a target="_blank" href="https://github.com/xfgryujk/blivechat/discussions/categories/%E6%8F%92%E4%BB%B6"
|
||||
>GitHub Discussions</a>获取一些已发布的插件</p>
|
||||
<p>插件安装方法:把解压后的插件目录放到“data/plugins”目录,然后重启blivechat</p>
|
||||
<p>注意事项:大部分插件需要开启“通过服务器转发消息”,并且连接到房间,才能接收消息</p>
|
||||
<p><a target="_blank" href="https://github.com/xfgryujk/blivechat/wiki/%E6%8F%92%E4%BB%B6%E7%B3%BB%E7%BB%9F">插件开发文档</a></p>
|
||||
`,
|
||||
author: '作者:',
|
||||
disabledByServer: '已被服务器禁用',
|
||||
admin: '管理',
|
||||
connected: '已连接',
|
||||
unconnected: '未连接',
|
||||
},
|
||||
}
|
||||
|
@ -16,6 +16,9 @@
|
||||
<el-menu-item :index="$router.resolve({ name: 'help' }).href">
|
||||
<i class="el-icon-question"></i>{{ $t('sidebar.help') }}
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="$router.resolve({ name: 'plugins' }).href">
|
||||
<i class="el-icon-magic-stick"></i>{{ $t('sidebar.plugins') }}
|
||||
</el-menu-item>
|
||||
<el-submenu index="1">
|
||||
<template slot="title">
|
||||
<i class="el-icon-link"></i>{{ $t('sidebar.links') }}
|
||||
|
@ -21,7 +21,8 @@ const router = new VueRouter({
|
||||
children: [
|
||||
{ path: '', name: 'home', component: () => import('./views/Home') },
|
||||
{ path: 'stylegen', name: 'stylegen', component: () => import('./views/StyleGenerator') },
|
||||
{ path: 'help', name: 'help', component: () => import('./views/Help') }
|
||||
{ path: 'help', name: 'help', component: () => import('./views/Help') },
|
||||
{ path: 'plugins', name: 'plugins', component: () => import('./views/Plugins') },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
183
frontend/src/views/Plugins.vue
Normal file
183
frontend/src/views/Plugins.vue
Normal file
@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<h1 style="display: inline;">{{ $t('plugins.plugins') }}</h1>
|
||||
<el-button round icon="el-icon-question" class="push-inline-end" @click="isHelpVisible = true">{{
|
||||
$t('plugins.help')
|
||||
}}</el-button>
|
||||
<el-dialog :title="$t('plugins.help')" :visible.sync="isHelpVisible">
|
||||
<div style="word-break: initial;" v-html="$t('plugins.helpContent')"></div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="plugins.length === 0" description=""></el-empty>
|
||||
<el-row v-else :gutter="16">
|
||||
<el-col v-for="plugin in plugins" :key="plugin.id" :sm="24" :md="12">
|
||||
<el-card class="card">
|
||||
<div class="card-body">
|
||||
<div class="title-line">
|
||||
<h3 class="name">{{ plugin.name }}</h3>
|
||||
<span class="version">{{ plugin.version }}</span>
|
||||
<span class="author push-inline-end">{{ $t('plugins.author') }}{{ plugin.author }}</span>
|
||||
</div>
|
||||
<div class="description">{{ plugin.description }}</div>
|
||||
|
||||
<div class="operations">
|
||||
<el-tooltip :content="$t('plugins.disabledByServer')" :disabled="serverConfig.enableAdminPlugins">
|
||||
<el-button type="primary" :disabled="!serverConfig.enableAdminPlugins || !plugin.isConnected"
|
||||
:loading="getPluginCtx(plugin.id).isOperating" @click="adminPlugin(plugin)"
|
||||
>{{ $t('plugins.admin') }}</el-button>
|
||||
</el-tooltip>
|
||||
<el-tag v-if="plugin.isConnected" type="success" class="status push-inline-end">{{ $t('plugins.connected') }}</el-tag>
|
||||
<el-tag v-else type="danger" class="status push-inline-end">{{ $t('plugins.unconnected') }}</el-tag>
|
||||
<el-switch :value="plugin.enabled" :disabled="!serverConfig.enableAdminPlugins || getPluginCtx(plugin.id).isOperating"
|
||||
@change="enabled => setPluginEnabled(plugin, enabled)"
|
||||
></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as mainApi from '@/api/main'
|
||||
import * as pluginsApi from '@/api/plugins'
|
||||
|
||||
export default {
|
||||
name: 'Plugins',
|
||||
data() {
|
||||
return {
|
||||
serverConfig: {
|
||||
enableAdminPlugins: true
|
||||
},
|
||||
isHelpVisible: false,
|
||||
plugins: [],
|
||||
pluginCtxMap: {},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateServerConfig()
|
||||
this.updatePlugins()
|
||||
},
|
||||
methods: {
|
||||
getPluginCtx(pluginId) {
|
||||
let ctx = this.pluginCtxMap[pluginId]
|
||||
if (ctx === undefined) {
|
||||
ctx = {
|
||||
isOperating: false,
|
||||
}
|
||||
this.$set(this.pluginCtxMap, pluginId, ctx)
|
||||
}
|
||||
return ctx
|
||||
},
|
||||
async updateServerConfig() {
|
||||
try {
|
||||
this.serverConfig = (await mainApi.getServerInfo()).config
|
||||
} catch (e) {
|
||||
this.$message.error(`Failed to fetch server information: ${e}`)
|
||||
throw e
|
||||
}
|
||||
},
|
||||
async updatePlugins() {
|
||||
try {
|
||||
this.plugins = (await pluginsApi.getPlugins()).plugins
|
||||
} catch (e) {
|
||||
this.$message.error(`Failed to fetch plugins: ${e}`)
|
||||
throw e
|
||||
}
|
||||
},
|
||||
async adminPlugin(plugin) {
|
||||
let ctx = this.getPluginCtx(plugin.id)
|
||||
ctx.isOperating = true
|
||||
try {
|
||||
await pluginsApi.openAdminUi(plugin.id)
|
||||
} catch (e) {
|
||||
this.$message.error(`Request failed: ${e}`)
|
||||
throw e
|
||||
} finally {
|
||||
ctx.isOperating = false
|
||||
}
|
||||
},
|
||||
async setPluginEnabled(plugin, enabled) {
|
||||
let ctx = this.getPluginCtx(plugin.id)
|
||||
ctx.isOperating = true
|
||||
try {
|
||||
let res
|
||||
try {
|
||||
res = await pluginsApi.setEnabled(plugin.id, enabled)
|
||||
} catch (e) {
|
||||
this.$message.error(`Request failed: ${e}`)
|
||||
throw e
|
||||
}
|
||||
plugin.enabled = res.enabled
|
||||
if (!res.isSwitchSuccess) {
|
||||
this.$message.error(`Failed to switch the plugin: ${res.msg}`)
|
||||
}
|
||||
|
||||
await new Promise(resolve => window.setTimeout(resolve, 3000))
|
||||
this.updatePlugins()
|
||||
} finally {
|
||||
ctx.isOperating = false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.push-inline-end {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-block-end: 16px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.title-line {
|
||||
margin-block-end: 1em;
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: baseline;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-block: 0 0;
|
||||
margin-inline-end: 8px;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.version {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.author {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-block-end: 1em;
|
||||
flex: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.operations {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
</style>
|
@ -2,7 +2,7 @@
|
||||
"name": "消息日志",
|
||||
"version": "1.0.0",
|
||||
"author": "xfgryujk",
|
||||
"description": "把收到的消息记录到文件中",
|
||||
"description": "把收到的消息记录到文件中。点击管理打开日志目录。日志不会自动清理,记得定期清理",
|
||||
"run": "msg-logging.exe",
|
||||
"enabled": true
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "文字转语音",
|
||||
"version": "1.0.0",
|
||||
"author": "xfgryujk",
|
||||
"description": "用语音读出收到的消息",
|
||||
"description": "用语音读出收到的消息。点击管理打开配置文件,编辑保存后重启插件生效",
|
||||
"run": "text-to-speech.exe",
|
||||
"enabled": true
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ def init():
|
||||
if plugin.enabled:
|
||||
try:
|
||||
plugin.start()
|
||||
except StartPluginError:
|
||||
except SwitchPluginError:
|
||||
pass
|
||||
|
||||
|
||||
@ -130,21 +130,21 @@ class PluginConfig:
|
||||
cfg['version'] = self.version
|
||||
cfg['author'] = self.author
|
||||
cfg['description'] = self.description
|
||||
cfg['run_cmd'] = self.run_cmd
|
||||
cfg['run'] = self.run_cmd
|
||||
cfg['enabled'] = self.enabled
|
||||
|
||||
tmp_path = path + '.tmp'
|
||||
with open(tmp_path, encoding='utf-8') as f:
|
||||
with open(tmp_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
||||
os.replace(tmp_path, path)
|
||||
|
||||
|
||||
class StartPluginError(Exception):
|
||||
"""启动插件时错误"""
|
||||
class SwitchPluginError(Exception):
|
||||
"""开关插件时错误"""
|
||||
|
||||
|
||||
class StartTooFrequently(StartPluginError):
|
||||
"""启动插件太频繁"""
|
||||
class SwitchTooFrequently(SwitchPluginError):
|
||||
"""开关插件太频繁"""
|
||||
|
||||
|
||||
class Plugin:
|
||||
@ -152,7 +152,7 @@ class Plugin:
|
||||
self._id = plugin_id
|
||||
self._config = cfg
|
||||
|
||||
self._last_start_time = datetime.datetime.fromtimestamp(0)
|
||||
self._last_switch_time = datetime.datetime.fromtimestamp(0)
|
||||
self._token = ''
|
||||
self._client: Optional['api.plugin.PluginWsHandler'] = None
|
||||
|
||||
@ -204,11 +204,7 @@ class Plugin:
|
||||
def start(self):
|
||||
if self.is_started:
|
||||
return
|
||||
|
||||
cur_time = datetime.datetime.now()
|
||||
if cur_time - self._last_start_time < datetime.timedelta(seconds=3):
|
||||
raise StartTooFrequently(f'plugin={self._id} starts too frequently')
|
||||
self._last_start_time = cur_time
|
||||
self._refresh_last_switch_time()
|
||||
|
||||
token = ''.join(random.choice(string.hexdigits) for _ in range(32))
|
||||
self._set_token(token)
|
||||
@ -228,11 +224,20 @@ class Plugin:
|
||||
)
|
||||
except OSError as e:
|
||||
logger.exception('plugin=%s failed to start', self._id)
|
||||
raise StartPluginError(str(e))
|
||||
raise SwitchPluginError(str(e))
|
||||
|
||||
def _refresh_last_switch_time(self):
|
||||
cur_time = datetime.datetime.now()
|
||||
if cur_time - self._last_switch_time < datetime.timedelta(seconds=3):
|
||||
raise SwitchTooFrequently(f'plugin={self._id} switches too frequently')
|
||||
self._last_switch_time = cur_time
|
||||
|
||||
def stop(self):
|
||||
if self.is_started:
|
||||
self._set_token('')
|
||||
if not self.is_started:
|
||||
return
|
||||
self._refresh_last_switch_time()
|
||||
|
||||
self._set_token('')
|
||||
|
||||
def _set_token(self, token):
|
||||
if self._token == token:
|
||||
|
Loading…
Reference in New Issue
Block a user