mirror of https://github.com/msgbyte/tailchat
feat: 插件中心与插件管理
parent
4971bed09c
commit
b95d47f9fa
@ -0,0 +1,48 @@
|
||||
export interface PluginManifest {
|
||||
/**
|
||||
* 插件用于显示的名称
|
||||
* @example 网页面板插件
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* 插件名, 插件唯一标识
|
||||
* @example com.msgbyte.webview
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* 插件地址
|
||||
*/
|
||||
url: string;
|
||||
|
||||
/**
|
||||
* 插件图标
|
||||
* 推荐大小: 128x128
|
||||
*/
|
||||
icon?: string;
|
||||
|
||||
/**
|
||||
* 插件版本号
|
||||
* 遵循 semver 规则
|
||||
*
|
||||
* major.minor.patch
|
||||
* @example 1.0.0
|
||||
*/
|
||||
version: string;
|
||||
|
||||
/**
|
||||
* 插件维护者
|
||||
*/
|
||||
author: string;
|
||||
|
||||
/**
|
||||
* 插件描述
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* 是否需要重启才能应用插件
|
||||
*/
|
||||
requireRestart: boolean;
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import { Avatar } from '@/components/Avatar';
|
||||
import { Button } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
PluginManifest,
|
||||
showToasts,
|
||||
t,
|
||||
useAsyncRequest,
|
||||
} from 'tailchat-shared';
|
||||
import { pluginManager } from '../manager';
|
||||
|
||||
/**
|
||||
* 插件项
|
||||
*/
|
||||
export const PluginStoreItem: React.FC<{
|
||||
manifest: PluginManifest;
|
||||
installed: boolean;
|
||||
builtin: boolean;
|
||||
}> = React.memo((props) => {
|
||||
const { manifest, builtin } = props;
|
||||
const [installed, setInstalled] = useState(props.installed);
|
||||
|
||||
const [{ loading }, handleInstallPlugin] = useAsyncRequest(async () => {
|
||||
await pluginManager.installPlugin(manifest);
|
||||
if (manifest.requireRestart === true) {
|
||||
showToasts(t('插件安装成功, 需要重启后生效'), 'success');
|
||||
} else {
|
||||
showToasts(t('插件安装成功'), 'success');
|
||||
}
|
||||
setInstalled(true);
|
||||
}, [manifest]);
|
||||
|
||||
return (
|
||||
<div className="rounded-md flex w-80 h-36 bg-black bg-opacity-40 py-2 px-3">
|
||||
<div className="flex w-full">
|
||||
<div className="mr-2">
|
||||
<Avatar shape="square" src={manifest.icon} name={manifest.label} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col flex-1">
|
||||
<div className="font-bold">{manifest.label}</div>
|
||||
|
||||
<div className="text-xs text-gray-300 text-opacity-50">
|
||||
{manifest.name}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">{manifest.description}</div>
|
||||
|
||||
<div className="mt-1 text-right">
|
||||
{builtin ? (
|
||||
<Button type="primary" disabled={true}>
|
||||
{t('已安装')}
|
||||
</Button>
|
||||
) : installed ? (
|
||||
<Button type="primary">{t('已安装')}</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={handleInstallPlugin}
|
||||
>
|
||||
{t('安装')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
PluginStoreItem.displayName = 'PluginStoreItem';
|
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 插件商店
|
||||
*/
|
||||
|
||||
import { LoadingSpinner } from '@/components/LoadingSpinner';
|
||||
import { Divider } from 'antd';
|
||||
import React from 'react';
|
||||
import { t, useAsync } from 'tailchat-shared';
|
||||
import { builtinPlugins } from '../builtin';
|
||||
import { pluginManager } from '../manager';
|
||||
import { PluginStoreItem } from './Item';
|
||||
|
||||
export const PluginStore: React.FC = React.memo(() => {
|
||||
const { loading, value: installedPluginNameList = [] } =
|
||||
useAsync(async () => {
|
||||
const plugins = await pluginManager.getInstalledPlugins();
|
||||
return plugins.map((p) => p.name);
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingSpinner tip={t('正在加载已安装插件')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-2 w-full">
|
||||
{/* 内置插件 */}
|
||||
<Divider orientation="left">{t('内置插件')}</Divider>
|
||||
|
||||
<div>
|
||||
{builtinPlugins.map((plugin) => (
|
||||
<PluginStoreItem
|
||||
key={plugin.name}
|
||||
manifest={plugin}
|
||||
installed={installedPluginNameList.includes(plugin.name)}
|
||||
builtin={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Divider orientation="left">{t('插件中心')}</Divider>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
PluginStore.displayName = 'PluginStore';
|
@ -0,0 +1,18 @@
|
||||
import type { PluginManifest } from 'tailchat-shared';
|
||||
|
||||
/**
|
||||
* 内置插件列表
|
||||
*
|
||||
* 该列表中的插件会被强制安装
|
||||
*/
|
||||
export const builtinPlugins: PluginManifest[] = [
|
||||
{
|
||||
label: '网页面板插件',
|
||||
name: 'com.msgbyte.webview',
|
||||
url: '/plugins/com.msgbyte.webview/index.js',
|
||||
version: '0.0.0',
|
||||
author: 'msgbyte',
|
||||
description: '为群组提供创建网页面板的功能',
|
||||
requireRestart: false,
|
||||
},
|
||||
];
|
@ -0,0 +1,67 @@
|
||||
import { getStorage, PluginManifest } from 'tailchat-shared';
|
||||
import { initMiniStar, loadSinglePlugin } from 'mini-star';
|
||||
import _once from 'lodash/once';
|
||||
import { builtinPlugins } from './builtin';
|
||||
|
||||
class PluginManager {
|
||||
/**
|
||||
* 存储插件列表的Key常量
|
||||
*/
|
||||
static STORE_KEY = '__installed_plugins';
|
||||
|
||||
/**
|
||||
* 初始化插件
|
||||
*/
|
||||
initPlugins = _once(async () => {
|
||||
const installedPlugins = [
|
||||
...builtinPlugins,
|
||||
...(await this.getInstalledPlugins()),
|
||||
];
|
||||
|
||||
const plugins = installedPlugins.map(({ name, url }) => ({
|
||||
name,
|
||||
url,
|
||||
}));
|
||||
|
||||
return initMiniStar({
|
||||
plugins,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取已安装插件列表(不包含强制安装的内部插件)
|
||||
*/
|
||||
async getInstalledPlugins(): Promise<PluginManifest[]> {
|
||||
const plugins: PluginManifest[] = await getStorage().get(
|
||||
PluginManager.STORE_KEY,
|
||||
[]
|
||||
);
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
*/
|
||||
async installPlugin(manifest: PluginManifest) {
|
||||
const plugins = await this.getInstalledPlugins();
|
||||
|
||||
const findedIndex = plugins.findIndex((p) => p.name === manifest.name);
|
||||
if (findedIndex >= 0) {
|
||||
// 已安装, 则更新
|
||||
plugins[findedIndex] = manifest;
|
||||
} else {
|
||||
// 未安装, 则推到最后
|
||||
plugins.push(manifest);
|
||||
}
|
||||
|
||||
await getStorage().save(PluginManager.STORE_KEY, plugins);
|
||||
|
||||
await loadSinglePlugin({
|
||||
name: manifest.name,
|
||||
url: manifest.url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const pluginManager = new PluginManager();
|
@ -0,0 +1,7 @@
|
||||
import { PluginStore } from '@/plugin/PluginStore';
|
||||
import React from 'react';
|
||||
|
||||
export const PluginsPanel: React.FC = React.memo(() => {
|
||||
return <PluginStore />;
|
||||
});
|
||||
PluginsPanel.displayName = 'PluginsPanel';
|
Loading…
Reference in New Issue