feat(iframe): add iframe bridge for ragflow integration

- Implement Penpal-based iframe communication bridge between host and child apps
- Add route handling for ragflow integration with '/route-ragflow' prefix
- Update navigation hooks to support embedded mode via iframe bridge
- Configure build and dependencies for new iframe-bridge package
- Adjust nginx config for proper SPA routing in subpath deployments
This commit is contained in:
2025-11-10 16:11:21 +08:00
parent 81fa34669a
commit a3ff72e575
17 changed files with 377 additions and 37137 deletions

View File

@@ -108,7 +108,7 @@ function AgentListPage() {
onCreateAgent={() => setCreateOpen(true)}
onEdit={(agent) => { setEditTarget(agent); setEditOpen(true); }}
onView={(agent) => {
navigate(`/agent/${agent.id}`);
navigate(`/route-ragflow/agent/${agent.id}`);
}}
onDelete={async (agent) => {
const confirmed = await dialog.confirm({

View File

@@ -0,0 +1,40 @@
import { useEffect, useMemo, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { createPenpalHostBridge } from '@teres/iframe-bridge';
import logger from '@/utils/logger';
export default function RagflowAgentPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const iframeRef = useRef<HTMLIFrameElement>(null);
const src = useMemo(() => `/ragflow/agent/${id ?? ''}`, [id]);
logger.info('RagflowAgentPage', id, src);
useEffect(() => {
const el = iframeRef.current;
if (!el) return;
const { destroy } = createPenpalHostBridge({
iframe: el,
methods: {
navigate: (to: string) => navigate(to),
close: () => navigate('/agents'),
agentReady: () => {
// 可选:在需要时记录或触发后续逻辑
},
},
});
return () => destroy();
}, [navigate]);
// 如需兼容旧的 postMessage 事件,可保留以下监听;为了纯 Penpal此处移除
return (
<iframe
ref={iframeRef}
title="ragflow-agent"
src={src}
style={{ width: '100%', height: '100vh', border: 'none' }}
/>
);
}

View File

@@ -0,0 +1,37 @@
import { useEffect, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { createPenpalHostBridge, toChildPath } from '@teres/iframe-bridge';
export default function RagflowIframePage() {
const location = useLocation();
const navigate = useNavigate();
// 将宿主的 "/route-ragflow/**" 路径转换为子应用的 "/ragflow/**" 路径
const childPath = toChildPath(location.pathname);
const src = `/ragflow${childPath}${location.search}${location.hash}`;
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
const el = iframeRef.current;
if (!el) return;
const { destroy } = createPenpalHostBridge({
iframe: el,
methods: {
navigate: (to: string) => navigate(to),
close: () => navigate('/agents'),
agentReady: () => {
// 可选:记录或触发后续逻辑
},
},
});
return () => destroy();
}, [navigate, src]);
return (
<iframe
ref={iframeRef}
title="ragflow"
src={src}
style={{ width: '100%', height: '100vh', border: 'none' }}
/>
);
}

View File

@@ -0,0 +1,9 @@
import { Outlet } from 'react-router-dom';
export default function RagflowLayout() {
return (
<div style={{ width: '100%', height: '100vh' }}>
<Outlet />
</div>
);
}

View File

@@ -27,6 +27,9 @@ import ChunkParsedResult from '@/pages/chunk/parsed-result';
import DocumentPreview from '@/pages/chunk/document-preview';
import AgentDetailPage from '@/pages/agent-mui/detail';
// import AgentDetailPage from '@/pages/agent';
import RagflowLayout from '@/pages/ragflow/layout';
import RagflowIframePage from '@/pages/ragflow/iframe';
import RagflowAgentPage from '@/pages/ragflow/agent';
const AppRoutes = () => {
return (
@@ -70,6 +73,13 @@ const AppRoutes = () => {
<Route path="mcp" element={<MCPSetting />} />
</Route>
{/* 通过 iframe 承载 ragflow 应用,避免路由冲突并保持同源 */}
{/* ragflow 路由分组layout 承载agent 为特定页面,其余走通用 iframe */}
<Route path="route-ragflow" element={<RagflowLayout />}>
<Route path="agent/:id" element={<RagflowAgentPage />} />
<Route path="*" element={<RagflowIframePage />} />
</Route>
{/* 处理未匹配的路由 */}
<Route path="*" element={<Navigate to="/knowledge" replace />} />
</Routes>