mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 03:15:06 +08:00
feat: add plugin enable config (#1678)
* add plugin enable config * fix logic error * improve codes * feat: add plugin system status checking api * perf: add ui displaying plugin system status * chore: fix linter errors --------- Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
@@ -91,3 +91,26 @@ class SystemRouterGroup(group.RouterGroup):
|
||||
)
|
||||
|
||||
return self.success(data=resp)
|
||||
|
||||
@self.route(
|
||||
'/status/plugin-system',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN,
|
||||
)
|
||||
async def _() -> str:
|
||||
plugin_connector_error = 'ok'
|
||||
is_connected = True
|
||||
|
||||
try:
|
||||
await self.ap.plugin_connector.ping_plugin_runtime()
|
||||
except Exception as e:
|
||||
plugin_connector_error = str(e)
|
||||
is_connected = False
|
||||
|
||||
return self.success(
|
||||
data={
|
||||
'is_enable': self.ap.plugin_connector.is_enable_plugin,
|
||||
'is_connected': is_connected,
|
||||
'plugin_connector_error': plugin_connector_error,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -40,6 +40,9 @@ class PluginRuntimeConnector:
|
||||
[PluginRuntimeConnector], typing.Coroutine[typing.Any, typing.Any, None]
|
||||
]
|
||||
|
||||
is_enable_plugin: bool = True
|
||||
"""Mark if the plugin system is enabled"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ap: app.Application,
|
||||
@@ -49,8 +52,13 @@ class PluginRuntimeConnector:
|
||||
):
|
||||
self.ap = ap
|
||||
self.runtime_disconnect_callback = runtime_disconnect_callback
|
||||
self.is_enable_plugin = self.ap.instance_config.data.get('plugin', {}).get('enable', True)
|
||||
|
||||
async def initialize(self):
|
||||
if not self.is_enable_plugin:
|
||||
self.ap.logger.info('Plugin system is disabled.')
|
||||
return
|
||||
|
||||
async def new_connection_callback(connection: base_connection.Connection):
|
||||
async def disconnect_callback(rchandler: handler.RuntimeConnectionHandler) -> bool:
|
||||
if platform.get_platform() == 'docker' or platform.use_websocket_to_connect_plugin_runtime():
|
||||
@@ -103,6 +111,12 @@ class PluginRuntimeConnector:
|
||||
async def initialize_plugins(self):
|
||||
pass
|
||||
|
||||
async def ping_plugin_runtime(self):
|
||||
if not hasattr(self, 'handler'):
|
||||
raise Exception('Plugin runtime is not connected')
|
||||
|
||||
return await self.handler.ping()
|
||||
|
||||
async def install_plugin(
|
||||
self,
|
||||
install_source: PluginInstallSource,
|
||||
@@ -175,6 +189,8 @@ class PluginRuntimeConnector:
|
||||
) -> context.EventContext:
|
||||
event_ctx = context.EventContext.from_event(event)
|
||||
|
||||
if not self.is_enable_plugin:
|
||||
return event_ctx
|
||||
event_ctx_result = await self.handler.emit_event(event_ctx.model_dump(serialize_as_any=True))
|
||||
|
||||
event_ctx = context.EventContext.model_validate(event_ctx_result['event_context'])
|
||||
@@ -205,6 +221,6 @@ class PluginRuntimeConnector:
|
||||
yield cmd_ret
|
||||
|
||||
def dispose(self):
|
||||
if isinstance(self.ctrl, stdio_client_controller.StdioClientController):
|
||||
if self.is_enable_plugin and isinstance(self.ctrl, stdio_client_controller.StdioClientController):
|
||||
self.ap.logger.info('Terminating plugin runtime process...')
|
||||
self.ctrl.process.terminate()
|
||||
|
||||
@@ -38,6 +38,7 @@ vdb:
|
||||
port: 6333
|
||||
api_key: ''
|
||||
plugin:
|
||||
enable: true
|
||||
runtime_ws_url: 'ws://langbot_plugin_runtime:5400/control/ws'
|
||||
enable_marketplace: true
|
||||
cloud_service_url: 'https://space.langbot.app'
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
UploadIcon,
|
||||
StoreIcon,
|
||||
Download,
|
||||
Power,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -28,12 +29,13 @@ import {
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PluginV4 } from '@/app/infra/entities/plugin';
|
||||
import { systemInfo } from '@/app/infra/http/HttpClient';
|
||||
import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
|
||||
|
||||
enum PluginInstallStatus {
|
||||
WAIT_INPUT = 'wait_input',
|
||||
@@ -54,9 +56,29 @@ export default function PluginConfigPage() {
|
||||
const [installError, setInstallError] = useState<string | null>(null);
|
||||
const [githubURL, setGithubURL] = useState('');
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [pluginSystemStatus, setPluginSystemStatus] =
|
||||
useState<ApiRespPluginSystemStatus | null>(null);
|
||||
const [statusLoading, setStatusLoading] = useState(true);
|
||||
const pluginInstalledRef = useRef<PluginInstalledComponentRef>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPluginSystemStatus = async () => {
|
||||
try {
|
||||
setStatusLoading(true);
|
||||
const status = await httpClient.getPluginSystemStatus();
|
||||
setPluginSystemStatus(status);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch plugin system status:', error);
|
||||
toast.error(t('plugins.failedToGetStatus'));
|
||||
} finally {
|
||||
setStatusLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPluginSystemStatus();
|
||||
}, [t]);
|
||||
|
||||
function watchTask(taskId: number) {
|
||||
let alreadySuccess = false;
|
||||
console.log('taskId:', taskId);
|
||||
@@ -140,6 +162,11 @@ export default function PluginConfigPage() {
|
||||
|
||||
const uploadPluginFile = useCallback(
|
||||
async (file: File) => {
|
||||
if (!pluginSystemStatus?.is_enable || !pluginSystemStatus?.is_connected) {
|
||||
toast.error(t('plugins.pluginSystemNotReady'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateFileType(file)) {
|
||||
toast.error(t('plugins.unsupportedFileType'));
|
||||
return;
|
||||
@@ -150,7 +177,7 @@ export default function PluginConfigPage() {
|
||||
setInstallError(null);
|
||||
installPlugin('local', { file });
|
||||
},
|
||||
[t],
|
||||
[t, pluginSystemStatus],
|
||||
);
|
||||
|
||||
const handleFileSelect = useCallback(() => {
|
||||
@@ -171,10 +198,18 @@ export default function PluginConfigPage() {
|
||||
[uploadPluginFile],
|
||||
);
|
||||
|
||||
const handleDragOver = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
setIsDragOver(true);
|
||||
}, []);
|
||||
const isPluginSystemReady =
|
||||
pluginSystemStatus?.is_enable && pluginSystemStatus?.is_connected;
|
||||
|
||||
const handleDragOver = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
if (isPluginSystemReady) {
|
||||
setIsDragOver(true);
|
||||
}
|
||||
},
|
||||
[isPluginSystemReady],
|
||||
);
|
||||
|
||||
const handleDragLeave = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -186,14 +221,76 @@ export default function PluginConfigPage() {
|
||||
event.preventDefault();
|
||||
setIsDragOver(false);
|
||||
|
||||
if (!isPluginSystemReady) {
|
||||
toast.error(t('plugins.pluginSystemNotReady'));
|
||||
return;
|
||||
}
|
||||
|
||||
const files = Array.from(event.dataTransfer.files);
|
||||
if (files.length > 0) {
|
||||
uploadPluginFile(files[0]);
|
||||
}
|
||||
},
|
||||
[uploadPluginFile],
|
||||
[uploadPluginFile, isPluginSystemReady, t],
|
||||
);
|
||||
|
||||
// 插件系统未启用的状态显示
|
||||
const renderPluginDisabledState = () => (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] text-center pt-[10vh]">
|
||||
<Power className="w-16 h-16 text-gray-400 mb-4" />
|
||||
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
{t('plugins.systemDisabled')}
|
||||
</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400 max-w-md">
|
||||
{t('plugins.systemDisabledDesc')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 插件系统连接异常的状态显示
|
||||
const renderPluginConnectionErrorState = () => (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] text-center pt-[10vh]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="72"
|
||||
height="72"
|
||||
fill="#BDBDBD"
|
||||
>
|
||||
<path d="M17.657 14.8284L16.2428 13.4142L17.657 12C19.2191 10.4379 19.2191 7.90526 17.657 6.34316C16.0949 4.78106 13.5622 4.78106 12.0001 6.34316L10.5859 7.75737L9.17171 6.34316L10.5859 4.92895C12.9291 2.5858 16.7281 2.5858 19.0712 4.92895C21.4143 7.27209 21.4143 11.0711 19.0712 13.4142L17.657 14.8284ZM14.8286 17.6569L13.4143 19.0711C11.0712 21.4142 7.27221 21.4142 4.92907 19.0711C2.58592 16.7279 2.58592 12.9289 4.92907 10.5858L6.34328 9.17159L7.75749 10.5858L6.34328 12C4.78118 13.5621 4.78118 16.0948 6.34328 17.6569C7.90538 19.219 10.438 19.219 12.0001 17.6569L13.4143 16.2427L14.8286 17.6569ZM14.8286 7.75737L16.2428 9.17159L9.17171 16.2427L7.75749 14.8284L14.8286 7.75737ZM5.77539 2.29291L7.70724 1.77527L8.74252 5.63897L6.81067 6.15661L5.77539 2.29291ZM15.2578 18.3611L17.1896 17.8434L18.2249 21.7071L16.293 22.2248L15.2578 18.3611ZM2.29303 5.77527L6.15673 6.81054L5.63909 8.7424L1.77539 7.70712L2.29303 5.77527ZM18.3612 15.2576L22.2249 16.2929L21.7072 18.2248L17.8435 17.1895L18.3612 15.2576Z"></path>
|
||||
</svg>
|
||||
|
||||
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
{t('plugins.connectionError')}
|
||||
</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400 max-w-md mb-4">
|
||||
{t('plugins.connectionErrorDesc')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 加载状态显示
|
||||
const renderLoadingState = () => (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] pt-[10vh]">
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
{t('plugins.loadingStatus')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 根据状态返回不同的内容
|
||||
if (statusLoading) {
|
||||
return renderLoadingState();
|
||||
}
|
||||
|
||||
if (!pluginSystemStatus?.is_enable) {
|
||||
return renderPluginDisabledState();
|
||||
}
|
||||
|
||||
if (!pluginSystemStatus?.is_connected) {
|
||||
return renderPluginConnectionErrorState();
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.pageContainer} ${isDragOver ? 'bg-blue-50' : ''}`}
|
||||
|
||||
@@ -215,6 +215,12 @@ export interface ApiRespSystemInfo {
|
||||
enable_marketplace: boolean;
|
||||
}
|
||||
|
||||
export interface ApiRespPluginSystemStatus {
|
||||
is_enable: boolean;
|
||||
is_connected: boolean;
|
||||
plugin_connector_error: string;
|
||||
}
|
||||
|
||||
export interface ApiRespAsyncTasks {
|
||||
tasks: AsyncTask[];
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
ApiRespProviderEmbeddingModels,
|
||||
ApiRespProviderEmbeddingModel,
|
||||
EmbeddingModel,
|
||||
ApiRespPluginSystemStatus,
|
||||
} from '@/app/infra/entities/api';
|
||||
import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest';
|
||||
import { GetBotLogsResponse } from '@/app/infra/http/requestParam/bots/GetBotLogsResponse';
|
||||
@@ -500,6 +501,10 @@ export class BackendClient extends BaseHttpClient {
|
||||
return this.get(`/api/v1/system/tasks/${id}`);
|
||||
}
|
||||
|
||||
public getPluginSystemStatus(): Promise<ApiRespPluginSystemStatus> {
|
||||
return this.get('/api/v1/system/status/plugin-system');
|
||||
}
|
||||
|
||||
// ============ User API ============
|
||||
public checkIfInited(): Promise<{ initialized: boolean }> {
|
||||
return this.get('/api/v1/user/init');
|
||||
|
||||
@@ -182,6 +182,17 @@ const enUS = {
|
||||
pluginSortSuccess: 'Plugin sort successful',
|
||||
pluginSortError: 'Plugin sort failed: ',
|
||||
pluginNoConfig: 'The plugin has no configuration items.',
|
||||
systemDisabled: 'Plugin System Disabled',
|
||||
systemDisabledDesc:
|
||||
'Plugin system is not enabled, please modify the configuration according to the documentation',
|
||||
connectionError: 'Plugin System Connection Error',
|
||||
connectionErrorDesc:
|
||||
'Please check the plugin system configuration or contact the administrator.',
|
||||
errorDetails: 'Error Details',
|
||||
loadingStatus: 'Checking plugin system status...',
|
||||
failedToGetStatus: 'Failed to get plugin system status',
|
||||
pluginSystemNotReady:
|
||||
'Plugin system is not ready, cannot perform this operation',
|
||||
deleting: 'Deleting...',
|
||||
deletePlugin: 'Delete Plugin',
|
||||
cancel: 'Cancel',
|
||||
|
||||
@@ -183,6 +183,17 @@ const jaJP = {
|
||||
pluginSortSuccess: 'プラグインの並び替えに成功しました',
|
||||
pluginSortError: 'プラグインの並び替えに失敗しました:',
|
||||
pluginNoConfig: 'プラグインに設定項目がありません。',
|
||||
systemDisabled: 'プラグインシステムが無効になっています',
|
||||
systemDisabledDesc:
|
||||
'プラグインシステムが無効になっています。プラグインシステムを有効にするか、ドキュメントに従って設定を変更してください',
|
||||
connectionError: 'プラグインシステム接続エラー',
|
||||
connectionErrorDesc:
|
||||
'プラグインシステム設定を確認するか、管理者に連絡してください',
|
||||
errorDetails: 'エラー詳細',
|
||||
loadingStatus: 'プラグインシステム状態を確認中...',
|
||||
failedToGetStatus: 'プラグインシステム状態の取得に失敗しました',
|
||||
pluginSystemNotReady:
|
||||
'プラグインシステムが準備されていません。この操作を実行できません',
|
||||
deleting: '削除中...',
|
||||
deletePlugin: 'プラグインを削除',
|
||||
cancel: 'キャンセル',
|
||||
|
||||
@@ -178,6 +178,14 @@ const zhHans = {
|
||||
pluginSortSuccess: '插件排序成功',
|
||||
pluginSortError: '插件排序失败:',
|
||||
pluginNoConfig: '插件没有配置项。',
|
||||
systemDisabled: '插件系统未启用',
|
||||
systemDisabledDesc: '尚未启用插件系统,请根据文档修改配置',
|
||||
connectionError: '插件系统连接异常',
|
||||
connectionErrorDesc: '请检查插件系统配置或联系管理员',
|
||||
errorDetails: '错误详情',
|
||||
loadingStatus: '正在检查插件系统状态...',
|
||||
failedToGetStatus: '获取插件系统状态失败',
|
||||
pluginSystemNotReady: '插件系统未就绪,无法执行此操作',
|
||||
deleting: '删除中...',
|
||||
deletePlugin: '删除插件',
|
||||
cancel: '取消',
|
||||
|
||||
@@ -178,6 +178,14 @@ const zhHant = {
|
||||
pluginSortSuccess: '外掛排序成功',
|
||||
pluginSortError: '外掛排序失敗:',
|
||||
pluginNoConfig: '外掛沒有設定項目。',
|
||||
systemDisabled: '外掛系統未啟用',
|
||||
systemDisabledDesc: '尚未啟用外掛系統,請根據文檔修改配置',
|
||||
connectionError: '外掛系統連接異常',
|
||||
connectionErrorDesc: '請檢查外掛系統配置或聯絡管理員',
|
||||
errorDetails: '錯誤詳情',
|
||||
loadingStatus: '正在檢查外掛系統狀態...',
|
||||
failedToGetStatus: '取得外掛系統狀態失敗',
|
||||
pluginSystemNotReady: '外掛系統未就緒,無法執行此操作',
|
||||
deleting: '刪除中...',
|
||||
deletePlugin: '刪除外掛',
|
||||
cancel: '取消',
|
||||
|
||||
Reference in New Issue
Block a user