diff --git a/pkg/api/http/controller/groups/plugins.py b/pkg/api/http/controller/groups/plugins.py index 4966553b..4a3f723e 100644 --- a/pkg/api/http/controller/groups/plugins.py +++ b/pkg/api/http/controller/groups/plugins.py @@ -45,9 +45,10 @@ class PluginsRouterGroup(group.RouterGroup): return self.http_status(404, -1, 'plugin not found') return self.success(data={'plugin': plugin}) elif quart.request.method == 'DELETE': + delete_data = quart.request.args.get('delete_data', 'false').lower() == 'true' ctx = taskmgr.TaskContext.new() wrapper = self.ap.task_mgr.create_user_task( - self.ap.plugin_connector.delete_plugin(author, plugin_name, task_context=ctx), + self.ap.plugin_connector.delete_plugin(author, plugin_name, delete_data=delete_data, task_context=ctx), kind='plugin-operation', name=f'plugin-remove-{plugin_name}', label=f'Removing plugin {plugin_name}', diff --git a/pkg/plugin/connector.py b/pkg/plugin/connector.py index 96530de2..4b5809fe 100644 --- a/pkg/plugin/connector.py +++ b/pkg/plugin/connector.py @@ -177,7 +177,11 @@ class PluginRuntimeConnector: task_context.trace(trace) async def delete_plugin( - self, plugin_author: str, plugin_name: str, task_context: taskmgr.TaskContext | None = None + self, + plugin_author: str, + plugin_name: str, + delete_data: bool = False, + task_context: taskmgr.TaskContext | None = None, ) -> dict[str, Any]: async for ret in self.handler.delete_plugin(plugin_author, plugin_name): current_action = ret.get('current_action', None) @@ -190,6 +194,12 @@ class PluginRuntimeConnector: if task_context is not None: task_context.trace(trace) + # Clean up plugin settings and binary storage if requested + if delete_data: + if task_context is not None: + task_context.trace('Cleaning up plugin configuration and storage...') + await self.handler.cleanup_plugin_data(plugin_author, plugin_name) + async def list_plugins(self) -> list[dict[str, Any]]: if not self.is_enable_plugin: return [] diff --git a/pkg/plugin/handler.py b/pkg/plugin/handler.py index b138fd42..da710f41 100644 --- a/pkg/plugin/handler.py +++ b/pkg/plugin/handler.py @@ -56,7 +56,9 @@ class RuntimeConnectionHandler(handler.Handler): .where(persistence_plugin.PluginSetting.plugin_name == plugin_name) ) - if result.first() is not None: + setting = result.first() + + if setting is not None: # delete plugin setting await self.ap.persistence_mgr.execute_async( sqlalchemy.delete(persistence_plugin.PluginSetting) @@ -71,6 +73,10 @@ class RuntimeConnectionHandler(handler.Handler): plugin_name=plugin_name, install_source=install_source, install_info=install_info, + # inherit from existing setting + enabled=setting.enabled if setting is not None else True, + priority=setting.priority if setting is not None else 0, + config=setting.config if setting is not None else {}, # noqa: F821 ) ) @@ -573,6 +579,23 @@ class RuntimeConnectionHandler(handler.Handler): 'mime_type': mime_type, } + async def cleanup_plugin_data(self, plugin_author: str, plugin_name: str) -> None: + """Cleanup plugin settings and binary storage""" + # Delete plugin settings + 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) + ) + + # Delete all binary storage for this plugin + owner = f'{plugin_author}/{plugin_name}' + await self.ap.persistence_mgr.execute_async( + sqlalchemy.delete(persistence_bstorage.BinaryStorage) + .where(persistence_bstorage.BinaryStorage.owner_type == 'plugin') + .where(persistence_bstorage.BinaryStorage.owner == owner) + ) + async def call_tool(self, tool_name: str, parameters: dict[str, Any]) -> dict[str, Any]: """Call tool""" result = await self.call_action( diff --git a/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx b/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx index 315e9960..612151d9 100644 --- a/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx @@ -15,6 +15,7 @@ import { DialogFooter, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; import { useTranslation } from 'react-i18next'; import { extractI18nObject } from '@/i18n/I18nProvider'; import { toast } from 'sonner'; @@ -43,6 +44,7 @@ const PluginInstalledComponent = forwardRef( PluginOperationType.DELETE, ); const [targetPlugin, setTargetPlugin] = useState(null); + const [deleteData, setDeleteData] = useState(false); const asyncTask = useAsyncTask({ onSuccess: () => { @@ -108,6 +110,7 @@ const PluginInstalledComponent = forwardRef( setTargetPlugin(plugin); setOperationType(PluginOperationType.DELETE); setShowOperationModal(true); + setDeleteData(false); asyncTask.reset(); } @@ -123,7 +126,11 @@ const PluginInstalledComponent = forwardRef( const apiCall = operationType === PluginOperationType.DELETE - ? httpClient.removePlugin(targetPlugin.author, targetPlugin.name) + ? httpClient.removePlugin( + targetPlugin.author, + targetPlugin.name, + deleteData, + ) : httpClient.upgradePlugin(targetPlugin.author, targetPlugin.name); apiCall @@ -161,16 +168,35 @@ const PluginInstalledComponent = forwardRef( {asyncTask.status === AsyncTaskStatus.WAIT_INPUT && ( -
- {operationType === PluginOperationType.DELETE - ? t('plugins.confirmDeletePlugin', { - author: targetPlugin?.author ?? '', - name: targetPlugin?.name ?? '', - }) - : t('plugins.confirmUpdatePlugin', { - author: targetPlugin?.author ?? '', - name: targetPlugin?.name ?? '', - })} +
+
+ {operationType === PluginOperationType.DELETE + ? t('plugins.confirmDeletePlugin', { + author: targetPlugin?.author ?? '', + name: targetPlugin?.name ?? '', + }) + : t('plugins.confirmUpdatePlugin', { + author: targetPlugin?.author ?? '', + name: targetPlugin?.name ?? '', + })} +
+ {operationType === PluginOperationType.DELETE && ( +
+ + setDeleteData(checked === true) + } + /> + +
+ )}
)} {asyncTask.status === AsyncTaskStatus.RUNNING && ( diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index cc47d3fa..319e2bc5 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -480,8 +480,11 @@ export class BackendClient extends BaseHttpClient { public removePlugin( author: string, name: string, + deleteData: boolean = false, ): Promise { - return this.delete(`/api/v1/plugins/${author}/${name}`); + return this.delete( + `/api/v1/plugins/${author}/${name}?delete_data=${deleteData}`, + ); } public upgradePlugin( diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index cca72c0e..b81f691e 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -199,7 +199,9 @@ const enUS = { saveConfig: 'Save Config', saving: 'Saving...', confirmDeletePlugin: - 'Are you sure you want to delete the plugin ({{author}}/{{name}})? This will also delete the plugin configuration.', + 'Are you sure you want to delete the plugin ({{author}}/{{name}})?', + deleteDataCheckbox: + 'Also delete plugin configuration and persistence storage', confirmDelete: 'Confirm Delete', deleteError: 'Delete failed: ', close: 'Close', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 2d574fdd..d87b20f8 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -200,7 +200,8 @@ const jaJP = { saveConfig: '設定を保存', saving: '保存中...', confirmDeletePlugin: - 'プラグイン「{{author}}/{{name}}」を削除してもよろしいですか?この操作により、プラグインの設定も削除されます。', + 'プラグイン「{{author}}/{{name}}」を削除してもよろしいですか?', + deleteDataCheckbox: 'プラグイン設定と永続化ストレージも削除する', confirmDelete: '削除を確認', deleteError: '削除に失敗しました:', close: '閉じる', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index ee912e7a..dbab2842 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -191,8 +191,8 @@ const zhHans = { cancel: '取消', saveConfig: '保存配置', saving: '保存中...', - confirmDeletePlugin: - '你确定要删除插件({{author}}/{{name}})吗?这将同时删除插件的配置。', + confirmDeletePlugin: '你确定要删除插件({{author}}/{{name}})吗?', + deleteDataCheckbox: '同时删除插件配置和持久化存储', confirmDelete: '确认删除', deleteError: '删除失败:', close: '关闭', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 78ba4c94..8fbe3daa 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -192,6 +192,7 @@ const zhHant = { saveConfig: '儲存設定', saving: '儲存中...', confirmDeletePlugin: '您確定要刪除外掛({{author}}/{{name}})嗎?', + deleteDataCheckbox: '同時刪除外掛設定和持久化儲存', confirmDelete: '確認刪除', deleteError: '刪除失敗:', close: '關閉',