@siact/ai-chatbot-bridge
v0.0.10
Published
用于在父容器页面与嵌入的 iframe 之间建立双向指令通信的轻量桥接库。基于 [Comlink](https://github.com/GoogleChromeLabs/comlink) 的 RPC 机制,通过 `HostBridge` 和 `ClientBridge`,宿主应用可以暴露受控的命令,iframe 客户端可发现并调用这些命令,实现对父容器的安全控制。
Readme
AI Chatbot Bridge
用于在父容器页面与嵌入的 iframe 之间建立双向指令通信的轻量桥接库。基于 Comlink 的 RPC 机制,通过 HostBridge 和 ClientBridge,宿主应用可以暴露受控的命令,iframe 客户端可发现并调用这些命令,实现对父容器的安全控制。
安装
pnpm add @siact/ai-chatbot-bridge
# 或
npm install @siact/ai-chatbot-bridge基本原理
┌─────────────────────────────┐
│ 宿主页面 (Host) │
│ ┌───────────────────────┐ │
│ │ HostBridge │ │
│ │ - expose() │ │
│ │ - CommandProvider │ │
│ └───────────────────────┘ │
└────────────┬────────────────┘
│ postMessage
│ (Comlink RPC)
│
┌────────────┴────────────────┐
│ iframe 客户端 (Client) │
│ ┌───────────────────────┐ │
│ │ ClientBridge │ │
│ │ - wrap() │ │
│ │ - discover() │ │
│ │ - execute() │ │
│ └───────────────────────┘ │
└─────────────────────────────┘快速开始
宿主页面(父容器)
import { HostBridge } from "@siact/ai-chatbot-bridge";
const hostBridge = new HostBridge({
iframe: document.querySelector("#app-iframe"),
allowedOrigins: ["https://your-client-origin"],
debug: true,
});
hostBridge.register({
project: "host-demo",
description: "父容器控制指令",
version: "1.0.0",
commands: [
{
name: "open-modal",
description: "在宿主中打开一个弹窗",
handler: (title: string) => {
console.log(`打开弹窗: ${title}`);
return { success: true };
},
},
{
name: "navigate",
description: "切换宿主路由",
handler: async (path: string) => {
console.log(`导航到: ${path}`);
return { success: true, path };
},
},
{
name: "get-user-info",
description: "获取当前用户信息",
handler: async () => {
return {
id: "user-123",
name: "张三",
role: "admin",
};
},
},
],
});
// 页面卸载时清理资源
window.addEventListener("beforeunload", () => hostBridge.destroy());iframe 客户端
import { ClientBridge } from "@siact/ai-chatbot-bridge";
const clientBridge = new ClientBridge({
targetOrigin: "https://your-host-origin",
debug: true,
});
async function init() {
// 发现宿主提供的命令
const provider = await clientBridge.discover();
console.log("可用命令:", provider.commands);
// 执行宿主命令
const userInfo = await clientBridge.execute("get-user-info");
console.log("用户信息:", userInfo);
await clientBridge.execute("open-modal", ["来自 iframe 的弹窗"]);
await clientBridge.execute("navigate", ["/dashboard"]);
}
init();
window.addEventListener("beforeunload", () => clientBridge.destroy());让客户端向宿主暴露命令
从 vX.Y.Z 起,客户端也可以通过 clientBridge.register() 注册自身命令,供宿主发现与调用:
import { ClientBridge } from "@siact/ai-chatbot-bridge";
const clientBridge = new ClientBridge({
targetOrigin: "https://your-host-origin",
debug: true,
});
clientBridge.register({
project: "client-demo",
description: "iframe 暴露给宿主的能力",
version: "1.0.0",
commands: [
{
name: "get-client-state",
description: "返回 iframe 内部的会话状态",
handler: () => {
return { theme: "dark", unreadCount: 2 };
},
},
{
name: "focus-input",
description: "聚焦聊天输入框",
handler: () => {
const input = document.querySelector<HTMLInputElement>("#chat-input");
input?.focus();
return { focused: !!input };
},
},
],
});宿主侧可调用新增的 discoverClient() 和 executeOnClient() 方法:
import { HostBridge } from "@siact/ai-chatbot-bridge";
const hostBridge = new HostBridge({
iframe: document.querySelector("#app-iframe"),
allowedOrigins: ["https://your-client-origin"],
debug: true,
});
async function useClientCommands() {
const clientProvider = await hostBridge.discoverClient();
console.log("客户端暴露的命令:", clientProvider.commands);
const clientState = await hostBridge.executeOnClient("get-client-state");
console.log("客户端状态:", clientState);
await hostBridge.executeOnClient("focus-input");
}
useClientCommands();配置说明
HostBridge 选项
iframe(可选): 指定目标 iframe 元素。如果不指定,Comlink 会自动使用window.parent。allowedOrigins(可选): 允许接收消息的来源,默认['*']。推荐在生产环境中填写具体域名以提高安全性。debug(可选): 启用调试模式,在控制台输出通信日志。
ClientBridge 选项
targetWindow(可选): 目标窗口对象,默认window.parent。targetOrigin(可选): 宿主页面的 origin,默认'*'。推荐在生产环境中指定具体 origin。debug(可选): 启用调试模式。
高级用法
错误处理
async function executeWithErrorHandling() {
try {
const result = await clientBridge.execute("non-existent-command");
} catch (error) {
if (error instanceof Error) {
console.error("错误信息:", error.message);
// 输出: 错误信息: Command "non-existent-command" not found
}
}
}缓存 discover 结果
ClientBridge 会自动缓存第一次 discover() 的结果,后续调用会直接返回缓存:
const provider1 = await clientBridge.discover(); // 发送请求到宿主
const provider2 = await clientBridge.discover(); // 返回缓存结果支持复杂数据类型
Comlink 支持传递和返回复杂的数据类型(对象、数组、Promise 等):
// 宿主侧
{
name: "get-data",
handler: async () => {
return {
users: [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
],
metadata: {
total: 2,
timestamp: new Date(),
},
};
},
}
// 客户端侧
const data = await clientBridge.execute("get-data");
console.log(data.users); // [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]完整示例
宿主页面 (host.html)
<!DOCTYPE html>
<html>
<head>
<title>宿主页面</title>
</head>
<body>
<h1>宿主页面</h1>
<iframe id="app-iframe" src="http://localhost:3000/client.html"></iframe>
<script type="module">
import { HostBridge } from "@siact/ai-chatbot-bridge";
const hostBridge = new HostBridge({
iframe: document.querySelector("#app-iframe"),
debug: true,
});
hostBridge.register({
project: "host-app",
version: "1.0.0",
commands: [
{
name: "alert",
handler: (message) => {
alert(message);
return { shown: true };
},
},
],
});
window.addEventListener("beforeunload", () => hostBridge.destroy());
</script>
</body>
</html>客户端页面 (client.html)
<!DOCTYPE html>
<html>
<head>
<title>客户端页面</title>
</head>
<body>
<h1>客户端页面</h1>
<button id="btn">点击我</button>
<script type="module">
import { ClientBridge } from "@siact/ai-chatbot-bridge";
const clientBridge = new ClientBridge({
debug: true,
});
document.getElementById("btn").addEventListener("click", async () => {
const result = await clientBridge.execute("alert", [
"来自 iframe 的问候!",
]);
console.log(result);
});
window.addEventListener("beforeunload", () => clientBridge.destroy());
</script>
</body>
</html>类型定义
CommandHandler
type CommandHandler = (...args: any[]) => any | Promise<any>;CommandDefinition
interface CommandDefinition {
name: string;
description?: string;
handler: CommandHandler;
}DiscoveredProvider
interface DiscoveredProvider {
project: string;
description?: string;
version?: string;
commands: DiscoveredCommand[];
}最佳实践
安全性
- 在生产环境中,始终在
allowedOrigins中指定具体的域名 - 在
ClientBridge中指定targetOrigin而不是使用'*' - 只暴露必要的命令,避免暴露敏感操作
性能
- 利用
discover()的自动缓存机制 - 避免频繁创建和销毁 Bridge 实例
- 使用
timeoutMs设置合理的超时时间
调试
- 在开发环境启用
debug: true以便追踪通信过程 - 在生产环境禁用 debug 模式以减少日志输出
资源清理
- 页面卸载时调用
destroy()方法,避免内存泄漏 - 清理事件监听器和定时器
常见问题
Q: 为什么使用 Comlink? A: Comlink 提供了一个简洁的 RPC 接口,自动处理 postMessage 通信的复杂性,使代码更易读易维护。
Q: 支持哪些数据类型? A: Comlink 支持大多数可序列化的 JavaScript 类型,包括对象、数组、Date、Map、Set 等。不支持函数和循环引用。
Q: 如何处理长时间运行的命令?
A: 可以增加 timeoutMs 的值,或者在命令中使用 WebSocket 等实时通信方式来推送进度更新。
Q: 能否在多个 iframe 之间通信?
A: 可以。每个 iframe 创建独立的 ClientBridge 实例,连接到同一个宿主。
构建与发布
pnpm install
pnpm buildpnpm build 将生成 dist/ 产物和类型声明,方便发布到私有或公共 npm 镜像。
