# OpenBird

**为 AI Agent 准备的飞书（Lark）工具箱。** 本地运行，通过浏览器 cookie 以你本人的身份接入飞书，把飞书能力打包成可直接调用的工具，提供两个显式运行模式：

1. **MCP 模式** — 通过 stdio 暴露 77 个飞书工具，供 AI Agent（Claude Desktop、Cursor 等）调用
2. **Relay 模式** — 连接飞书 WebSocket，将消息标准化后 POST 到你提供的 webhook 地址

## 快速开始

无需 clone 源码，直接通过 `npx` 运行即可。唯一的前置条件是准备好飞书 cookie：打开飞书网页版（feishu.cn），浏览器 DevTools 从任意 `*.feishu.cn` 请求的请求头中复制完整的 `Cookie` 值。

### MCP 模式

```bash
OPENBIRD_COOKIE="your_cookie_here" npx openbird mcp
```

仅启动 MCP Server（stdio），不连接 WebSocket，不转发事件。

### Relay 模式

```bash
OPENBIRD_COOKIE="your_cookie_here" \
  npx openbird relay http://localhost:3000/webhook
```

连接飞书 WebSocket 并将所有事件转发到你提供的 webhook 地址。

### 在 MCP 客户端中配置

以 Claude Desktop / Cursor 为例，在 MCP server 配置里填：

```json
{
  "mcpServers": {
    "openbird": {
      "command": "npx",
      "args": ["-y", "openbird", "mcp"],
      "env": {
        "OPENBIRD_COOKIE": "your_cookie_here"
      }
    }
  }
}
```

## 配置

| 环境变量 | 必填 | 说明 |
|---|---|---|
| `OPENBIRD_COOKIE` | 是 | 飞书浏览器 cookie 字符串 |
| `OPENBIRD_DEBUG` | 否 | 设为 `true` 开启调试日志 |
| `OPENBIRD_ENRICH` | 否 | Relay 模式下设为 `false` 可关闭事件富化 |

支持 `.env` 文件配置。

### 迁移说明

旧版本通过 `OPENBIRD_WEBHOOK_URL` 是否存在来隐式决定是否同时启动 webhook 转发。现在改为显式命令：

- `npx openbird mcp`
- `npx openbird relay <webhook-url>`

Relay 模式不再自动启动 MCP server。

## Webhook 事件

OpenBird 将飞书 WebSocket 消息标准化为稳定的事件格式：

```json
{
  "type": "im.message.receive_v1",
  "event_id": "evt_7604769001905884091",
  "timestamp": 1739347200000,
  "data": {
    "id": "7604769001905884091",
    "conversation": {
      "id": "7599271773103737795",
      "type": "group"
    },
    "sender": {
      "id": "7128839302827827201",
      "type": "user"
    },
    "content": {
      "type": "text",
      "text": "hello"
    },
    "thread_id": null
  }
}
```

`im.thread.reply_count_v1` 示例：

```json
{
  "type": "im.thread.reply_count_v1",
  "event_id": "evt_123",
  "timestamp": 1712023530000,
  "semantic": {
    "entity": "thread",
    "action": "activity_detected",
    "kind": "state_signal",
    "consumable": false,
    "requires_hydration": "fetch_thread_messages",
    "dedupe_key": "thread:thread_1:reply_count:12",
    "dedupe_strategy": "drop_exact_match",
    "routing_hints": ["thread", "activity"]
  },
  "data": {
    "chatId": "chat_1",
    "threadId": "thread_1",
    "replyCount": 12,
    "updateTime": 1712023530000
  }
}
```

当前仅 `im.thread.reply_count_v1` 会携带 `semantic` 字段。其他 webhook 事件仍保持现有结构。

### 事件类型

| 类型 | 说明 |
|---|---|
| `im.message.receive_v1` | 聊天消息（文本、富文本、图片、卡片等） |
| `im.message.reaction_v1` | 表情回应 |
| `im.message.read_count_v1` | 消息阅读数变更 |
| `im.message.preview_v1` | 消息预览 |
| `im.message.urgent_v1` | 加急通知 |
| `im.message.urgent_ack_v1` | 加急确认 |
| `im.reaction.user_v1` | 用户表情反应配置变更 |
| `im.reaction.user_mru_v1` | 用户最近使用表情反应变更 |
| `im.chat.read_state_v1` | 会话已读状态 |
| `im.chat.tabs_v1` | 会话 Tab 变更 |
| `im.chat.top_notice_v1` | 会话置顶通知 |
| `im.thread.reply_count_v1` | 话题回复数 |
| `im.thread.read_state_v1` | 话题已读状态 |
| `im.thread.updated_v1` | 话题更新 |
| `feed.cards_v1` | Feed 卡片 |
| `calendar.event.sync_v1` | 日历事件同步 |
| `system.device.status_v1` | 设备状态 |
| `system.event.unknown` | 未识别的推送事件 |

### 消息内容类型

| 类型值 | 名称 | 说明 |
|---|---|---|
| 2 | rich | 富文本 |
| 3 | file | 文件 |
| 4 | text | 纯文本 |
| 5 | image | 图片 |
| 6 | system | 系统消息 |
| 7 | audio | 音频 |
| 9 | share_group_chat | 群聊分享 |
| 10 | sticker | 表情贴纸 |
| 11 | merge_forward | 合并转发 |
| 12 | calendar | 日历事件 |
| 14 | card | 卡片（交互式） |
| 15 | media | 媒体文件 |
| 23 | share_user_card | 用户名片分享 |
| 24 | todo | 待办任务 |

### 交付语义

- 通过 HTTP POST 发送 JSON body
- `event_id` 全局唯一，可用于幂等判断
- 非 2xx 响应会指数退避重试，最多 5 次

### 接收事件

使用 [openbird-webhook-node](https://github.com/ztxtxwd/openbird-webhook-node) 接收事件：

```bash
pnpm add openbird-webhook-node
```

```js
import { createServer } from 'openbird-webhook-node'

createServer({
  port: 3000,
  onMessage(event) {
    console.log(event.data.sender.id, event.data.content.text)
  },
})
```

然后用 Relay 模式启动 OpenBird：

```bash
OPENBIRD_COOKIE="your_cookie_here" \
  npx openbird relay http://localhost:3000/webhook
```

## MCP 工具

通过 [Model Context Protocol](https://modelcontextprotocol.io/) 提供 **77 个工具**（stdio 通信），覆盖消息、会话、话题、用户、通讯录、机器人、日历、加急、百科、云文档等 14 个类别。

### 消息

| 工具 | 说明 |
|---|---|
| `send_message` | 发送消息（支持 Markdown） |
| `send_mention_message` | 发送 @提及 消息 |
| `send_reply` | 回复指定消息（引用回复） |
| `put_message_link` | 生成消息链接 |
| `get_message_link_permission` | 查询消息链接预览权限 |
| `add_reaction` | 添加表情回应 |
| `remove_reaction` | 移除表情回应 |
| `pin_message` | 在会话中置顶消息 |
| `list_chat_pins` | 列出会话中置顶的消息 |
| `unpin_message` | 取消置顶消息 |

### 定时消息

| 工具 | 说明 |
|---|---|
| `put_schedule_message` | 创建定时消息（未来时间发送） |
| `patch_schedule_message` | 更新 / 暂停 / 删除 / 立即发送定时消息 |
| `pull_schedule_message_by_ids` | 按 ID 批量查询定时消息 |

### 会话

| 工具 | 说明 |
|---|---|
| `get_chat_history` | 获取聊天历史消息 |
| `get_chat_meta` | 获取会话元信息（最后消息位置、已读位置等） |
| `create_chat` | 创建一对一会话 |
| `create_group` | 创建群聊 |
| `patch_group_chat` | 更新群聊信息（名称、描述、图标） |
| `search` | 搜索用户和群组 |
| `search_in_chat` | 在指定会话中搜索消息 |
| `mark_chat_read` | 标记会话已读 |
| `pin_session` | 置顶会话 |
| `unpin_session` | 取消置顶会话 |
| `mark_session` | 标记会话为重要 |
| `unmark_session` | 取消标记会话 |

### 话题

| 工具 | 说明 |
|---|---|
| `create_thread` | 从消息创建话题 |
| `get_thread_messages` | 获取话题内的消息列表 |

### 用户

| 工具 | 说明 |
|---|---|
| `get_user_info` | 根据用户 ID 获取信息（同时支持用户和机器人） |
| `get_user_by_id` | 获取用户资料（包含 alias 和备注） |
| `get_user_profile_card` | 获取用户名片（头像、签名等） |
| `set_user_signature` | 设置当前用户签名 |
| `get_user_relation` | 查询与目标用户的关系 |
| `get_user_presence` | 获取用户在线状态 |

### 通讯录

| 工具 | 说明 |
|---|---|
| `save_contact_alias` | 保存联系人备注名和备忘 |
| `sync_contact_info` | 同步联系人信息 |

### Webhook 机器人

| 工具 | 说明 |
|---|---|
| `create_webhook_bot` | 在指定会话中创建 webhook 机器人 |
| `get_webhook_bot_info` | 获取 webhook 机器人的 webhook 地址和签名配置 |
| `send_webhook_bot_text` | 通过 webhook URL 发送文本消息 |
| `send_webhook_bot_post` | 通过 webhook URL 发送富文本 post 消息 |
| `send_webhook_bot_image` | 通过 webhook URL 使用已有 `image_key` 发送图片消息 |
| `send_webhook_bot_share_chat` | 通过 webhook URL 发送群名片消息 |
| `send_webhook_bot_card` | 通过 webhook URL 发送交互式卡片消息 |
| `send_webhook_bot_message` | 通过 webhook URL 发送原始 webhook payload |

### 日历

| 工具 | 说明 |
|---|---|
| `get_calendar_events` | 获取用户日历事件列表 |
| `web_list_calendar_events` | 按时间范围列出日历事件 |
| `mget_events_with_ids` | 按 event key 批量查询事件详情 |
| `create_calendar_event` | 创建日历事件（标题、时间、地点、提醒、与会者） |
| `update_calendar_event` | 更新已有日历事件 |
| `delete_calendar_event` | 删除日历事件 |
| `calendar_rsvp` | 回复日历邀请（接受/拒绝/待定） |
| `reply_calendar_event_invitation` | 带 RSVP 评论回复邀请 |
| `transfer_calendar_event` | 转让事件所有权给其他用户 |
| `web_share_calendar_event` | 把日历事件分享到一个或多个会话 |
| `upgrade_to_meeting` | 把日程升级为视频会议（创建会议群） |
| `create_meeting_minute` | 为日程创建会议纪要文档（返回飞书文档 URL） |
| `get_place_tips` | 事件地点自动补全 |

### 会议室与组织

| 工具 | 说明 |
|---|---|
| `search_guests` | 搜索可加为与会者的用户和群组 |
| `search_meeting_rooms` | 在指定时间窗口内搜索可用会议室 |
| `pull_buildings` | 列出包含会议室的楼宇 |
| `pull_meeting_rooms_in_building` | 按楼宇列出会议室 |
| `pull_tenants_by_ids` | 按租户 ID 批量查询组织信息 |

### 加急

| 工具 | 说明 |
|---|---|
| `send_urgent` | 发送加急通知 |
| `get_urgent_ack_status` | 查询加急确认状态 |

### 百科

| 工具 | 说明 |
|---|---|
| `get_baike_card` | 获取飞书词典词条卡片（定义、全称、贡献者、详情链接） |
| `get_baike_recommend_docs` | 获取词条的推荐文档 |

### 云文档

| 工具 | 说明 |
|---|---|
| `docx_open_document` | 打开飞书文档进行编辑，返回 block tree 并缓存供后续操作使用 |
| `get_docx_block_tree` | 获取文档 block tree（支持 wiki 或 docx） |
| `docx_insert_block` | 插入新 block |
| `docx_edit_text` | 编辑 block 的文本内容 |
| `docx_change_block_type` | 修改 block 类型（如文本→标题、列表项→待办） |
| `docx_set_block_properties` | 设置 block 属性（done / folded / align / language / emoji / 缩进 等） |
| `docx_delete_blocks` | 删除一个或多个 block |
| `docx_move_block` | 移动 block 到新的父节点或位置 |

### 配置

| 工具 | 说明 |
|---|---|
| `get_config` | 获取飞书配置项 |
| `get_feature_flags` | 获取 Feature Flags |

### 媒体

| 工具 | 说明 |
|---|---|
| `download_image` | 下载图片（返回 base64 编码数据） |
| `download_file` | 下载消息附件到本地临时文件并返回路径 |

### 使用 MCP Inspector 测试

```bash
OPENBIRD_COOKIE="your_cookie_here" \
  npx @modelcontextprotocol/inspector npx -y openbird mcp
```

## 项目结构

```
openbird/
├── bin/openbird.js              # CLI 入口
├── src/
│   ├── index.js                 # 模式分发与共享启动逻辑
│   ├── logger.js                # 日志（全部写 stderr，stdout 留给 MCP）
│   ├── core/                    # 飞书协议实现
│   │   ├── api.js               # HTTP API 客户端（34 个方法）
│   │   ├── auth.js              # Cookie 认证
│   │   ├── websocket.js         # WebSocket 客户端
│   │   ├── builders/            # Protobuf 请求/响应构建
│   │   ├── proto/               # .proto 定义及生成代码
│   │   └── utils/               # Cookie、加密、varint 工具
│   ├── webhook/
│   │   ├── normalizer.js        # WebSocket 消息 -> OpenBird 事件
│   │   └── dispatcher.js        # HTTP POST 转发（含重试）
│   └── mcp/
│       └── server.js            # MCP Server（77 个工具）
├── examples/
│   └── basic-usage.mjs          # MCP 示例
└── docs/
    └── plans/                   # 设计文档
```

## 开发

```bash
# 安装依赖
pnpm install

# 运行测试
pnpm test

# 测试覆盖率
pnpm test:coverage

# 修改 proto.proto 后重新生成代码
pnpm generate:proto

# 启动 MCP 模式
node bin/openbird.js mcp

# 启动 Relay 模式
node bin/openbird.js relay http://localhost:3000/webhook
```

### 测试

项目使用 [Vitest](https://vitest.dev/) 作为测试框架，当前有 **324 个测试**覆盖所有核心模块：

| 模块 | 测试数 |
|---|---|
| cookie, time, varint, encryption | 56 |
| header, params, richtext, proto | 103 |
| auth, api, websocket | 137 |
| normalizer, dispatcher | 23 |
| logger | 5 |

```bash
pnpm test              # 运行所有测试
pnpm test:watch         # 监听模式
pnpm test:coverage      # 覆盖率报告
```

## 工作原理

OpenBird 逆向了飞书网页版的 WebSocket 协议和 HTTP API，使用你的浏览器 cookie 以你的用户身份认证——和浏览器使用的是同一个会话。

**获取 cookie**：打开飞书网页版（feishu.cn），打开浏览器开发者工具，从任意 `*.feishu.cn` 请求中复制完整的 `Cookie` 请求头的值。

**注意**：这是非官方工具。飞书可能随时修改协议，使用风险自负。

## License

MIT
