feat(iframe-bridge): implement iframe communication bridge and language sync
This commit is contained in:
141
packages/iframe-bridge/README.md
Normal file
141
packages/iframe-bridge/README.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# @teres/iframe-bridge
|
||||
|
||||
一个基于 [Penpal](https://github.com/Aaronius/penpal) 的轻量 iframe 通信桥,帮助宿主页面与子应用(iframe)安全、稳定地进行双向方法调用与状态同步。
|
||||
|
||||
## 为什么需要它
|
||||
- 将跨项目的交互(如语言切换、导航、关闭、就绪通知)抽象为清晰的 API。
|
||||
- 在 Monorepo 或多项目场景下,提供“宿主 ↔ 子应用”的统一通信约定。
|
||||
- 屏蔽 Penpal 细节,专注业务方法与类型定义。
|
||||
|
||||
## 安装与版本
|
||||
- 已在工作空间内作为包使用(`packages/iframe-bridge`)。
|
||||
- 依赖:`penpal@^6.2.1`。
|
||||
|
||||
## 目录结构
|
||||
```
|
||||
packages/iframe-bridge/
|
||||
├── package.json
|
||||
├── README.md
|
||||
└── src/
|
||||
├── index.ts # 入口聚合与导出(不含业务逻辑)
|
||||
├── types.ts # 对外类型:HostApi、ChildApi
|
||||
├── path.ts # 路径转换与是否嵌入判断
|
||||
├── client.ts # 子端到父端的连接与客户端方法
|
||||
├── host.ts # 宿主侧创建与管理与子端的连接
|
||||
└── logger.ts # 统一日志工具
|
||||
```
|
||||
|
||||
## 核心概念
|
||||
- Host(宿主):包含 `iframe` 的父页面。
|
||||
- Child(子端):被嵌入在 `iframe` 中的子应用。子端暴露方法给宿主调用。
|
||||
- Penpal:在宿主与子端之间建立安全的双向 RPC(方法调用)连接。
|
||||
|
||||
## 快速上手
|
||||
|
||||
### 宿主侧(Host)示例:创建连接并缓存子端引用
|
||||
```ts
|
||||
import { createPenpalHostBridge } from '@teres/iframe-bridge';
|
||||
|
||||
// iframeRef 指向你页面中的 <iframe>
|
||||
const { child, destroy } = await createPenpalHostBridge({
|
||||
iframe: iframeRef.current!,
|
||||
methods: {
|
||||
// 暴露给“子端”可调用的宿主方法
|
||||
navigate: (path: string) => {
|
||||
// 宿主导航逻辑
|
||||
},
|
||||
close: () => {
|
||||
// 关闭或返回逻辑
|
||||
},
|
||||
agentReady: (agentId?: string) => {
|
||||
// 子端就绪通知处理
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// child 是一个 Promise,解析后可拿到子端暴露的方法(例如 changeLanguage)
|
||||
await child; // 也可以缓存后续使用
|
||||
|
||||
// 组件卸载或需要重建连接时
|
||||
await destroy();
|
||||
```
|
||||
|
||||
在你的项目中(如 `src/pages/ragflow/iframe.tsx`)通常会把 `child` 引用存到一个管理器,供语言切换等场景调用。
|
||||
|
||||
### 子端侧(Child)示例:暴露方法并与宿主交互
|
||||
```ts
|
||||
import Bridge from '@teres/iframe-bridge';
|
||||
|
||||
// 在子应用初始化时调用(例如根组件 useEffect 中)
|
||||
Bridge.initChildBridge({
|
||||
// 暴露给“宿主”可调用的子端方法
|
||||
methods: {
|
||||
changeLanguage: async (lang: string) => {
|
||||
// 你的 i18n 切换逻辑,例如:
|
||||
// await i18n.changeLanguage(lang);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 主动与宿主交互(可选)
|
||||
Bridge.clientReady();
|
||||
Bridge.clientNavigate('/some/path');
|
||||
Bridge.clientClose();
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### 类型(types.ts)
|
||||
- `HostApi`
|
||||
- `navigate(path: string): void`
|
||||
- `close(): void`
|
||||
- `agentReady(agentId?: string): void`
|
||||
- `ChildApi`
|
||||
- `changeLanguage(lang: string): Promise<void>`
|
||||
|
||||
### 路径与嵌入(path.ts)
|
||||
- `isEmbedded(): boolean` 判断当前窗口是否运行在 iframe 中(且需要通信)。
|
||||
- `toHostPath(childPath: string): string` 将子端路径转换为宿主侧路由前缀路径。
|
||||
- `toChildPath(hostPath: string): string` 将宿主路径映射回子端路径。
|
||||
|
||||
### 子端客户端方法(client.ts)
|
||||
- `getClientHostApi(): Promise<HostApi>` 建立并缓存与宿主的连接。
|
||||
- `clientNavigate(path: string): Promise<void>` 请求宿主导航。
|
||||
- `clientClose(): Promise<void>` 请求宿主关闭或返回。
|
||||
- `clientReady(agentId?: string): Promise<void>` 通知宿主子端就绪。
|
||||
- `initChildBridge(options: { methods: Partial<ChildApi> }): Promise<void>` 在子端暴露方法供宿主调用(如 `changeLanguage`)。
|
||||
|
||||
### 宿主桥接(host.ts)
|
||||
- `createPenpalHostBridge({ iframe, methods })`
|
||||
- `iframe: HTMLIFrameElement`
|
||||
- `methods: Partial<HostApi>` 暴露给子端可调用的宿主方法。
|
||||
- 返回:`{ child: Promise<ChildExposed>, destroy: () => Promise<void> }`
|
||||
- `child`:解析后可获得子端暴露的方法(如 `changeLanguage`)。
|
||||
- `destroy`:销毁连接与事件监听。
|
||||
|
||||
## 语言联动最佳实践
|
||||
1. 子端在初始化中通过 `initChildBridge` 暴露 `changeLanguage`。
|
||||
2. 宿主在创建连接后缓存 `child` 引用。
|
||||
3. 宿主语言切换组件在变更时调用:
|
||||
```ts
|
||||
const child = await childPromise; // 取到缓存的子端引用
|
||||
await child.changeLanguage(nextLang);
|
||||
```
|
||||
|
||||
## 调试与排错
|
||||
- 确保 iframe 与宿主同源或允许跨源通信(Penpal 支持跨源,但需正确 URL)。
|
||||
- 连接建立需要子端加载完成;在子端 `initChildBridge` 之前调用子端方法会报错或超时。
|
||||
- 若需要观察连接过程,可在 `logger.ts` 中启用调试日志或在业务代码中打点。
|
||||
- 组件卸载时务必调用 `destroy()`,避免内存泄漏或事件残留。
|
||||
|
||||
## 设计说明
|
||||
- 入口 `index.ts` 仅做导出聚合,具体逻辑按职责拆分到 `types/logger/path/client/host`。
|
||||
- 子端侧会缓存一次宿主连接,避免重复握手,提高性能与稳定性。
|
||||
|
||||
## 常见问题
|
||||
- 调用时机过早:请在子端完成初始化(`initChildBridge`)后再由宿主调用子端方法。
|
||||
- 路径映射:`toHostPath`/`toChildPath` 默认适配当前项目前缀,如需自定义可扩展配置模块。
|
||||
- 异步方法:所有跨端调用均为异步,注意 `await` 与错误处理。
|
||||
|
||||
---
|
||||
若你需要把路径前缀改为可配置项或扩展更多方法(如主题切换、会话同步),欢迎提 issue 或继续迭代。
|
||||
98
packages/iframe-bridge/src/client.ts
Normal file
98
packages/iframe-bridge/src/client.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 子端(iframe 内)到宿主的客户端方法集合。
|
||||
* - 负责与父窗口建立并缓存 Penpal 连接
|
||||
* - 提供便捷的客户端调用:navigate/close/ready
|
||||
*/
|
||||
import { connectToParent } from 'penpal';
|
||||
import type { HostApi, ChildApi } from './types';
|
||||
import { isEmbedded } from './path';
|
||||
import { log } from './logger';
|
||||
|
||||
// Cached Penpal connections
|
||||
// 缓存与宿主的连接,以避免重复握手
|
||||
let clientHostApiPromise: Promise<HostApi> | null = null;
|
||||
|
||||
/** Create a Penpal connection from child to parent and cache the Host API. */
|
||||
/**
|
||||
* 建立从子端到父端的 Penpal 连接,并返回宿主 HostApi(带缓存)。
|
||||
* @param options.parentOrigin 可选,宿主的 origin(默认使用当前 window.location.origin)。
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求宿主进行导航。
|
||||
* @param to 目标路径(会由宿主自行处理前缀与实际路由规则)
|
||||
*/
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知宿主:子端已就绪(可携带 agentId 等信息)。
|
||||
*/
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 子应用初始化:暴露 ChildApi 给宿主,并获取宿主 HostApi(用于后续 clientNavigate、clientClose 等)。
|
||||
* 如果已调用过 getClientHostApi,会复用同一连接。
|
||||
*/
|
||||
/**
|
||||
* 子应用初始化:
|
||||
* - 暴露 ChildApi 给宿主(如 changeLanguage)
|
||||
* - 并获取宿主 HostApi(复用同一连接)
|
||||
*/
|
||||
export function initChildBridge(params: {
|
||||
methods?: ChildApi;
|
||||
parentOrigin?: string;
|
||||
}) {
|
||||
const parentOrigin = params.parentOrigin ?? window.location.origin;
|
||||
log('initChildBridge() start', { parentOrigin, hasMethods: !!params.methods });
|
||||
const connection = connectToParent<HostApi>({ parentOrigin, methods: params.methods as any });
|
||||
// 设置并复用 clientHostApiPromise
|
||||
clientHostApiPromise = connection.promise;
|
||||
connection.promise
|
||||
.then(() => log('initChildBridge() parent connected'))
|
||||
.catch((err) => log('initChildBridge() connect error', err));
|
||||
return connection.promise;
|
||||
}
|
||||
38
packages/iframe-bridge/src/host.ts
Normal file
38
packages/iframe-bridge/src/host.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 宿主(父窗口)与子端(iframe)之间的桥接创建。
|
||||
* - 绑定 iframe 元素,暴露宿主方法给子端调用
|
||||
* - 返回 child Promise(解析为子端暴露的方法)与 destroy 销毁函数
|
||||
*/
|
||||
import { connectToChild } from 'penpal';
|
||||
import type { HostApi, ChildApi } from './types';
|
||||
import { log } from './logger';
|
||||
|
||||
/** Establish Penpal host bridge bound to an iframe and expose Host API to child. */
|
||||
/**
|
||||
* 建立宿主侧 Penpal 桥接并暴露 HostApi。
|
||||
* @param params.iframe 目标 iframe 元素
|
||||
* @param params.origin 可选,子端 origin(默认当前 window.location.origin)
|
||||
* @param params.methods 宿主暴露给子端的方法集合
|
||||
* @returns `{ child, destroy }`
|
||||
* - `child`: Promise<ChildApiResolved>,解析后可直接调用子端暴露的方法(如 changeLanguage)
|
||||
* - `destroy`: 断开连接与清理监听器
|
||||
*/
|
||||
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?.();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,149 +1,31 @@
|
||||
// 入口文件:仅聚合与导出 API,不承载具体逻辑
|
||||
export type { HostApi, ChildApi } from './types';
|
||||
export { isEmbedded, toHostPath, toChildPath } from './path';
|
||||
export {
|
||||
getClientHostApi,
|
||||
clientNavigate,
|
||||
clientClose,
|
||||
clientReady,
|
||||
initChildBridge,
|
||||
} from './client';
|
||||
export { createPenpalHostBridge } from './host';
|
||||
|
||||
// 默认导出一个聚合对象:
|
||||
// - 兼容某些只识别 default 的打包方案或联邦场景
|
||||
// - 同时保留命名导出,建议优先使用命名导出
|
||||
import { isEmbedded, toHostPath, toChildPath } from './path';
|
||||
import {
|
||||
getClientHostApi,
|
||||
clientNavigate,
|
||||
clientClose,
|
||||
clientReady,
|
||||
initChildBridge,
|
||||
} from './client';
|
||||
import { createPenpalHostBridge } from './host';
|
||||
|
||||
/**
|
||||
* @teres/iframe-bridge
|
||||
* Penpal-based utilities for host ↔ iframe communication,
|
||||
* plus minimal helpers for embed detection and path conversion.
|
||||
* 默认聚合导出对象:包含全部对外 API。
|
||||
*/
|
||||
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,
|
||||
@@ -153,6 +35,7 @@ const bridge = {
|
||||
clientClose,
|
||||
clientReady,
|
||||
createPenpalHostBridge,
|
||||
initChildBridge,
|
||||
};
|
||||
|
||||
export default bridge;
|
||||
|
||||
15
packages/iframe-bridge/src/logger.ts
Normal file
15
packages/iframe-bridge/src/logger.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 统一日志前缀,便于在控制台筛选相关输出。
|
||||
*/
|
||||
export const LOG_PREFIX = '[@teres/iframe-bridge]';
|
||||
|
||||
/**
|
||||
* 简单的日志输出封装:
|
||||
* - 在某些环境下 console 可能被拦截,故使用 try/catch。
|
||||
*/
|
||||
export function log(...args: any[]) {
|
||||
try {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(LOG_PREFIX, ...args);
|
||||
} catch {}
|
||||
}
|
||||
69
packages/iframe-bridge/src/path.ts
Normal file
69
packages/iframe-bridge/src/path.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 路径与嵌入状态相关的工具函数:
|
||||
* - 判断是否处于 iframe 场景(需进行跨窗口通信)
|
||||
* - 提供宿主/子端路径的前缀转换
|
||||
*/
|
||||
import { log } from './logger';
|
||||
|
||||
/**
|
||||
* 安全获取当前窗口的 pathname,避免跨域等异常导致报错。
|
||||
*/
|
||||
function safePathname(): string {
|
||||
try {
|
||||
const p = window.location?.pathname || '';
|
||||
log('safePathname()', p);
|
||||
return p;
|
||||
} catch {
|
||||
log('safePathname() error, return empty pathname');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否运行在 iframe 中。
|
||||
* 如因跨域访问 window.top 抛错,则视为在 iframe 中(embedded)。
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否处于“嵌入模式”:
|
||||
* - 存在 '/route-ragflow' 路径标识 或
|
||||
* - 运行在 iframe 中
|
||||
*/
|
||||
export function isEmbedded(): boolean {
|
||||
const path = safePathname();
|
||||
const inIframe = isInIframe();
|
||||
const embedded = path.includes('/route-ragflow') || inIframe;
|
||||
log('isEmbedded()', { path, inIframe, embedded });
|
||||
return embedded;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将子端路由转换为宿主路由(添加宿主前缀)。
|
||||
* 默认会为路径添加 '/ragflow' 前缀。
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将宿主路由转换回子端路由(去除 '/route-ragflow' 前缀)。
|
||||
*/
|
||||
export function toChildPath(to: string): string {
|
||||
const target = to.replace(/^\/route-ragflow/, '') || '/';
|
||||
log('toChildPath()', { input: to, target });
|
||||
return target;
|
||||
}
|
||||
22
packages/iframe-bridge/src/types.ts
Normal file
22
packages/iframe-bridge/src/types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Penpal RPC API 定义(宿主与子端共享的类型)。
|
||||
*/
|
||||
export interface HostApi {
|
||||
/** 宿主导航到指定路径 */
|
||||
navigate: (to: string) => void;
|
||||
/** 宿主执行关闭或返回动作 */
|
||||
close: () => void;
|
||||
/** 接收子端就绪通知(可携带 agentId 等信息) */
|
||||
agentReady?: (payload?: any) => void;
|
||||
}
|
||||
|
||||
export interface ChildApi {
|
||||
/** 子端主动请求宿主导航(若需要) */
|
||||
navigate?: (to: string) => void;
|
||||
/** 子端主动请求宿主关闭(若需要) */
|
||||
close?: () => void;
|
||||
/** 子端向宿主发送就绪事件(若需要) */
|
||||
ready?: (payload?: any) => void;
|
||||
/** 子应用语言切换(例如 'zh' | 'en' | 'zh-TRADITIONAL') */
|
||||
changeLanguage?: (lng: string) => Promise<void> | void;
|
||||
}
|
||||
Reference in New Issue
Block a user