mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-27 07:54:19 +00: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.http_status(404, -1, 'plugin not found')
|
||||||
return self.success(data={'plugin': plugin})
|
return self.success(data={'plugin': plugin})
|
||||||
elif quart.request.method == 'DELETE':
|
elif quart.request.method == 'DELETE':
|
||||||
|
delete_data = quart.request.args.get('delete_data', 'false').lower() == 'true'
|
||||||
ctx = taskmgr.TaskContext.new()
|
ctx = taskmgr.TaskContext.new()
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
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',
|
kind='plugin-operation',
|
||||||
name=f'plugin-remove-{plugin_name}',
|
name=f'plugin-remove-{plugin_name}',
|
||||||
label=f'Removing plugin {plugin_name}',
|
label=f'Removing plugin {plugin_name}',
|
||||||
|
|||||||
+11
-1
@@ -177,7 +177,11 @@ class PluginRuntimeConnector:
|
|||||||
task_context.trace(trace)
|
task_context.trace(trace)
|
||||||
|
|
||||||
async def delete_plugin(
|
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]:
|
) -> dict[str, Any]:
|
||||||
async for ret in self.handler.delete_plugin(plugin_author, plugin_name):
|
async for ret in self.handler.delete_plugin(plugin_author, plugin_name):
|
||||||
current_action = ret.get('current_action', None)
|
current_action = ret.get('current_action', None)
|
||||||
@@ -190,6 +194,12 @@ class PluginRuntimeConnector:
|
|||||||
if task_context is not None:
|
if task_context is not None:
|
||||||
task_context.trace(trace)
|
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]]:
|
async def list_plugins(self) -> list[dict[str, Any]]:
|
||||||
if not self.is_enable_plugin:
|
if not self.is_enable_plugin:
|
||||||
return []
|
return []
|
||||||
|
|||||||
+24
-1
@@ -56,7 +56,9 @@ class RuntimeConnectionHandler(handler.Handler):
|
|||||||
.where(persistence_plugin.PluginSetting.plugin_name == plugin_name)
|
.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
|
# delete plugin setting
|
||||||
await self.ap.persistence_mgr.execute_async(
|
await self.ap.persistence_mgr.execute_async(
|
||||||
sqlalchemy.delete(persistence_plugin.PluginSetting)
|
sqlalchemy.delete(persistence_plugin.PluginSetting)
|
||||||
@@ -71,6 +73,10 @@ class RuntimeConnectionHandler(handler.Handler):
|
|||||||
plugin_name=plugin_name,
|
plugin_name=plugin_name,
|
||||||
install_source=install_source,
|
install_source=install_source,
|
||||||
install_info=install_info,
|
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,
|
'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]:
|
async def call_tool(self, tool_name: str, parameters: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Call tool"""
|
"""Call tool"""
|
||||||
result = await self.call_action(
|
result = await self.call_action(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -43,6 +44,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
|||||||
PluginOperationType.DELETE,
|
PluginOperationType.DELETE,
|
||||||
);
|
);
|
||||||
const [targetPlugin, setTargetPlugin] = useState<PluginCardVO | null>(null);
|
const [targetPlugin, setTargetPlugin] = useState<PluginCardVO | null>(null);
|
||||||
|
const [deleteData, setDeleteData] = useState<boolean>(false);
|
||||||
|
|
||||||
const asyncTask = useAsyncTask({
|
const asyncTask = useAsyncTask({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -108,6 +110,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
|||||||
setTargetPlugin(plugin);
|
setTargetPlugin(plugin);
|
||||||
setOperationType(PluginOperationType.DELETE);
|
setOperationType(PluginOperationType.DELETE);
|
||||||
setShowOperationModal(true);
|
setShowOperationModal(true);
|
||||||
|
setDeleteData(false);
|
||||||
asyncTask.reset();
|
asyncTask.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +126,11 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
|||||||
|
|
||||||
const apiCall =
|
const apiCall =
|
||||||
operationType === PluginOperationType.DELETE
|
operationType === PluginOperationType.DELETE
|
||||||
? httpClient.removePlugin(targetPlugin.author, targetPlugin.name)
|
? httpClient.removePlugin(
|
||||||
|
targetPlugin.author,
|
||||||
|
targetPlugin.name,
|
||||||
|
deleteData,
|
||||||
|
)
|
||||||
: httpClient.upgradePlugin(targetPlugin.author, targetPlugin.name);
|
: httpClient.upgradePlugin(targetPlugin.author, targetPlugin.name);
|
||||||
|
|
||||||
apiCall
|
apiCall
|
||||||
@@ -161,16 +168,35 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{asyncTask.status === AsyncTaskStatus.WAIT_INPUT && (
|
{asyncTask.status === AsyncTaskStatus.WAIT_INPUT && (
|
||||||
<div>
|
<div className="flex flex-col gap-4">
|
||||||
{operationType === PluginOperationType.DELETE
|
<div>
|
||||||
? t('plugins.confirmDeletePlugin', {
|
{operationType === PluginOperationType.DELETE
|
||||||
author: targetPlugin?.author ?? '',
|
? t('plugins.confirmDeletePlugin', {
|
||||||
name: targetPlugin?.name ?? '',
|
author: targetPlugin?.author ?? '',
|
||||||
})
|
name: targetPlugin?.name ?? '',
|
||||||
: t('plugins.confirmUpdatePlugin', {
|
})
|
||||||
author: targetPlugin?.author ?? '',
|
: t('plugins.confirmUpdatePlugin', {
|
||||||
name: targetPlugin?.name ?? '',
|
author: targetPlugin?.author ?? '',
|
||||||
})}
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{asyncTask.status === AsyncTaskStatus.RUNNING && (
|
{asyncTask.status === AsyncTaskStatus.RUNNING && (
|
||||||
|
|||||||
@@ -480,8 +480,11 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
public removePlugin(
|
public removePlugin(
|
||||||
author: string,
|
author: string,
|
||||||
name: string,
|
name: string,
|
||||||
|
deleteData: boolean = false,
|
||||||
): Promise<AsyncTaskCreatedResp> {
|
): Promise<AsyncTaskCreatedResp> {
|
||||||
return this.delete(`/api/v1/plugins/${author}/${name}`);
|
return this.delete(
|
||||||
|
`/api/v1/plugins/${author}/${name}?delete_data=${deleteData}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public upgradePlugin(
|
public upgradePlugin(
|
||||||
|
|||||||
@@ -199,7 +199,9 @@ const enUS = {
|
|||||||
saveConfig: 'Save Config',
|
saveConfig: 'Save Config',
|
||||||
saving: 'Saving...',
|
saving: 'Saving...',
|
||||||
confirmDeletePlugin:
|
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',
|
confirmDelete: 'Confirm Delete',
|
||||||
deleteError: 'Delete failed: ',
|
deleteError: 'Delete failed: ',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
|
|||||||
@@ -200,7 +200,8 @@ const jaJP = {
|
|||||||
saveConfig: '設定を保存',
|
saveConfig: '設定を保存',
|
||||||
saving: '保存中...',
|
saving: '保存中...',
|
||||||
confirmDeletePlugin:
|
confirmDeletePlugin:
|
||||||
'プラグイン「{{author}}/{{name}}」を削除してもよろしいですか?この操作により、プラグインの設定も削除されます。',
|
'プラグイン「{{author}}/{{name}}」を削除してもよろしいですか?',
|
||||||
|
deleteDataCheckbox: 'プラグイン設定と永続化ストレージも削除する',
|
||||||
confirmDelete: '削除を確認',
|
confirmDelete: '削除を確認',
|
||||||
deleteError: '削除に失敗しました:',
|
deleteError: '削除に失敗しました:',
|
||||||
close: '閉じる',
|
close: '閉じる',
|
||||||
|
|||||||
@@ -191,8 +191,8 @@ const zhHans = {
|
|||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
saveConfig: '保存配置',
|
saveConfig: '保存配置',
|
||||||
saving: '保存中...',
|
saving: '保存中...',
|
||||||
confirmDeletePlugin:
|
confirmDeletePlugin: '你确定要删除插件({{author}}/{{name}})吗?',
|
||||||
'你确定要删除插件({{author}}/{{name}})吗?这将同时删除插件的配置。',
|
deleteDataCheckbox: '同时删除插件配置和持久化存储',
|
||||||
confirmDelete: '确认删除',
|
confirmDelete: '确认删除',
|
||||||
deleteError: '删除失败:',
|
deleteError: '删除失败:',
|
||||||
close: '关闭',
|
close: '关闭',
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ const zhHant = {
|
|||||||
saveConfig: '儲存設定',
|
saveConfig: '儲存設定',
|
||||||
saving: '儲存中...',
|
saving: '儲存中...',
|
||||||
confirmDeletePlugin: '您確定要刪除外掛({{author}}/{{name}})嗎?',
|
confirmDeletePlugin: '您確定要刪除外掛({{author}}/{{name}})嗎?',
|
||||||
|
deleteDataCheckbox: '同時刪除外掛設定和持久化儲存',
|
||||||
confirmDelete: '確認刪除',
|
confirmDelete: '確認刪除',
|
||||||
deleteError: '刪除失敗:',
|
deleteError: '刪除失敗:',
|
||||||
close: '關閉',
|
close: '關閉',
|
||||||
|
|||||||
Reference in New Issue
Block a user