npm package: @siact/ai-chatbot-bridge
Description: 用于在父容器页面与嵌入的 iframe 之间建立双向指令通信的轻量桥接库。基于 [Comlink](https://github.com/GoogleChromeLabs/comlink) 的 RPC 机制，通过 `HostBridge` 和 `ClientBridge`，宿主应用可以暴露受控的命令，iframe 客户端可发现并调用这些命令，实现对父容器的安全控制。
Latest version: 0.0.10

# AI Chatbot Bridge

用于在父容器页面与嵌入的 iframe 之间建立双向指令通信的轻量桥接库。基于 [Comlink](https://github.com/GoogleChromeLabs/comlink) 的 RPC 机制，通过 `HostBridge` 和 `ClientBridge`，宿主应用可以暴露受控的命令，iframe 客户端可发现并调用这些命令，实现对父容器的安全控制。

## 安装

```bash
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()          │  │
│  └───────────────────────┘  │
└─────────────────────────────┘
```

## 快速开始

### 宿主页面（父容器）

```typescript
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 客户端

```typescript
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()` 注册自身命令，供宿主发现与调用：

```typescript
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()` 方法：

```typescript
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`** (可选): 启用调试模式。

## 高级用法

### 错误处理

```typescript
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()` 的结果，后续调用会直接返回缓存：

```typescript
const provider1 = await clientBridge.discover(); // 发送请求到宿主
const provider2 = await clientBridge.discover(); // 返回缓存结果
```

### 支持复杂数据类型

Comlink 支持传递和返回复杂的数据类型（对象、数组、Promise 等）：

```typescript
// 宿主侧
{
  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)

```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)

```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

```typescript
type CommandHandler = (...args: any[]) => any | Promise<any>;
```

### CommandDefinition

```typescript
interface CommandDefinition {
  name: string;
  description?: string;
  handler: CommandHandler;
}
```

### DiscoveredProvider

```typescript
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` 实例，连接到同一个宿主。

## 构建与发布

```bash
pnpm install
pnpm build
```

`pnpm build` 将生成 `dist/` 产物和类型声明，方便发布到私有或公共 npm 镜像。