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 路由
|
# ragflow_web(Umi)部署在子路径,支持 SPA 路由
|
||||||
location ${RAGFLOW_BASE} {
|
location ${RAGFLOW_BASE} {
|
||||||
alias /usr/share/nginx/html/ragflow/;
|
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",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"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",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@teres/iframe-bridge": "workspace:*",
|
||||||
|
"penpal": "^6.2.1",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@mui/icons-material": "^7.3.4",
|
"@mui/icons-material": "^7.3.4",
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -b"
|
||||||
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@@ -29,5 +32,8 @@
|
|||||||
"penpal": {
|
"penpal": {
|
||||||
"optional": true
|
"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':
|
'@mui/x-date-pickers':
|
||||||
specifier: ^8.14.0
|
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)
|
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':
|
'@xyflow/react':
|
||||||
specifier: ^12.8.6
|
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)
|
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:
|
loglevel:
|
||||||
specifier: ^1.9.2
|
specifier: ^1.9.2
|
||||||
version: 1.9.2
|
version: 1.9.2
|
||||||
|
penpal:
|
||||||
|
specifier: ^6.2.1
|
||||||
|
version: 6.2.2
|
||||||
react:
|
react:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
@@ -160,6 +166,10 @@ importers:
|
|||||||
penpal:
|
penpal:
|
||||||
specifier: ^6.2.1
|
specifier: ^6.2.1
|
||||||
version: 6.2.2
|
version: 6.2.2
|
||||||
|
devDependencies:
|
||||||
|
typescript:
|
||||||
|
specifier: ~5.9.3
|
||||||
|
version: 5.9.3
|
||||||
|
|
||||||
packages/shared-auth: {}
|
packages/shared-auth: {}
|
||||||
|
|
||||||
@@ -291,6 +301,9 @@ importers:
|
|||||||
'@tanstack/react-table':
|
'@tanstack/react-table':
|
||||||
specifier: ^8.20.5
|
specifier: ^8.20.5
|
||||||
version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
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':
|
'@uiw/react-markdown-preview':
|
||||||
specifier: ^5.1.3
|
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)
|
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:
|
openai-speech-stream-player:
|
||||||
specifier: ^1.0.8
|
specifier: ^1.0.8
|
||||||
version: 1.0.8
|
version: 1.0.8
|
||||||
|
penpal:
|
||||||
|
specifier: ^6.2.1
|
||||||
|
version: 6.2.2
|
||||||
pptx-preview:
|
pptx-preview:
|
||||||
specifier: ^1.0.5
|
specifier: ^1.0.5
|
||||||
version: 1.0.7
|
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',
|
outputPath: 'dist',
|
||||||
alias: { '@parent': path.resolve(__dirname, '../') },
|
alias: { '@parent': path.resolve(__dirname, '../') },
|
||||||
npmClient: 'pnpm',
|
npmClient: 'pnpm',
|
||||||
|
mfsu: false,
|
||||||
base: RAGFLOW_BASE,
|
base: RAGFLOW_BASE,
|
||||||
routes,
|
routes,
|
||||||
publicPath: RAGFLOW_BASE,
|
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": {
|
"dependencies": {
|
||||||
|
"@teres/iframe-bridge": "workspace:*",
|
||||||
|
"penpal": "^6.2.1",
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@ant-design/pro-components": "^2.6.46",
|
"@ant-design/pro-components": "^2.6.46",
|
||||||
"@ant-design/pro-layout": "^7.17.16",
|
"@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 { NavigateToDataflowResultProps } from '@/pages/dataflow-result/interface';
|
||||||
import { Routes } from '@/routes';
|
import { Routes } from '@/routes';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import Bridge from '@teres/iframe-bridge';
|
||||||
import { useNavigate, useParams, useSearchParams } from 'umi';
|
import { useNavigate, useParams, useSearchParams } from 'umi';
|
||||||
|
|
||||||
export enum QueryStringMap {
|
export enum QueryStringMap {
|
||||||
@@ -14,106 +15,131 @@ export const useNavigatePage = () => {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { id } = useParams();
|
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(() => {
|
const navigateToDatasetList = useCallback(() => {
|
||||||
navigate(Routes.Datasets);
|
navigateOrPost(Routes.Datasets);
|
||||||
}, [navigate]);
|
}, [navigateOrPost]);
|
||||||
|
|
||||||
const navigateToDataset = useCallback(
|
const navigateToDataset = useCallback(
|
||||||
(id: string) => () => {
|
(id: string) => () => {
|
||||||
// navigate(`${Routes.DatasetBase}${Routes.DataSetOverview}/${id}`);
|
// navigate(`${Routes.DatasetBase}${Routes.DataSetOverview}/${id}`);
|
||||||
navigate(`${Routes.Dataset}/${id}`);
|
navigateOrPost(`${Routes.Dataset}/${id}`);
|
||||||
},
|
},
|
||||||
[navigate],
|
[navigateOrPost],
|
||||||
);
|
);
|
||||||
const navigateToDatasetOverview = useCallback(
|
const navigateToDatasetOverview = useCallback(
|
||||||
(id: string) => () => {
|
(id: string) => () => {
|
||||||
navigate(`${Routes.DatasetBase}${Routes.DataSetOverview}/${id}`);
|
navigateOrPost(`${Routes.DatasetBase}${Routes.DataSetOverview}/${id}`);
|
||||||
},
|
},
|
||||||
[navigate],
|
[navigateOrPost],
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigateToDataFile = useCallback(
|
const navigateToDataFile = useCallback(
|
||||||
(id: string) => () => {
|
(id: string) => () => {
|
||||||
navigate(`${Routes.DatasetBase}${Routes.DatasetBase}/${id}`);
|
navigateOrPost(`${Routes.DatasetBase}${Routes.DatasetBase}/${id}`);
|
||||||
},
|
},
|
||||||
[navigate],
|
[navigateOrPost],
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigateToHome = useCallback(() => {
|
const navigateToHome = useCallback(() => {
|
||||||
navigate(Routes.Root);
|
navigateOrPost(Routes.Root);
|
||||||
}, [navigate]);
|
}, [navigateOrPost]);
|
||||||
|
|
||||||
const navigateToProfile = useCallback(() => {
|
const navigateToProfile = useCallback(() => {
|
||||||
navigate(Routes.ProfileSetting);
|
navigateOrPost(Routes.ProfileSetting);
|
||||||
}, [navigate]);
|
}, [navigateOrPost]);
|
||||||
|
|
||||||
const navigateToOldProfile = useCallback(() => {
|
const navigateToOldProfile = useCallback(() => {
|
||||||
navigate(Routes.UserSetting);
|
navigateOrPost(Routes.UserSetting);
|
||||||
}, [navigate]);
|
}, [navigateOrPost]);
|
||||||
|
|
||||||
const navigateToChatList = useCallback(() => {
|
const navigateToChatList = useCallback(() => {
|
||||||
navigate(Routes.Chats);
|
navigateOrPost(Routes.Chats);
|
||||||
}, [navigate]);
|
}, [navigateOrPost]);
|
||||||
|
|
||||||
const navigateToChat = useCallback(
|
const navigateToChat = useCallback(
|
||||||
(id: string) => () => {
|
(id: string) => () => {
|
||||||
navigate(`${Routes.Chat}/${id}`);
|
navigateOrPost(`${Routes.Chat}/${id}`);
|
||||||
},
|
},
|
||||||
[navigate],
|
[navigateOrPost],
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigateToAgents = useCallback(() => {
|
const navigateToAgents = useCallback(() => {
|
||||||
navigate(Routes.Agents);
|
// 嵌入模式下返回宿主应用的 /agents;独立模式跳转 ragflow 内部 /agents
|
||||||
}, [navigate]);
|
navigateOrPost(Routes.Agents, { hostAgents: true });
|
||||||
|
}, [navigateOrPost]);
|
||||||
|
|
||||||
const navigateToAgentList = useCallback(() => {
|
const navigateToAgentList = useCallback(() => {
|
||||||
navigate(Routes.AgentList);
|
navigateOrPost(Routes.AgentList);
|
||||||
}, [navigate]);
|
}, [navigateOrPost]);
|
||||||
|
|
||||||
const navigateToAgent = useCallback(
|
const navigateToAgent = useCallback(
|
||||||
(id: string, category?: AgentCategory) => () => {
|
(id: string, category?: AgentCategory) => () => {
|
||||||
navigate(`${Routes.Agent}/${id}?${AgentQuery.Category}=${category}`);
|
navigateOrPost(`${Routes.Agent}/${id}?${AgentQuery.Category}=${category}`);
|
||||||
},
|
},
|
||||||
[navigate],
|
[navigateOrPost],
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigateToDataflow = useCallback(
|
const navigateToDataflow = useCallback(
|
||||||
(id: string) => () => {
|
(id: string) => () => {
|
||||||
navigate(`${Routes.DataFlow}/${id}`);
|
navigateOrPost(`${Routes.DataFlow}/${id}`);
|
||||||
},
|
},
|
||||||
[navigate],
|
[navigateOrPost],
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigateToAgentLogs = useCallback(
|
const navigateToAgentLogs = useCallback(
|
||||||
(id: string) => () => {
|
(id: string) => () => {
|
||||||
navigate(`${Routes.AgentLogPage}/${id}`);
|
navigateOrPost(`${Routes.AgentLogPage}/${id}`);
|
||||||
},
|
},
|
||||||
[navigate],
|
[navigateOrPost],
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigateToAgentTemplates = useCallback(() => {
|
const navigateToAgentTemplates = useCallback(() => {
|
||||||
navigate(Routes.AgentTemplates);
|
navigateOrPost(Routes.AgentTemplates);
|
||||||
}, [navigate]);
|
}, [navigateOrPost]);
|
||||||
|
|
||||||
const navigateToSearchList = useCallback(() => {
|
const navigateToSearchList = useCallback(() => {
|
||||||
navigate(Routes.Searches);
|
navigateOrPost(Routes.Searches);
|
||||||
}, [navigate]);
|
}, [navigateOrPost]);
|
||||||
|
|
||||||
const navigateToSearch = useCallback(
|
const navigateToSearch = useCallback(
|
||||||
(id: string) => () => {
|
(id: string) => () => {
|
||||||
navigate(`${Routes.Search}/${id}`);
|
navigateOrPost(`${Routes.Search}/${id}`);
|
||||||
},
|
},
|
||||||
[navigate],
|
[navigateOrPost],
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigateToChunkParsedResult = useCallback(
|
const navigateToChunkParsedResult = useCallback(
|
||||||
(id: string, knowledgeId?: string) => () => {
|
(id: string, knowledgeId?: string) => () => {
|
||||||
navigate(
|
navigateOrPost(
|
||||||
`${Routes.ParsedResult}/chunks?id=${knowledgeId}&doc_id=${id}`,
|
`${Routes.ParsedResult}/chunks?id=${knowledgeId}&doc_id=${id}`,
|
||||||
// `${Routes.DataflowResult}?id=${knowledgeId}&doc_id=${id}&type=chunk`,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[navigate],
|
[navigateOrPost],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getQueryString = useCallback(
|
const getQueryString = useCallback(
|
||||||
@@ -134,34 +160,36 @@ export const useNavigatePage = () => {
|
|||||||
|
|
||||||
const navigateToChunk = useCallback(
|
const navigateToChunk = useCallback(
|
||||||
(route: Routes) => {
|
(route: Routes) => {
|
||||||
navigate(
|
navigateOrPost(
|
||||||
`${route}/${id}?${QueryStringMap.KnowledgeId}=${getQueryString(QueryStringMap.KnowledgeId)}`,
|
`${route}/${id}?${QueryStringMap.KnowledgeId}=${getQueryString(QueryStringMap.KnowledgeId)}`,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[getQueryString, id, navigate],
|
[getQueryString, id, navigateOrPost],
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigateToFiles = useCallback(
|
const navigateToFiles = useCallback(
|
||||||
(folderId?: string) => {
|
(folderId?: string) => {
|
||||||
navigate(`${Routes.Files}?folderId=${folderId}`);
|
navigateOrPost(`${Routes.Files}?folderId=${folderId}`);
|
||||||
},
|
},
|
||||||
[navigate],
|
[navigateOrPost],
|
||||||
);
|
);
|
||||||
|
|
||||||
const navigateToDataflowResult = useCallback(
|
const navigateToDataflowResult = useCallback(
|
||||||
(props: NavigateToDataflowResultProps) => () => {
|
(props: NavigateToDataflowResultProps) => () => {
|
||||||
let params: string[] = [];
|
let params: string[] = [];
|
||||||
Object.keys(props).forEach((key) => {
|
Object.keys(props).forEach((key) => {
|
||||||
|
// @ts-ignore
|
||||||
if (props[key]) {
|
if (props[key]) {
|
||||||
|
// @ts-ignore
|
||||||
params.push(`${key}=${props[key]}`);
|
params.push(`${key}=${props[key]}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
navigate(
|
navigateOrPost(
|
||||||
// `${Routes.ParsedResult}/${id}?${QueryStringMap.KnowledgeId}=${knowledgeId}`,
|
// `${Routes.ParsedResult}/${id}?${QueryStringMap.KnowledgeId}=${knowledgeId}`,
|
||||||
`${Routes.DataflowResult}?${params.join('&')}`,
|
`${Routes.DataflowResult}?${params.join('&')}`,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[navigate],
|
[navigateOrPost],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ function AgentListPage() {
|
|||||||
onCreateAgent={() => setCreateOpen(true)}
|
onCreateAgent={() => setCreateOpen(true)}
|
||||||
onEdit={(agent) => { setEditTarget(agent); setEditOpen(true); }}
|
onEdit={(agent) => { setEditTarget(agent); setEditOpen(true); }}
|
||||||
onView={(agent) => {
|
onView={(agent) => {
|
||||||
navigate(`/agent/${agent.id}`);
|
navigate(`/route-ragflow/agent/${agent.id}`);
|
||||||
}}
|
}}
|
||||||
onDelete={async (agent) => {
|
onDelete={async (agent) => {
|
||||||
const confirmed = await dialog.confirm({
|
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 DocumentPreview from '@/pages/chunk/document-preview';
|
||||||
import AgentDetailPage from '@/pages/agent-mui/detail';
|
import AgentDetailPage from '@/pages/agent-mui/detail';
|
||||||
// import AgentDetailPage from '@/pages/agent';
|
// 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 = () => {
|
const AppRoutes = () => {
|
||||||
return (
|
return (
|
||||||
@@ -70,6 +73,13 @@ const AppRoutes = () => {
|
|||||||
<Route path="mcp" element={<MCPSetting />} />
|
<Route path="mcp" element={<MCPSetting />} />
|
||||||
</Route>
|
</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 />} />
|
<Route path="*" element={<Navigate to="/knowledge" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "./tsconfig.app.json" },
|
{ "path": "./tsconfig.app.json" },
|
||||||
{ "path": "./tsconfig.node.json" }
|
{ "path": "./tsconfig.node.json" },
|
||||||
|
{ "path": "./packages/iframe-bridge/tsconfig.json" }
|
||||||
],
|
],
|
||||||
// exclude rag_web_core/**/*
|
// exclude rag_web_core/**/*
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
|||||||
Reference in New Issue
Block a user