mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-26 03:44:58 +08:00
perf: config reset logic (#1742)
* fix: inherit settings from existing settings * feat: add optional data cleanup checkbox to plugin uninstall dialog (#1743) * Initial plan * Add checkbox for plugin config/storage deletion - Add delete_data parameter to backend API endpoint - Update delete_plugin flow to clean up settings and binary storage - Add checkbox in uninstall dialog using shadcn/ui - Add translations for checkbox label in all languages Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * perf: param list --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> Co-authored-by: Junyan Qin <rockchinq@gmail.com> * chore: fix linter errors --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
Junyan Qin
parent
350e59fa6b
commit
3d12632c9f
@@ -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}',
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<PluginInstalledComponentRef>(
|
||||
PluginOperationType.DELETE,
|
||||
);
|
||||
const [targetPlugin, setTargetPlugin] = useState<PluginCardVO | null>(null);
|
||||
const [deleteData, setDeleteData] = useState<boolean>(false);
|
||||
|
||||
const asyncTask = useAsyncTask({
|
||||
onSuccess: () => {
|
||||
@@ -108,6 +110,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
setTargetPlugin(plugin);
|
||||
setOperationType(PluginOperationType.DELETE);
|
||||
setShowOperationModal(true);
|
||||
setDeleteData(false);
|
||||
asyncTask.reset();
|
||||
}
|
||||
|
||||
@@ -123,7 +126,11 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
|
||||
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,6 +168,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
{asyncTask.status === AsyncTaskStatus.WAIT_INPUT && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
{operationType === PluginOperationType.DELETE
|
||||
? t('plugins.confirmDeletePlugin', {
|
||||
@@ -172,6 +180,24 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
name: targetPlugin?.name ?? '',
|
||||
})}
|
||||
</div>
|
||||
{operationType === PluginOperationType.DELETE && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="delete-data"
|
||||
checked={deleteData}
|
||||
onCheckedChange={(checked) =>
|
||||
setDeleteData(checked === true)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="delete-data"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
||||
>
|
||||
{t('plugins.deleteDataCheckbox')}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{asyncTask.status === AsyncTaskStatus.RUNNING && (
|
||||
<div>
|
||||
|
||||
@@ -480,8 +480,11 @@ export class BackendClient extends BaseHttpClient {
|
||||
public removePlugin(
|
||||
author: string,
|
||||
name: string,
|
||||
deleteData: boolean = false,
|
||||
): Promise<AsyncTaskCreatedResp> {
|
||||
return this.delete(`/api/v1/plugins/${author}/${name}`);
|
||||
return this.delete(
|
||||
`/api/v1/plugins/${author}/${name}?delete_data=${deleteData}`,
|
||||
);
|
||||
}
|
||||
|
||||
public upgradePlugin(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -200,7 +200,8 @@ const jaJP = {
|
||||
saveConfig: '設定を保存',
|
||||
saving: '保存中...',
|
||||
confirmDeletePlugin:
|
||||
'プラグイン「{{author}}/{{name}}」を削除してもよろしいですか?この操作により、プラグインの設定も削除されます。',
|
||||
'プラグイン「{{author}}/{{name}}」を削除してもよろしいですか?',
|
||||
deleteDataCheckbox: 'プラグイン設定と永続化ストレージも削除する',
|
||||
confirmDelete: '削除を確認',
|
||||
deleteError: '削除に失敗しました:',
|
||||
close: '閉じる',
|
||||
|
||||
@@ -191,8 +191,8 @@ const zhHans = {
|
||||
cancel: '取消',
|
||||
saveConfig: '保存配置',
|
||||
saving: '保存中...',
|
||||
confirmDeletePlugin:
|
||||
'你确定要删除插件({{author}}/{{name}})吗?这将同时删除插件的配置。',
|
||||
confirmDeletePlugin: '你确定要删除插件({{author}}/{{name}})吗?',
|
||||
deleteDataCheckbox: '同时删除插件配置和持久化存储',
|
||||
confirmDelete: '确认删除',
|
||||
deleteError: '删除失败:',
|
||||
close: '关闭',
|
||||
|
||||
@@ -192,6 +192,7 @@ const zhHant = {
|
||||
saveConfig: '儲存設定',
|
||||
saving: '儲存中...',
|
||||
confirmDeletePlugin: '您確定要刪除外掛({{author}}/{{name}})嗎?',
|
||||
deleteDataCheckbox: '同時刪除外掛設定和持久化儲存',
|
||||
confirmDelete: '確認刪除',
|
||||
deleteError: '刪除失敗:',
|
||||
close: '關閉',
|
||||
|
||||
Reference in New Issue
Block a user