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:
@@ -50,7 +50,9 @@ server {
|
||||
# ragflow_web(Umi)部署在子路径,支持 SPA 路由
|
||||
location ${RAGFLOW_BASE} {
|
||||
alias /usr/share/nginx/html/ragflow/;
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
# 注意:使用别名目录时,SPA 回退必须指向子路径下的 index.html
|
||||
# 否则会错误地回退到根应用的 index.html,导致页面刷新后空白或错路由
|
||||
try_files \$uri \$uri/ ${RAGFLOW_BASE}index.html;
|
||||
}
|
||||
|
||||
# 静态资源缓存
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:both": "pnpm -r --parallel --filter teres_web_frontend --filter ragflow_web run dev",
|
||||
"dev:ragflow": "pnpm -r --filter ragflow_web run dev",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@teres/iframe-bridge": "workspace:*",
|
||||
"penpal": "^6.2.1",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^7.3.4",
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -b"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -29,5 +32,8 @@
|
||||
"penpal": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.9.3"
|
||||
}
|
||||
}
|
||||
158
packages/iframe-bridge/src/index.ts
Normal file
158
packages/iframe-bridge/src/index.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* @teres/iframe-bridge
|
||||
* Penpal-based utilities for host ↔ iframe communication,
|
||||
* plus minimal helpers for embed detection and path conversion.
|
||||
*/
|
||||
import { connectToChild, connectToParent } from 'penpal';
|
||||
|
||||
// Penpal RPC API definitions
|
||||
export interface HostApi {
|
||||
navigate: (to: string) => void;
|
||||
close: () => void;
|
||||
agentReady?: (payload?: any) => void;
|
||||
}
|
||||
|
||||
export interface ChildApi {
|
||||
navigate?: (to: string) => void;
|
||||
close?: () => void;
|
||||
ready?: (payload?: any) => void;
|
||||
}
|
||||
|
||||
// Unified logger for debugging host/child interactions
|
||||
const LOG_PREFIX = '[@teres/iframe-bridge]';
|
||||
function log(...args: any[]) {
|
||||
try {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(LOG_PREFIX, ...args);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function safePathname(): string {
|
||||
try {
|
||||
const p = window.location?.pathname || '';
|
||||
log('safePathname()', p);
|
||||
return p;
|
||||
} catch {
|
||||
log('safePathname() error, return empty pathname');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function isInIframe(): boolean {
|
||||
try {
|
||||
const inIframe = window.self !== window.top;
|
||||
log('isInIframe()', inIframe);
|
||||
return inIframe;
|
||||
} catch {
|
||||
// Cross-origin access throws, treat as embedded
|
||||
log('isInIframe() cross-origin error, treat as embedded');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function isEmbedded(): boolean {
|
||||
const path = safePathname();
|
||||
const inIframe = isInIframe();
|
||||
const embedded = path.includes('/route-ragflow') || inIframe;
|
||||
log('isEmbedded()', { path, inIframe, embedded });
|
||||
return embedded;
|
||||
}
|
||||
|
||||
/** Add "/ragflow" prefix when targeting host routes. */
|
||||
export function toHostPath(to: string): string {
|
||||
const normalized = to.startsWith('/') ? to : `/${to}`;
|
||||
const target = normalized.startsWith('/route-ragflow') ? normalized : `/ragflow${normalized}`;
|
||||
log('toHostPath()', { input: to, normalized, target });
|
||||
return target;
|
||||
}
|
||||
|
||||
/** Remove leading "/route-ragflow" prefix when targeting child routes. */
|
||||
export function toChildPath(to: string): string {
|
||||
const target = to.replace(/^\/route-ragflow/, '') || '/';
|
||||
log('toChildPath()', { input: to, target });
|
||||
return target;
|
||||
}
|
||||
|
||||
// Cached Penpal connections
|
||||
let clientHostApiPromise: Promise<HostApi> | null = null;
|
||||
|
||||
/** Create a Penpal connection from child to parent and cache the Host API. */
|
||||
export function getClientHostApi(options?: { parentOrigin?: string }): Promise<HostApi> {
|
||||
if (!isEmbedded()) {
|
||||
log('getClientHostApi() aborted: not embedded');
|
||||
return Promise.reject(new Error('Not embedded'));
|
||||
}
|
||||
if (!clientHostApiPromise) {
|
||||
const parentOrigin = options?.parentOrigin ?? window.location.origin;
|
||||
log('getClientHostApi() connecting to parent', { parentOrigin });
|
||||
clientHostApiPromise = connectToParent<HostApi>({ parentOrigin }).promise.then((host) => {
|
||||
log('getClientHostApi() connected', {
|
||||
hasNavigate: typeof host.navigate === 'function',
|
||||
hasClose: typeof host.close === 'function',
|
||||
});
|
||||
return host;
|
||||
});
|
||||
}
|
||||
return clientHostApiPromise;
|
||||
}
|
||||
|
||||
export async function clientNavigate(to: string): Promise<void> {
|
||||
log('clientNavigate() begin', { to });
|
||||
const host = await getClientHostApi();
|
||||
await host.navigate(to);
|
||||
log('clientNavigate() done', { to });
|
||||
}
|
||||
|
||||
export async function clientClose(): Promise<void> {
|
||||
log('clientClose() begin');
|
||||
const host = await getClientHostApi();
|
||||
log('clientClose() host api', {
|
||||
hasClose: typeof host.close === 'function',
|
||||
});
|
||||
await host.close();
|
||||
log('clientClose() done');
|
||||
}
|
||||
|
||||
export async function clientReady(payload?: any): Promise<void> {
|
||||
log('clientReady() begin', { payload });
|
||||
const host = await getClientHostApi();
|
||||
const agentReady = host.agentReady;
|
||||
const hasAgentReady = typeof agentReady === 'function';
|
||||
log('clientReady() host agentReady', { hasAgentReady });
|
||||
if (hasAgentReady) await agentReady!(payload);
|
||||
log('clientReady() done');
|
||||
}
|
||||
|
||||
/** Establish Penpal host bridge bound to an iframe and expose Host API to child. */
|
||||
export function createPenpalHostBridge(params: {
|
||||
iframe: HTMLIFrameElement;
|
||||
origin?: string;
|
||||
methods: HostApi;
|
||||
}) {
|
||||
const childOrigin = params.origin ?? window.location.origin;
|
||||
log('createPenpalHostBridge() start', { childOrigin });
|
||||
const connection = connectToChild<ChildApi>({ iframe: params.iframe, childOrigin, methods: params.methods as any });
|
||||
connection.promise.then(() => log('createPenpalHostBridge() child connected')).catch((err) => log('createPenpalHostBridge() connect error', err));
|
||||
return {
|
||||
child: connection.promise,
|
||||
destroy: () => {
|
||||
log('createPenpalHostBridge() destroy');
|
||||
return connection.destroy?.();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Provide a default export with all APIs to maximize compatibility with bundlers
|
||||
// that prefer default-only federation or mis-handle named ESM exports in dev.
|
||||
const bridge = {
|
||||
isEmbedded,
|
||||
toHostPath,
|
||||
toChildPath,
|
||||
getClientHostApi,
|
||||
clientNavigate,
|
||||
clientClose,
|
||||
clientReady,
|
||||
createPenpalHostBridge,
|
||||
};
|
||||
|
||||
export default bridge;
|
||||
15
packages/iframe-bridge/tsconfig.json
Normal file
15
packages/iframe-bridge/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "../../tsconfig.node.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"tsBuildInfoFile": "./dist/.tsbuildinfo",
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"allowImportingTsExtensions": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -26,6 +26,9 @@ importers:
|
||||
'@mui/x-date-pickers':
|
||||
specifier: ^8.14.0
|
||||
version: 8.14.0(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(date-fns@4.1.0)(dayjs@1.11.18)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@teres/iframe-bridge':
|
||||
specifier: workspace:*
|
||||
version: link:packages/iframe-bridge
|
||||
'@xyflow/react':
|
||||
specifier: ^12.8.6
|
||||
version: 12.8.6(@types/react@19.2.2)(immer@10.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -56,6 +59,9 @@ importers:
|
||||
loglevel:
|
||||
specifier: ^1.9.2
|
||||
version: 1.9.2
|
||||
penpal:
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.2
|
||||
react:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1
|
||||
@@ -160,6 +166,10 @@ importers:
|
||||
penpal:
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.2
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ~5.9.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/shared-auth: {}
|
||||
|
||||
@@ -291,6 +301,9 @@ importers:
|
||||
'@tanstack/react-table':
|
||||
specifier: ^8.20.5
|
||||
version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@teres/iframe-bridge':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/iframe-bridge
|
||||
'@uiw/react-markdown-preview':
|
||||
specifier: ^5.1.3
|
||||
version: 5.1.5(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -375,6 +388,9 @@ importers:
|
||||
openai-speech-stream-player:
|
||||
specifier: ^1.0.8
|
||||
version: 1.0.8
|
||||
penpal:
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.2
|
||||
pptx-preview:
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.7
|
||||
|
||||
3
ragflow_web/.env.production
Normal file
3
ragflow_web/.env.production
Normal file
@@ -0,0 +1,3 @@
|
||||
PORT=9222
|
||||
RAGFLOW_BASE=/ragflow/
|
||||
UMI_APP_API_BASE_URL=http://150.158.121.95
|
||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
||||
outputPath: 'dist',
|
||||
alias: { '@parent': path.resolve(__dirname, '../') },
|
||||
npmClient: 'pnpm',
|
||||
mfsu: false,
|
||||
base: RAGFLOW_BASE,
|
||||
routes,
|
||||
publicPath: RAGFLOW_BASE,
|
||||
|
||||
37090
ragflow_web/package-lock.json
generated
37090
ragflow_web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,8 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@teres/iframe-bridge": "workspace:*",
|
||||
"penpal": "^6.2.1",
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@ant-design/pro-components": "^2.6.46",
|
||||
"@ant-design/pro-layout": "^7.17.16",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AgentCategory, AgentQuery } from '@/constants/agent';
|
||||
import { NavigateToDataflowResultProps } from '@/pages/dataflow-result/interface';
|
||||
import { Routes } from '@/routes';
|
||||
import { useCallback } from 'react';
|
||||
import Bridge from '@teres/iframe-bridge';
|
||||
import { useNavigate, useParams, useSearchParams } from 'umi';
|
||||
|
||||
export enum QueryStringMap {
|
||||
@@ -14,106 +15,131 @@ export const useNavigatePage = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const { id } = useParams();
|
||||
|
||||
// 统一由 @teres/iframe-bridge 提供嵌入判断与消息封装
|
||||
|
||||
// 统一的跳转封装:嵌入模式下向父页面发送消息,否则使用内部 navigate
|
||||
const navigateOrPost = useCallback(
|
||||
(to: string, options?: { hostAgents?: boolean }) => {
|
||||
console.log('Bridge: ', Bridge);
|
||||
console.log('Bridge.isEmbedded', Bridge.isEmbedded());
|
||||
if (Bridge.isEmbedded()) {
|
||||
if (options?.hostAgents) {
|
||||
// 返回到宿主应用的 /agents
|
||||
if (typeof Bridge.clientClose === 'function') {
|
||||
Bridge.clientClose();
|
||||
} else {
|
||||
Bridge.clientNavigate(Bridge.toHostPath(Routes.Agents));
|
||||
}
|
||||
return;
|
||||
}
|
||||
Bridge.clientNavigate(Bridge.toHostPath(to));
|
||||
return;
|
||||
}
|
||||
navigate(to);
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const navigateToDatasetList = useCallback(() => {
|
||||
navigate(Routes.Datasets);
|
||||
}, [navigate]);
|
||||
navigateOrPost(Routes.Datasets);
|
||||
}, [navigateOrPost]);
|
||||
|
||||
const navigateToDataset = useCallback(
|
||||
(id: string) => () => {
|
||||
// navigate(`${Routes.DatasetBase}${Routes.DataSetOverview}/${id}`);
|
||||
navigate(`${Routes.Dataset}/${id}`);
|
||||
navigateOrPost(`${Routes.Dataset}/${id}`);
|
||||
},
|
||||
[navigate],
|
||||
[navigateOrPost],
|
||||
);
|
||||
const navigateToDatasetOverview = useCallback(
|
||||
(id: string) => () => {
|
||||
navigate(`${Routes.DatasetBase}${Routes.DataSetOverview}/${id}`);
|
||||
navigateOrPost(`${Routes.DatasetBase}${Routes.DataSetOverview}/${id}`);
|
||||
},
|
||||
[navigate],
|
||||
[navigateOrPost],
|
||||
);
|
||||
|
||||
const navigateToDataFile = useCallback(
|
||||
(id: string) => () => {
|
||||
navigate(`${Routes.DatasetBase}${Routes.DatasetBase}/${id}`);
|
||||
navigateOrPost(`${Routes.DatasetBase}${Routes.DatasetBase}/${id}`);
|
||||
},
|
||||
[navigate],
|
||||
[navigateOrPost],
|
||||
);
|
||||
|
||||
const navigateToHome = useCallback(() => {
|
||||
navigate(Routes.Root);
|
||||
}, [navigate]);
|
||||
navigateOrPost(Routes.Root);
|
||||
}, [navigateOrPost]);
|
||||
|
||||
const navigateToProfile = useCallback(() => {
|
||||
navigate(Routes.ProfileSetting);
|
||||
}, [navigate]);
|
||||
navigateOrPost(Routes.ProfileSetting);
|
||||
}, [navigateOrPost]);
|
||||
|
||||
const navigateToOldProfile = useCallback(() => {
|
||||
navigate(Routes.UserSetting);
|
||||
}, [navigate]);
|
||||
navigateOrPost(Routes.UserSetting);
|
||||
}, [navigateOrPost]);
|
||||
|
||||
const navigateToChatList = useCallback(() => {
|
||||
navigate(Routes.Chats);
|
||||
}, [navigate]);
|
||||
navigateOrPost(Routes.Chats);
|
||||
}, [navigateOrPost]);
|
||||
|
||||
const navigateToChat = useCallback(
|
||||
(id: string) => () => {
|
||||
navigate(`${Routes.Chat}/${id}`);
|
||||
navigateOrPost(`${Routes.Chat}/${id}`);
|
||||
},
|
||||
[navigate],
|
||||
[navigateOrPost],
|
||||
);
|
||||
|
||||
const navigateToAgents = useCallback(() => {
|
||||
navigate(Routes.Agents);
|
||||
}, [navigate]);
|
||||
// 嵌入模式下返回宿主应用的 /agents;独立模式跳转 ragflow 内部 /agents
|
||||
navigateOrPost(Routes.Agents, { hostAgents: true });
|
||||
}, [navigateOrPost]);
|
||||
|
||||
const navigateToAgentList = useCallback(() => {
|
||||
navigate(Routes.AgentList);
|
||||
}, [navigate]);
|
||||
navigateOrPost(Routes.AgentList);
|
||||
}, [navigateOrPost]);
|
||||
|
||||
const navigateToAgent = useCallback(
|
||||
(id: string, category?: AgentCategory) => () => {
|
||||
navigate(`${Routes.Agent}/${id}?${AgentQuery.Category}=${category}`);
|
||||
navigateOrPost(`${Routes.Agent}/${id}?${AgentQuery.Category}=${category}`);
|
||||
},
|
||||
[navigate],
|
||||
[navigateOrPost],
|
||||
);
|
||||
|
||||
const navigateToDataflow = useCallback(
|
||||
(id: string) => () => {
|
||||
navigate(`${Routes.DataFlow}/${id}`);
|
||||
navigateOrPost(`${Routes.DataFlow}/${id}`);
|
||||
},
|
||||
[navigate],
|
||||
[navigateOrPost],
|
||||
);
|
||||
|
||||
const navigateToAgentLogs = useCallback(
|
||||
(id: string) => () => {
|
||||
navigate(`${Routes.AgentLogPage}/${id}`);
|
||||
navigateOrPost(`${Routes.AgentLogPage}/${id}`);
|
||||
},
|
||||
[navigate],
|
||||
[navigateOrPost],
|
||||
);
|
||||
|
||||
const navigateToAgentTemplates = useCallback(() => {
|
||||
navigate(Routes.AgentTemplates);
|
||||
}, [navigate]);
|
||||
navigateOrPost(Routes.AgentTemplates);
|
||||
}, [navigateOrPost]);
|
||||
|
||||
const navigateToSearchList = useCallback(() => {
|
||||
navigate(Routes.Searches);
|
||||
}, [navigate]);
|
||||
navigateOrPost(Routes.Searches);
|
||||
}, [navigateOrPost]);
|
||||
|
||||
const navigateToSearch = useCallback(
|
||||
(id: string) => () => {
|
||||
navigate(`${Routes.Search}/${id}`);
|
||||
navigateOrPost(`${Routes.Search}/${id}`);
|
||||
},
|
||||
[navigate],
|
||||
[navigateOrPost],
|
||||
);
|
||||
|
||||
const navigateToChunkParsedResult = useCallback(
|
||||
(id: string, knowledgeId?: string) => () => {
|
||||
navigate(
|
||||
navigateOrPost(
|
||||
`${Routes.ParsedResult}/chunks?id=${knowledgeId}&doc_id=${id}`,
|
||||
// `${Routes.DataflowResult}?id=${knowledgeId}&doc_id=${id}&type=chunk`,
|
||||
);
|
||||
},
|
||||
[navigate],
|
||||
[navigateOrPost],
|
||||
);
|
||||
|
||||
const getQueryString = useCallback(
|
||||
@@ -134,34 +160,36 @@ export const useNavigatePage = () => {
|
||||
|
||||
const navigateToChunk = useCallback(
|
||||
(route: Routes) => {
|
||||
navigate(
|
||||
navigateOrPost(
|
||||
`${route}/${id}?${QueryStringMap.KnowledgeId}=${getQueryString(QueryStringMap.KnowledgeId)}`,
|
||||
);
|
||||
},
|
||||
[getQueryString, id, navigate],
|
||||
[getQueryString, id, navigateOrPost],
|
||||
);
|
||||
|
||||
const navigateToFiles = useCallback(
|
||||
(folderId?: string) => {
|
||||
navigate(`${Routes.Files}?folderId=${folderId}`);
|
||||
navigateOrPost(`${Routes.Files}?folderId=${folderId}`);
|
||||
},
|
||||
[navigate],
|
||||
[navigateOrPost],
|
||||
);
|
||||
|
||||
const navigateToDataflowResult = useCallback(
|
||||
(props: NavigateToDataflowResultProps) => () => {
|
||||
let params: string[] = [];
|
||||
Object.keys(props).forEach((key) => {
|
||||
// @ts-ignore
|
||||
if (props[key]) {
|
||||
// @ts-ignore
|
||||
params.push(`${key}=${props[key]}`);
|
||||
}
|
||||
});
|
||||
navigate(
|
||||
navigateOrPost(
|
||||
// `${Routes.ParsedResult}/${id}?${QueryStringMap.KnowledgeId}=${knowledgeId}`,
|
||||
`${Routes.DataflowResult}?${params.join('&')}`,
|
||||
);
|
||||
},
|
||||
[navigate],
|
||||
[navigateOrPost],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -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({
|
||||
|
||||
40
src/pages/ragflow/agent.tsx
Normal file
40
src/pages/ragflow/agent.tsx
Normal 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' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
37
src/pages/ragflow/iframe.tsx
Normal file
37
src/pages/ragflow/iframe.tsx
Normal 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' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
9
src/pages/ragflow/layout.tsx
Normal file
9
src/pages/ragflow/layout.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export default function RagflowLayout() {
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100vh' }}>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "./packages/iframe-bridge/tsconfig.json" }
|
||||
],
|
||||
// exclude rag_web_core/**/*
|
||||
"exclude": [
|
||||
|
||||
Reference in New Issue
Block a user