Feat/dbm20 rag (#2037)

* feat(rag): add knowledge base migration from v4.9.0 to plugin architecture

Rewrite dbm020 to backup old knowledge_bases data and preserve
external_knowledge_bases table. Add migration API endpoints and
frontend dialog so users can opt-in to auto-install LangRAG plugin
and restore their knowledge bases with original UUIDs preserved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(rag): query marketplace for actual plugin version instead of 'latest'

The marketplace API does not support 'latest' as a version string.
Fetch the plugin info first to get latest_version, then use that
concrete version for installation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 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>

* refactor: to red and no more

* fix lint

* fix ruff lint

* feat: add external migration

* fix: show

* feat: add external plugin auto download

* feat: update migration messages for knowledge base in multiple languages

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
huanghuoguoguo
2026-03-09 20:05:38 +08:00
committed by GitHub
parent d31f25c8df
commit 2a74a8d6ae
11 changed files with 723 additions and 93 deletions

View File

@@ -0,0 +1,157 @@
'use client';
import { useState } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { useTranslation } from 'react-i18next';
import { httpClient } from '@/app/infra/http/HttpClient';
import { useAsyncTask, AsyncTaskStatus } from '@/hooks/useAsyncTask';
import { toast } from 'sonner';
import { Loader2 } from 'lucide-react';
interface KBMigrationDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
internalKbCount: number;
externalKbCount: number;
onMigrationComplete: () => void;
}
export default function KBMigrationDialog({
open,
onOpenChange,
internalKbCount,
externalKbCount,
onMigrationComplete,
}: KBMigrationDialogProps) {
const { t } = useTranslation();
const [dismissing, setDismissing] = useState(false);
const asyncTask = useAsyncTask({
onSuccess: () => {
toast.success(t('knowledge.migration.success'));
onOpenChange(false);
onMigrationComplete();
},
onError: (error) => {
toast.error(`${t('knowledge.migration.error')}${error}`);
},
});
const handleMigration = async (installPlugin: boolean) => {
try {
const resp = await httpClient.executeRagMigration(installPlugin);
asyncTask.startTask(resp.task_id);
} catch {
toast.error(t('knowledge.migration.error'));
}
};
const handleDismiss = async () => {
setDismissing(true);
try {
await httpClient.dismissRagMigration();
onOpenChange(false);
} catch {
toast.error(t('knowledge.migration.dismissError'));
} finally {
setDismissing(false);
}
};
const isRunning = asyncTask.status === AsyncTaskStatus.RUNNING;
const isError = asyncTask.status === AsyncTaskStatus.ERROR;
const totalCount = internalKbCount + externalKbCount;
return (
<Dialog
open={open}
onOpenChange={(v) => {
if (!isRunning) onOpenChange(v);
}}
>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t('knowledge.migration.title')}</DialogTitle>
<DialogDescription>
{t('knowledge.migration.description')}
</DialogDescription>
</DialogHeader>
<div className="py-4 space-y-3">
{!isRunning && !isError && (
<p className="text-sm text-muted-foreground">
{t('knowledge.migration.detected', {
total: totalCount,
internal: internalKbCount,
external: externalKbCount,
})}
</p>
)}
{isRunning && (
<div className="flex items-center gap-3">
<Loader2 className="h-5 w-5 animate-spin text-primary" />
<p className="text-sm">{t('knowledge.migration.running')}</p>
</div>
)}
{isError && (
<div className="space-y-2">
<p className="text-sm text-destructive">
{t('knowledge.migration.error')}
</p>
{asyncTask.error && (
<p className="text-xs text-muted-foreground bg-muted p-2 rounded">
{asyncTask.error}
</p>
)}
</div>
)}
</div>
<DialogFooter className="flex flex-col gap-2 sm:flex-col">
{!isRunning && !isError && (
<>
<Button onClick={() => handleMigration(true)} className="w-full">
{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 && (
<Button onClick={() => handleMigration(true)} className="w-full">
{t('knowledge.migration.retry')}
</Button>
)}
{!isRunning && (
<Button
variant="ghost"
onClick={handleDismiss}
disabled={dismissing}
className="w-full text-destructive hover:text-destructive"
>
{t('knowledge.migration.dismiss')}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -7,6 +7,7 @@ import { useEffect, useState } from 'react';
import { KnowledgeBaseVO } from '@/app/home/knowledge/components/kb-card/KBCardVO';
import KBCard from '@/app/home/knowledge/components/kb-card/KBCard';
import KBDetailDialog from '@/app/home/knowledge/KBDetailDialog';
import KBMigrationDialog from '@/app/home/knowledge/components/kb-migration-dialog/KBMigrationDialog';
import { httpClient } from '@/app/infra/http/HttpClient';
import { KnowledgeBase } from '@/app/infra/entities/api';
@@ -18,10 +19,29 @@ export default function KnowledgePage() {
const [selectedKbId, setSelectedKbId] = useState<string>('');
const [detailDialogOpen, setDetailDialogOpen] = useState(false);
// Migration dialog state
const [migrationDialogOpen, setMigrationDialogOpen] = useState(false);
const [migrationInternalCount, setMigrationInternalCount] = useState(0);
const [migrationExternalCount, setMigrationExternalCount] = useState(0);
useEffect(() => {
getKnowledgeBaseList();
checkMigrationStatus();
}, []);
async function checkMigrationStatus() {
try {
const resp = await httpClient.getRagMigrationStatus();
if (resp.needed) {
setMigrationInternalCount(resp.internal_kb_count);
setMigrationExternalCount(resp.external_kb_count);
setMigrationDialogOpen(true);
}
} catch {
// Silently ignore - migration check is non-critical
}
}
async function getKnowledgeBaseList() {
const resp = await httpClient.getKnowledgeBases();
@@ -85,8 +105,20 @@ export default function KnowledgePage() {
getKnowledgeBaseList();
};
const handleMigrationComplete = () => {
getKnowledgeBaseList();
};
return (
<div>
<KBMigrationDialog
open={migrationDialogOpen}
onOpenChange={setMigrationDialogOpen}
internalKbCount={migrationInternalCount}
externalKbCount={migrationExternalCount}
onMigrationComplete={handleMigrationComplete}
/>
<KBDetailDialog
open={detailDialogOpen}
onOpenChange={setDetailDialogOpen}

View File

@@ -262,6 +262,12 @@ export interface ApiRespSystemInfo {
limitation: SystemLimitation;
}
export interface RagMigrationStatusResp {
needed: boolean;
internal_kb_count: number;
external_kb_count: number;
}
export interface ApiRespPluginSystemStatus {
is_enable: boolean;
is_connected: boolean;

View File

@@ -40,6 +40,7 @@ import {
ModelProvider,
ApiRespKnowledgeEngines,
ApiRespParsers,
RagMigrationStatusResp,
} from '@/app/infra/entities/api';
import { Plugin } from '@/app/infra/entities/plugin';
import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest';
@@ -710,6 +711,23 @@ export class BackendClient extends BaseHttpClient {
return this.get('/api/v1/system/status/plugin-system');
}
// ============ RAG Migration API ============
public getRagMigrationStatus(): Promise<RagMigrationStatusResp> {
return this.get('/api/v1/knowledge/migration/status');
}
public executeRagMigration(
installPlugin: boolean = true,
): Promise<AsyncTaskCreatedResp> {
return this.post('/api/v1/knowledge/migration/execute', {
install_plugin: installPlugin,
});
}
public dismissRagMigration(): Promise<object> {
return this.post('/api/v1/knowledge/migration/dismiss');
}
public getPluginDebugInfo(): Promise<{
debug_url: string;
plugin_debug_key: string;