feat(rag): add data-only migration option and fix dialog width

Add option to migrate knowledge base data without auto-installing
the LangRAG plugin (for offline/intranet environments). Also
narrow the migration dialog to match other confirmation dialogs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
youhuanghe
2026-03-08 15:05:05 +00:00
parent 8da52b6dc7
commit 1b37dababa
7 changed files with 109 additions and 89 deletions
@@ -2,6 +2,7 @@ import asyncio
import json import json
import httpx import httpx
import quart
import sqlalchemy import sqlalchemy
from ... import group from ... import group
@@ -52,55 +53,56 @@ class KnowledgeMigrationRouterGroup(group.RouterGroup):
) )
return result.first() is not None return result.first() is not None
async def _execute_rag_migration(self, task_context: taskmgr.TaskContext): async def _execute_rag_migration(self, task_context: taskmgr.TaskContext, install_plugin: bool = True):
"""Execute RAG migration: install langrag plugin and restore backup data.""" """Execute RAG migration: optionally install langrag plugin and restore backup data."""
warnings = [] warnings = []
# Step 1: Install langrag plugin from marketplace if install_plugin:
task_context.trace('Installing LangRAG plugin from marketplace...', action='install-plugin') # Step 1: Install langrag plugin from marketplace
try: task_context.trace('Installing LangRAG plugin from marketplace...', action='install-plugin')
# Query marketplace for latest version
space_url = self.ap.instance_config.data.get('space', {}).get('url', DEFAULT_SPACE_URL).rstrip('/')
async with httpx.AsyncClient(trust_env=True, timeout=15) as client:
resp = await client.get(
f'{space_url}/api/v1/marketplace/plugins/{LANGRAG_PLUGIN_AUTHOR}/{LANGRAG_PLUGIN_NAME}'
)
resp.raise_for_status()
plugin_data = resp.json().get('data', {}).get('plugin', {})
plugin_version = plugin_data.get('latest_version')
if not plugin_version:
raise Exception('Could not determine latest LangRAG version from marketplace')
install_info = {
'plugin_author': LANGRAG_PLUGIN_AUTHOR,
'plugin_name': LANGRAG_PLUGIN_NAME,
'plugin_version': plugin_version,
}
await self.ap.plugin_connector.install_plugin(
PluginInstallSource.MARKETPLACE, install_info, task_context=task_context
)
except Exception as e:
# Plugin may already be installed
self.ap.logger.warning(f'LangRAG plugin install returned: {e}')
task_context.trace(f'Plugin install note: {e}')
# Step 2: Wait for the plugin to be available
task_context.trace('Waiting for LangRAG plugin to become available...', action='wait-plugin')
max_retries = 30
for i in range(max_retries):
try: try:
engines = await self.ap.plugin_connector.list_knowledge_engines() # Query marketplace for latest version
engine_ids = [e.get('plugin_id') for e in engines] space_url = self.ap.instance_config.data.get('space', {}).get('url', DEFAULT_SPACE_URL).rstrip('/')
if LANGRAG_PLUGIN_ID in engine_ids: async with httpx.AsyncClient(trust_env=True, timeout=15) as client:
task_context.trace('LangRAG plugin is ready.') resp = await client.get(
break f'{space_url}/api/v1/marketplace/plugins/{LANGRAG_PLUGIN_AUTHOR}/{LANGRAG_PLUGIN_NAME}'
except Exception: )
pass resp.raise_for_status()
if i == max_retries - 1: plugin_data = resp.json().get('data', {}).get('plugin', {})
raise Exception( plugin_version = plugin_data.get('latest_version')
f'LangRAG plugin ({LANGRAG_PLUGIN_ID}) did not become available after {max_retries} retries' if not plugin_version:
raise Exception('Could not determine latest LangRAG version from marketplace')
install_info = {
'plugin_author': LANGRAG_PLUGIN_AUTHOR,
'plugin_name': LANGRAG_PLUGIN_NAME,
'plugin_version': plugin_version,
}
await self.ap.plugin_connector.install_plugin(
PluginInstallSource.MARKETPLACE, install_info, task_context=task_context
) )
await asyncio.sleep(2) except Exception as e:
# Plugin may already be installed
self.ap.logger.warning(f'LangRAG plugin install returned: {e}')
task_context.trace(f'Plugin install note: {e}')
# Step 2: Wait for the plugin to be available
task_context.trace('Waiting for LangRAG plugin to become available...', action='wait-plugin')
max_retries = 30
for i in range(max_retries):
try:
engines = await self.ap.plugin_connector.list_knowledge_engines()
engine_ids = [e.get('plugin_id') for e in engines]
if LANGRAG_PLUGIN_ID in engine_ids:
task_context.trace('LangRAG plugin is ready.')
break
except Exception:
pass
if i == max_retries - 1:
raise Exception(
f'LangRAG plugin ({LANGRAG_PLUGIN_ID}) did not become available after {max_retries} retries'
)
await asyncio.sleep(2)
# Step 3: Restore internal knowledge bases from backup # Step 3: Restore internal knowledge bases from backup
task_context.trace('Restoring internal knowledge bases...', action='restore-internal') task_context.trace('Restoring internal knowledge bases...', action='restore-internal')
@@ -283,9 +285,12 @@ class KnowledgeMigrationRouterGroup(group.RouterGroup):
if not needed: if not needed:
return self.http_status(400, -1, 'RAG migration is not needed') return self.http_status(400, -1, 'RAG migration is not needed')
data = await quart.request.get_json(silent=True) or {}
install_plugin = data.get('install_plugin', 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._execute_rag_migration(task_context=ctx), self._execute_rag_migration(task_context=ctx, install_plugin=install_plugin),
kind='rag-migration', kind='rag-migration',
name='rag-migration-execute', name='rag-migration-execute',
label='Migrating knowledge bases to plugin architecture', label='Migrating knowledge bases to plugin architecture',
@@ -45,9 +45,9 @@ export default function KBMigrationDialog({
}, },
}); });
const handleStartMigration = async () => { const handleMigration = async (installPlugin: boolean) => {
try { try {
const resp = await httpClient.executeRagMigration(); const resp = await httpClient.executeRagMigration(installPlugin);
asyncTask.startTask(resp.task_id); asyncTask.startTask(resp.task_id);
} catch { } catch {
toast.error(t('knowledge.migration.error')); toast.error(t('knowledge.migration.error'));
@@ -77,7 +77,7 @@ export default function KBMigrationDialog({
if (!isRunning) onOpenChange(v); if (!isRunning) onOpenChange(v);
}} }}
> >
<DialogContent className="sm:max-w-[500px]"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>{t('knowledge.migration.title')}</DialogTitle> <DialogTitle>{t('knowledge.migration.title')}</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -87,18 +87,13 @@ export default function KBMigrationDialog({
<div className="py-4 space-y-3"> <div className="py-4 space-y-3">
{!isRunning && !isError && ( {!isRunning && !isError && (
<> <p className="text-sm text-muted-foreground">
<p className="text-sm text-muted-foreground"> {t('knowledge.migration.detected', {
{t('knowledge.migration.detected', { total: totalCount,
total: totalCount, internal: internalKbCount,
internal: internalKbCount, external: externalKbCount,
external: externalKbCount, })}
})} </p>
</p>
<p className="text-sm text-muted-foreground">
{t('knowledge.migration.installHint')}
</p>
</>
)} )}
{isRunning && ( {isRunning && (
@@ -122,26 +117,39 @@ export default function KBMigrationDialog({
)} )}
</div> </div>
<DialogFooter> <DialogFooter className="flex flex-col gap-2 sm:flex-col">
{!isRunning && (
<Button
variant="outline"
onClick={handleDismiss}
disabled={dismissing}
>
{t('knowledge.migration.dismiss')}
</Button>
)}
{!isRunning && !isError && ( {!isRunning && !isError && (
<Button onClick={handleStartMigration}> <>
{t('knowledge.migration.start')} <Button onClick={() => handleMigration(true)} className="w-full">
</Button> {t('knowledge.migration.startWithInstall')}
</Button>
<Button
variant="outline"
onClick={() => handleMigration(false)}
className="w-full"
>
{t('knowledge.migration.startDataOnly')}
</Button>
<p className="text-xs text-muted-foreground text-center">
{t('knowledge.migration.dataOnlyHint')}
</p>
</>
)} )}
{isError && ( {isError && (
<Button onClick={handleStartMigration}> <Button onClick={() => handleMigration(true)} className="w-full">
{t('knowledge.migration.retry')} {t('knowledge.migration.retry')}
</Button> </Button>
)} )}
{!isRunning && (
<Button
variant="ghost"
onClick={handleDismiss}
disabled={dismissing}
className="w-full"
>
{t('knowledge.migration.dismiss')}
</Button>
)}
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
+6 -2
View File
@@ -716,8 +716,12 @@ export class BackendClient extends BaseHttpClient {
return this.get('/api/v1/knowledge/migration/status'); return this.get('/api/v1/knowledge/migration/status');
} }
public executeRagMigration(): Promise<AsyncTaskCreatedResp> { public executeRagMigration(
return this.post('/api/v1/knowledge/migration/execute'); installPlugin: boolean = true,
): Promise<AsyncTaskCreatedResp> {
return this.post('/api/v1/knowledge/migration/execute', {
install_plugin: installPlugin,
});
} }
public dismissRagMigration(): Promise<object> { public dismissRagMigration(): Promise<object> {
+4 -3
View File
@@ -779,9 +779,10 @@ const enUS = {
'Legacy knowledge base data detected. Migration to the new plugin-based RAG architecture is required.', 'Legacy knowledge base data detected. Migration to the new plugin-based RAG architecture is required.',
detected: detected:
'Found {{total}} knowledge base(s) to migrate ({{internal}} internal, {{external}} external).', 'Found {{total}} knowledge base(s) to migrate ({{internal}} internal, {{external}} external).',
installHint: startWithInstall: 'Auto-install Plugin & Migrate',
'Migration will automatically install the LangRAG plugin and restore your knowledge base data. Documents and vector data will be preserved.', startDataOnly: 'Migrate Data Only',
start: 'Migrate Now', dataOnlyHint:
'"Migrate Data Only" is for offline/intranet environments. Please install the LangRAG plugin manually before migrating.',
dismiss: 'Skip for Now', dismiss: 'Skip for Now',
running: 'Migrating knowledge bases, please wait...', running: 'Migrating knowledge bases, please wait...',
success: 'Knowledge base migration completed', success: 'Knowledge base migration completed',
+4 -3
View File
@@ -768,9 +768,10 @@ const jaJP = {
'旧バージョンのナレッジベースデータが検出されました。新しいプラグインベースのRAGアーキテクチャへの移行が必要です。', '旧バージョンのナレッジベースデータが検出されました。新しいプラグインベースのRAGアーキテクチャへの移行が必要です。',
detected: detected:
'移行が必要なナレッジベースが{{total}}件見つかりました(内部{{internal}}件、外部{{external}}件)。', '移行が必要なナレッジベースが{{total}}件見つかりました(内部{{internal}}件、外部{{external}}件)。',
installHint: startWithInstall: 'プラグインを自動インストールして移行',
'移行により LangRAG プラグインが自動的にインストールされ、ナレッジベースデータが復元されます。ドキュメントとベクトルデータは保持されます。', startDataOnly: 'データのみ移行',
start: '今すぐ移行', dataOnlyHint:
'「データのみ移行」はオフライン環境向けです。事前に LangRAG プラグインを手動でインストールしてください。',
dismiss: '後で', dismiss: '後で',
running: 'ナレッジベースを移行中です。しばらくお待ちください...', running: 'ナレッジベースを移行中です。しばらくお待ちください...',
success: 'ナレッジベースの移行が完了しました', success: 'ナレッジベースの移行が完了しました',
+3 -3
View File
@@ -747,9 +747,9 @@ const zhHans = {
description: '检测到旧版知识库数据,需要迁移到新的插件化 RAG 架构。', description: '检测到旧版知识库数据,需要迁移到新的插件化 RAG 架构。',
detected: detected:
'共检测到 {{total}} 个知识库需要迁移({{internal}} 个内置知识库,{{external}} 个外部知识库)。', '共检测到 {{total}} 个知识库需要迁移({{internal}} 个内置知识库,{{external}} 个外部知识库)。',
installHint: startWithInstall: '自动安装插件并迁移',
'迁移将自动安装 LangRAG 插件并恢复您的知识库数据,文档和向量数据将被保留。', startDataOnly: '迁移数据',
start: '立即迁移', dataOnlyHint: '「仅迁移数据」适合内网环境使用,请自行安装 LangRAG 插件后再迁移',
dismiss: '暂不迁移', dismiss: '暂不迁移',
running: '正在迁移知识库,请稍候...', running: '正在迁移知识库,请稍候...',
success: '知识库迁移完成', success: '知识库迁移完成',
+4 -3
View File
@@ -727,9 +727,10 @@ const zhHant = {
description: '檢測到舊版知識庫資料,需要遷移到新的插件化 RAG 架構。', description: '檢測到舊版知識庫資料,需要遷移到新的插件化 RAG 架構。',
detected: detected:
'共檢測到 {{total}} 個知識庫需要遷移({{internal}} 個內建知識庫,{{external}} 個外部知識庫)。', '共檢測到 {{total}} 個知識庫需要遷移({{internal}} 個內建知識庫,{{external}} 個外部知識庫)。',
installHint: startWithInstall: '自動安裝插件並遷移',
'遷移將自動安裝 LangRAG 插件並恢復您的知識庫資料,文件和向量資料將被保留。', startDataOnly: '遷移資料',
start: '立即遷移', dataOnlyHint:
'「僅遷移資料」適合內網環境使用,請自行安裝 LangRAG 插件後再遷移。',
dismiss: '暫不遷移', dismiss: '暫不遷移',
running: '正在遷移知識庫,請稍候...', running: '正在遷移知識庫,請稍候...',
success: '知識庫遷移完成', success: '知識庫遷移完成',