From 5179b3e53a1ab165a48f9649ee9796b7af85f4a2 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 16 Aug 2025 15:42:49 +0800 Subject: [PATCH] feat: trace plugin installation --- pkg/entity/persistence/plugin.py | 2 + .../dbm005_plugin_install_source.py | 25 +++ pkg/plugin/connector.py | 11 +- pkg/plugin/handler.py | 52 +++++- pkg/utils/constants.py | 2 +- web/src/app/home/plugins/page.tsx | 152 +++++++++--------- .../PluginUploadDialog.tsx | 69 -------- web/src/i18n/locales/en-US.ts | 2 +- web/src/i18n/locales/ja-JP.ts | 2 +- web/src/i18n/locales/zh-Hans.ts | 2 +- 10 files changed, 167 insertions(+), 152 deletions(-) create mode 100644 pkg/persistence/migrations/dbm005_plugin_install_source.py delete mode 100644 web/src/app/home/plugins/plugin-upload-dialog/PluginUploadDialog.tsx diff --git a/pkg/entity/persistence/plugin.py b/pkg/entity/persistence/plugin.py index 30db6bd6..aebed2f0 100644 --- a/pkg/entity/persistence/plugin.py +++ b/pkg/entity/persistence/plugin.py @@ -13,6 +13,8 @@ class PluginSetting(Base): enabled = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False, default=True) priority = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, default=0) config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default=dict) + install_source = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, default='github') + install_info = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default=dict) created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()) updated_at = sqlalchemy.Column( sqlalchemy.DateTime, diff --git a/pkg/persistence/migrations/dbm005_plugin_install_source.py b/pkg/persistence/migrations/dbm005_plugin_install_source.py new file mode 100644 index 00000000..11547f88 --- /dev/null +++ b/pkg/persistence/migrations/dbm005_plugin_install_source.py @@ -0,0 +1,25 @@ +import sqlalchemy +from .. import migration + + +@migration.migration_class(5) +class DBMigratePluginInstallSource(migration.DBMigration): + """插件安装来源""" + + async def upgrade(self): + """升级""" + # add new column install_source, use default value 'github', via alter table + await self.ap.persistence_mgr.execute_async( + sqlalchemy.text( + "ALTER TABLE plugin_settings ADD COLUMN install_source VARCHAR(255) NOT NULL DEFAULT 'github'" + ) + ) + + # add new column install_info, use default value {}, via alter table + await self.ap.persistence_mgr.execute_async( + sqlalchemy.text("ALTER TABLE plugin_settings ADD COLUMN install_info JSON NOT NULL DEFAULT '{}'") + ) + + async def downgrade(self): + """降级""" + pass diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index cf363c7c..92c26c28 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -105,7 +105,16 @@ class PluginRuntimeConnector: install_info: dict[str, Any], task_context: taskmgr.TaskContext | None = None, ): - return await self.handler.install_plugin(install_source.value, install_info) + async for ret in self.handler.install_plugin(install_source.value, install_info): + current_action = ret.get('current_action', None) + if current_action is not None: + if task_context is not None: + task_context.set_current_action(current_action) + + trace = ret.get('trace', None) + if trace is not None: + if task_context is not None: + task_context.trace(trace) async def list_plugins(self) -> list[dict[str, Any]]: return await self.handler.list_plugins() diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index 502db9cb..bc151321 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -39,6 +39,43 @@ class RuntimeConnectionHandler(handler.Handler): super().__init__(connection, disconnect_callback) self.ap = ap + @self.action(RuntimeToLangBotAction.INITIALIZE_PLUGIN_SETTINGS) + async def initialize_plugin_settings(data: dict[str, Any]) -> handler.ActionResponse: + """Initialize plugin settings""" + # check if exists plugin setting + plugin_author = data['plugin_author'] + plugin_name = data['plugin_name'] + install_source = data['install_source'] + install_info = data['install_info'] + + result = await self.ap.persistence_mgr.execute_async( + sqlalchemy.select(persistence_plugin.PluginSetting) + .where(persistence_plugin.PluginSetting.plugin_author == plugin_author) + .where(persistence_plugin.PluginSetting.plugin_name == plugin_name) + ) + + if result.first() is not None: + # delete plugin setting + await self.ap.persistence_mgr.execute_async( + sqlalchemy.delete(persistence_plugin.PluginSetting) + .where(persistence_plugin.PluginSetting.plugin_author == plugin_author) + .where(persistence_plugin.PluginSetting.plugin_name == plugin_name) + ) + + # create plugin setting + await self.ap.persistence_mgr.execute_async( + sqlalchemy.insert(persistence_plugin.PluginSetting).values( + plugin_author=plugin_author, + plugin_name=plugin_name, + install_source=install_source, + install_info=install_info, + ) + ) + + return handler.ActionResponse.success( + data={}, + ) + @self.action(RuntimeToLangBotAction.GET_PLUGIN_SETTINGS) async def get_plugin_settings(data: dict[str, Any]) -> handler.ActionResponse: """Get plugin settings""" @@ -56,6 +93,8 @@ class RuntimeConnectionHandler(handler.Handler): 'enabled': False, 'priority': 0, 'plugin_config': {}, + 'install_source': 'local', + 'install_info': {}, } setting = result.first() @@ -64,6 +103,8 @@ class RuntimeConnectionHandler(handler.Handler): data['enabled'] = setting.enabled data['priority'] = setting.priority data['plugin_config'] = setting.config + data['install_source'] = setting.install_source + data['install_info'] = setting.install_info return handler.ActionResponse.success( data=data, @@ -373,17 +414,22 @@ class RuntimeConnectionHandler(handler.Handler): timeout=10, ) - async def install_plugin(self, install_source: str, install_info: dict[str, Any]) -> dict[str, Any]: + async def install_plugin( + self, install_source: str, install_info: dict[str, Any] + ) -> typing.AsyncGenerator[dict[str, Any], None]: """Install plugin""" - return await self.call_action( + gen = self.call_action_generator( LangBotToRuntimeAction.INSTALL_PLUGIN, { 'install_source': install_source, 'install_info': install_info, }, - timeout=10, + timeout=120, ) + async for ret in gen: + yield ret + async def list_plugins(self) -> list[dict[str, Any]]: """List plugins""" result = await self.call_action( diff --git a/pkg/utils/constants.py b/pkg/utils/constants.py index f822b477..19a92715 100644 --- a/pkg/utils/constants.py +++ b/pkg/utils/constants.py @@ -1,6 +1,6 @@ semantic_version = 'v4.0.7' -required_database_version = 4 +required_database_version = 5 """标记本版本所需要的数据库结构版本,用于判断数据库迁移""" debug_mode = False diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index b2776132..4d1bbc46 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -4,13 +4,16 @@ import PluginInstalledComponent, { } from '@/app/home/plugins/plugin-installed/PluginInstalledComponent'; import PluginMarketComponent from '@/app/home/plugins/plugin-market/PluginMarketComponent'; import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog'; -import PluginUploadDialog, { - UploadModalStatus, -} from '@/app/home/plugins/plugin-upload-dialog/PluginUploadDialog'; import styles from './plugins.module.css'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; -import { PlusIcon, ChevronDownIcon, UploadIcon, StoreIcon } from 'lucide-react'; +import { + PlusIcon, + ChevronDownIcon, + UploadIcon, + StoreIcon, + Download, +} from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, @@ -25,7 +28,7 @@ import { DialogFooter, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; -import { GithubIcon } from 'lucide-react'; +import { Upload } from 'lucide-react'; import { useState, useRef, useCallback } from 'react'; import { httpClient } from '@/app/infra/http/HttpClient'; import { toast } from 'sonner'; @@ -33,6 +36,7 @@ import { useTranslation } from 'react-i18next'; enum PluginInstallStatus { WAIT_INPUT = 'wait_input', + ASK_CONFIRM = 'ask_confirm', INSTALLING = 'installing', ERROR = 'error', } @@ -46,56 +50,72 @@ export default function PluginConfigPage() { useState(PluginInstallStatus.WAIT_INPUT); const [installError, setInstallError] = useState(null); const [githubURL, setGithubURL] = useState(''); - const [uploadModalOpen, setUploadModalOpen] = useState(false); - const [uploadStatus, setUploadStatus] = useState( - UploadModalStatus.UPLOADING, - ); - const [uploadError, setUploadError] = useState(null); const [isDragOver, setIsDragOver] = useState(false); const pluginInstalledRef = useRef(null); const fileInputRef = useRef(null); - function handleModalConfirm() { - installPlugin(githubURL); - } - function installPlugin(url: string) { - setPluginInstallStatus(PluginInstallStatus.INSTALLING); - httpClient - .installPluginFromGithub(url) - .then((resp) => { - const taskId = resp.task_id; + function watchTask(taskId: number) { + let alreadySuccess = false; + console.log('taskId:', taskId); - let alreadySuccess = false; - console.log('taskId:', taskId); - - // 每秒拉取一次任务状态 - const interval = setInterval(() => { - httpClient.getAsyncTask(taskId).then((resp) => { - console.log('task status:', resp); - if (resp.runtime.done) { - clearInterval(interval); - if (resp.runtime.exception) { - setInstallError(resp.runtime.exception); - setPluginInstallStatus(PluginInstallStatus.ERROR); - } else { - // success - if (!alreadySuccess) { - toast.success(t('plugins.installSuccess')); - alreadySuccess = true; - } - setGithubURL(''); - setModalOpen(false); - pluginInstalledRef.current?.refreshPluginList(); - } + // 每秒拉取一次任务状态 + const interval = setInterval(() => { + httpClient.getAsyncTask(taskId).then((resp) => { + console.log('task status:', resp); + if (resp.runtime.done) { + clearInterval(interval); + if (resp.runtime.exception) { + setInstallError(resp.runtime.exception); + setPluginInstallStatus(PluginInstallStatus.ERROR); + } else { + // success + if (!alreadySuccess) { + toast.success(t('plugins.installSuccess')); + alreadySuccess = true; } - }); - }, 1000); - }) - .catch((err) => { - console.log('error when install plugin:', err); - setInstallError(err.message); - setPluginInstallStatus(PluginInstallStatus.ERROR); + setGithubURL(''); + setModalOpen(false); + pluginInstalledRef.current?.refreshPluginList(); + } + } }); + }, 1000); + } + + function handleModalConfirm() { + installPlugin('github', { url: githubURL }); + } + + function installPlugin( + installSource: string, + installInfo: Record, + ) { + setPluginInstallStatus(PluginInstallStatus.INSTALLING); + if (installSource === 'github') { + httpClient + .installPluginFromGithub(installInfo.url) + .then((resp) => { + const taskId = resp.task_id; + watchTask(taskId); + }) + .catch((err) => { + console.log('error when install plugin:', err); + setInstallError(err.message); + setPluginInstallStatus(PluginInstallStatus.ERROR); + }); + } else if (installSource === 'local') { + httpClient + .installPluginFromLocal(installInfo.file) + .then((resp) => { + const taskId = resp.task_id; + watchTask(taskId); + }) + .catch((err) => { + console.log('error when install plugin:', err); + setInstallError(err.message); + setPluginInstallStatus(PluginInstallStatus.ERROR); + }); + } } const validateFileType = (file: File): boolean => { @@ -111,21 +131,10 @@ export default function PluginConfigPage() { return; } - setUploadModalOpen(true); - setUploadStatus(UploadModalStatus.UPLOADING); - setUploadError(null); - - try { - // 暂时直接显示成功,等后续实现进度显示 - setTimeout(() => { - setUploadStatus(UploadModalStatus.SUCCESS); - toast.success(t('plugins.uploadSuccess')); - pluginInstalledRef.current?.refreshPluginList(); - }, 1000); - } catch (err: unknown) { - setUploadError((err as Error)?.message || t('plugins.uploadFailed')); - setUploadStatus(UploadModalStatus.ERROR); - } + setModalOpen(true); + setPluginInstallStatus(PluginInstallStatus.INSTALLING); + setInstallError(null); + installPlugin('local', { file }); }, [t], ); @@ -250,8 +259,8 @@ export default function PluginConfigPage() { - - {t('plugins.installFromGithub')} + + {t('plugins.installPlugin')} {pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && ( @@ -277,12 +286,13 @@ export default function PluginConfigPage() { )} - {pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && ( + {(pluginInstallStatus === PluginInstallStatus.WAIT_INPUT || + pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM) && ( <> - @@ -296,14 +306,6 @@ export default function PluginConfigPage() { - {/* 上传状态弹窗 */} - - {/* 拖拽提示覆盖层 */} {isDragOver && (
diff --git a/web/src/app/home/plugins/plugin-upload-dialog/PluginUploadDialog.tsx b/web/src/app/home/plugins/plugin-upload-dialog/PluginUploadDialog.tsx deleted file mode 100644 index 6cc0273e..00000000 --- a/web/src/app/home/plugins/plugin-upload-dialog/PluginUploadDialog.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client'; - -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { UploadIcon } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; - -export enum UploadModalStatus { - UPLOADING = 'uploading', - SUCCESS = 'success', - ERROR = 'error', -} - -interface PluginUploadDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - status: UploadModalStatus; - error?: string | null; -} - -export default function PluginUploadDialog({ - open, - onOpenChange, - status, - error, -}: PluginUploadDialogProps) { - const { t } = useTranslation(); - - return ( - - - - - - {t('plugins.uploadLocalPlugin')} - - -
- {status === UploadModalStatus.UPLOADING && ( -

{t('plugins.uploadingPlugin')}

- )} - {status === UploadModalStatus.SUCCESS && ( -

{t('plugins.uploadSuccess')}

- )} - {status === UploadModalStatus.ERROR && ( - <> -

{t('plugins.uploadFailed')}

-

{error}

- - )} -
- - {(status === UploadModalStatus.SUCCESS || - status === UploadModalStatus.ERROR) && ( - - )} - -
-
- ); -} diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 85c0c07c..1a7f4499 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -142,7 +142,7 @@ const enUS = { marketplace: 'Marketplace', arrange: 'Sort Plugins', install: 'Install', - installFromGithub: 'Install Plugin from GitHub', + installPlugin: 'Install Plugin', onlySupportGithub: 'Currently only supports installation from GitHub', enterGithubLink: 'Enter GitHub link of the plugin', installing: 'Installing plugin...', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 6613d118..1576500f 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -142,7 +142,7 @@ const jaJP = { marketplace: 'プラグインマーケット', arrange: '並び替え', install: 'インストール', - installFromGithub: 'GitHubからプラグインをインストール', + installPlugin: 'プラグインをインストール', onlySupportGithub: '現在はGitHubからのインストールのみサポートしています', enterGithubLink: 'プラグインのGitHubリンクを入力してください', installing: 'プラグインをインストール中...', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 821cdaec..867410f1 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -139,7 +139,7 @@ const zhHans = { marketplace: '插件市场', arrange: '编排', install: '安装', - installFromGithub: '从 GitHub 安装插件', + installPlugin: '安装插件', onlySupportGithub: '目前仅支持从 GitHub 安装', enterGithubLink: '请输入插件的Github链接', installing: '正在安装插件...',