Browse Source

created feishu

warrior 23 hours ago
parent
commit
98aa47d044

+ 7 - 0
.claude/settings.local.json

@@ -0,0 +1,7 @@
+{
+  "permissions": {
+    "allow": [
+      "WebFetch(domain:open.feishu.cn)"
+    ]
+  }
+}

+ 26 - 0
api/module/feishu/config.ts

@@ -0,0 +1,26 @@
+/**
+ * 飞书配置管理
+ */
+
+export interface FeishuConfig {
+  app_id: string;
+  app_secret: string;
+}
+
+/**
+ * 获取飞书配置
+ * 优先从环境变量读取,如果没有则使用默认配置
+ */
+export function getFeishuConfig(): FeishuConfig {
+  return {
+    app_id: process.env.FEISHU_APP_ID || '',
+    app_secret: process.env.FEISHU_APP_SECRET || ''
+  };
+}
+
+/**
+ * 验证配置是否有效
+ */
+export function validateConfig(config: FeishuConfig): boolean {
+  return !!(config.app_id && config.app_secret);
+}

+ 271 - 0
api/module/feishu/docs/ACCESS_TOKEN.md

@@ -0,0 +1,271 @@
+# 飞书 Access Token 配置指南
+
+## 概述
+
+本模块支持两种消息推送方式:
+
+1. **Webhook 方式**(无需 access token)
+   - 推送到群组
+   - 通过 @user_id 提及群组中的个人
+   - 简单易用,无需额外配置
+
+2. **开放平台 API 方式**(需要 access token)
+   - 直接推送到个人聊天
+   - 批量推送到多人
+   - 需要配置 app_id 和 app_secret
+
+## 为什么需要 Access Token?
+
+飞书的自定义机器人 Webhook 只能推送到群组,如果需要直接推送到个人聊天,就需要使用飞书开放平台 API,这需要:
+
+1. 创建企业自建应用
+2. 获取 app_id 和 app_secret
+3. 通过这两个参数获取 tenant_access_token
+4. 使用 token 调用发送消息 API
+
+## Token 管理机制
+
+本模块实现了完整的 token 管理机制:
+
+- **自动获取**:首次使用时自动从飞书 API 获取 token
+- **数据库存储**:token 保存到 Parse Server 的 Store 表的 config 字段
+- **自动刷新**:token 过期前自动刷新(提前 5 分钟)
+- **缓存优化**:优先从数据库读取,避免频繁请求飞书 API
+
+### 数据库存储格式
+
+Token 保存在 Store 表的 config 字段中,格式如下:
+
+```json
+{
+  "feishu": {
+    "accessToken": "t-xxx",
+    "expireTime": 1709971200000
+  }
+}
+```
+
+- `accessToken`: 飞书的 tenant_access_token
+- `expireTime`: 过期时间戳(毫秒),提前 5 分钟过期
+
+## 配置步骤
+
+### 1. 创建企业自建应用
+
+1. 访问 [飞书开放平台](https://open.feishu.cn/)
+2. 登录企业管理员账号
+3. 进入"开发者后台" → "企业自建应用"
+4. 点击"创建企业自建应用"
+5. 填写应用信息(名称、描述、图标等)
+6. 创建完成后,进入应用详情页
+
+### 2. 获取 App ID 和 App Secret
+
+在应用详情页的"凭证与基础信息"中,可以看到:
+
+- **App ID**:应用的唯一标识(格式:cli_xxx)
+- **App Secret**:应用的密钥(点击"查看"按钮显示)
+
+**重要提示:**
+- App Secret 只显示一次,请妥善保管
+- 如果忘记,可以重置,但会导致旧的 token 失效
+
+### 3. 配置应用权限
+
+在应用详情页的"权限管理"中,需要开通以下权限:
+
+- **im:message** - 获取与发送单聊、群组消息
+- **im:message:send_as_bot** - 以应用的身份发消息
+
+配置权限后,需要等待管理员审批(如果你是管理员,可以直接通过)。
+
+### 4. 配置环境变量
+
+在服务器上配置以下环境变量:
+
+```bash
+# Linux/Mac
+export FEISHU_APP_ID="cli_xxx"
+export FEISHU_APP_SECRET="your_app_secret"
+
+# Windows (PowerShell)
+$env:FEISHU_APP_ID="cli_xxx"
+$env:FEISHU_APP_SECRET="your_app_secret"
+
+# Windows (CMD)
+set FEISHU_APP_ID=cli_xxx
+set FEISHU_APP_SECRET=your_app_secret
+```
+
+或者在 `.env` 文件中配置(如果项目使用 dotenv):
+
+```env
+FEISHU_APP_ID=cli_xxx
+FEISHU_APP_SECRET=your_app_secret
+```
+
+### 5. 验证配置
+
+启动服务后,调用任意需要 access token 的接口,系统会自动:
+
+1. 从环境变量读取 app_id 和 app_secret
+2. 调用飞书 API 获取 tenant_access_token
+3. 保存到数据库(Store 表的 config 字段)
+4. 返回成功响应
+
+如果配置错误,会返回错误信息:
+
+```json
+{
+  "success": false,
+  "error": "飞书配置无效:请设置 FEISHU_APP_ID 和 FEISHU_APP_SECRET 环境变量",
+  "timestamp": "2026-03-09T14:00:00.000Z"
+}
+```
+
+## Token 生命周期
+
+### Token 有效期
+
+- 飞书的 tenant_access_token 有效期为 **2 小时**(7200 秒)
+- 本模块会提前 5 分钟刷新 token,避免边界情况
+
+### Token 刷新流程
+
+```
+1. 前端请求接口
+   ↓
+2. 后端检查数据库中的 token
+   ↓
+3. 判断是否过期
+   ├─ 未过期:直接使用
+   └─ 已过期:
+      ├─ 调用飞书 API 获取新 token
+      ├─ 保存到数据库
+      └─ 使用新 token
+   ↓
+4. 调用飞书 API 发送消息
+   ↓
+5. 返回结果
+```
+
+### 手动刷新 Token
+
+如果需要手动刷新 token(例如,怀疑 token 失效),可以:
+
+1. 删除数据库中的 token 记录
+2. 下次请求时会自动重新获取
+
+或者,可以添加一个管理接口来手动刷新 token(需要自行实现)。
+
+## 安全建议
+
+1. **保护 App Secret**
+   - 不要在前端代码中暴露 app_secret
+   - 不要提交到 Git 仓库
+   - 使用环境变量或密钥管理服务
+
+2. **限制应用权限**
+   - 只开通必要的权限
+   - 定期审查应用权限
+
+3. **监控 Token 使用**
+   - 记录 token 获取和刷新日志
+   - 监控异常的 token 使用情况
+
+4. **定期轮换密钥**
+   - 定期重置 app_secret
+   - 更新环境变量配置
+
+## 常见问题
+
+### Q1: 为什么获取 token 失败?
+
+**可能原因:**
+- app_id 或 app_secret 配置错误
+- 应用未启用或已停用
+- 网络连接问题
+
+**解决方法:**
+1. 检查环境变量配置是否正确
+2. 在飞书开放平台检查应用状态
+3. 检查服务器网络连接
+
+### Q2: Token 过期后会自动刷新吗?
+
+是的,本模块会自动检查 token 是否过期,如果过期会自动刷新。
+
+### Q3: 可以手动指定 token 吗?
+
+不建议。本模块已经实现了完整的 token 管理机制,手动指定 token 可能导致:
+- Token 过期后无法自动刷新
+- 多个服务实例之间 token 不一致
+
+### Q4: 数据库中的 token 丢失了怎么办?
+
+不用担心,下次请求时会自动重新获取并保存。
+
+### Q5: 如何查看当前的 token?
+
+可以直接查询 Parse Server 的 Store 表:
+
+```javascript
+const query = new Parse.Query('Store');
+query.equalTo('objectId', 'config');
+const store = await query.first({ useMasterKey: true });
+const config = store.get('config');
+console.log(config.feishu);
+```
+
+## 技术实现
+
+### Token 管理器
+
+本模块使用单例模式实现了 `TokenManager` 类,负责:
+
+- 从飞书 API 获取 token
+- 保存到数据库
+- 从数据库读取
+- 检查是否过期
+- 自动刷新
+
+### 配置管理
+
+配置管理模块从环境变量读取 app_id 和 app_secret:
+
+```typescript
+export function getFeishuConfig(): FeishuConfig {
+  return {
+    app_id: process.env.FEISHU_APP_ID || '',
+    app_secret: process.env.FEISHU_APP_SECRET || ''
+  };
+}
+```
+
+### 使用示例
+
+在路由中使用 token:
+
+```typescript
+import { getTokenManager } from './token-manager';
+
+// 获取 token
+const tokenManager = getTokenManager();
+const accessToken = await tokenManager.getAccessToken();
+
+// 使用 token 调用飞书 API
+const response = await fetch('https://open.feishu.cn/open-apis/im/v1/messages', {
+  method: 'POST',
+  headers: {
+    'Content-Type': 'application/json',
+    'Authorization': `Bearer ${accessToken}`
+  },
+  body: JSON.stringify({...})
+});
+```
+
+## 参考资料
+
+- [飞书开放平台 - 获取 tenant_access_token](https://open.feishu.cn/document/server-docs/api-call-guide/calling-process/get-access-token)
+- [飞书开放平台 - 发送消息](https://open.feishu.cn/document/server-docs/im-v1/message/create)
+- [飞书开放平台 - 权限说明](https://open.feishu.cn/document/home/introduction-to-scope-and-authorization/list-of-permissions-by-feature)

+ 457 - 0
api/module/feishu/docs/API_REFERENCE.md

@@ -0,0 +1,457 @@
+# 飞书消息推送 API 参考文档
+
+## 概述
+
+本模块提供两种消息推送方式:
+
+1. **Webhook 方式**:推送到群组,无需 access token
+2. **开放平台 API 方式**:推送到个人聊天,需要 access token
+
+## 基础 URL
+
+```
+http://your-server:3000/api/feishu
+```
+
+## 接口分类
+
+### 一、Webhook 方式(推送到群组)
+
+这些接口使用飞书自定义机器人的 Webhook URL,无需配置 access token。
+
+#### 1.1 通用消息发送
+
+**接口地址:** `POST /send`
+
+**功能说明:** 支持发送所有类型的飞书消息到群组。
+
+**请求参数:**
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| webhook_url | string | 是 | 飞书机器人的 Webhook URL |
+| msg_type | string | 是 | 消息类型:text、post、interactive |
+| content | object | 是 | 消息内容,根据 msg_type 不同而不同 |
+
+**请求示例:**
+
+```javascript
+fetch('http://your-server:3000/api/feishu/send', {
+  method: 'POST',
+  headers: { 'Content-Type': 'application/json' },
+  body: JSON.stringify({
+    webhook_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+    msg_type: 'text',
+    content: { text: '这是一条测试消息' }
+  })
+});
+```
+
+**响应示例:**
+
+```json
+{
+  "success": true,
+  "data": {
+    "message": "消息发送成功",
+    "feishu_response": { "code": 0, "msg": "success" }
+  },
+  "timestamp": "2026-03-09T14:00:00.000Z"
+}
+```
+
+#### 1.2 发送文本消息
+
+**接口地址:** `POST /send/text`
+
+**功能说明:** 快速发送纯文本消息到群组。
+
+**请求参数:**
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| webhook_url | string | 是 | 飞书机器人的 Webhook URL |
+| text | string | 是 | 消息文本内容 |
+
+**请求示例:**
+
+```javascript
+fetch('http://your-server:3000/api/feishu/send/text', {
+  method: 'POST',
+  headers: { 'Content-Type': 'application/json' },
+  body: JSON.stringify({
+    webhook_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+    text: '这是一条简单的文本消息'
+  })
+});
+```
+
+#### 1.3 发送富文本消息
+
+**接口地址:** `POST /send/post`
+
+**功能说明:** 发送富文本消息到群组,支持标题、@用户、链接等。
+
+**请求参数:**
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| webhook_url | string | 是 | 飞书机器人的 Webhook URL |
+| title | string | 否 | 消息标题 |
+| content | array | 是 | 消息内容数组(二维数组) |
+
+**请求示例(@单个用户):**
+
+```javascript
+fetch('http://your-server:3000/api/feishu/send/post', {
+  method: 'POST',
+  headers: { 'Content-Type': 'application/json' },
+  body: JSON.stringify({
+    webhook_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+    title: '任务提醒',
+    content: [
+      [
+        { tag: 'at', user_id: 'ou_xxxxxx' },
+        { tag: 'text', text: ' 您有一个新任务需要处理' }
+      ]
+    ]
+  })
+});
+```
+
+**请求示例(@所有人):**
+
+```javascript
+fetch('http://your-server:3000/api/feishu/send/post', {
+  method: 'POST',
+  headers: { 'Content-Type': 'application/json' },
+  body: JSON.stringify({
+    webhook_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+    title: '重要通知',
+    content: [
+      [
+        { tag: 'at', user_id: 'all' },
+        { tag: 'text', text: ' 系统将于今晚 22:00 进行维护' }
+      ]
+    ]
+  })
+});
+```
+
+### 二、开放平台 API 方式(推送到个人)
+
+这些接口使用飞书开放平台 API,需要配置 access token。详见 [ACCESS_TOKEN.md](./ACCESS_TOKEN.md)。
+
+#### 2.1 发送消息到个人
+
+**接口地址:** `POST /message/send`
+
+**功能说明:** 直接发送消息到个人聊天。
+
+**前置条件:**
+- 已配置 FEISHU_APP_ID 和 FEISHU_APP_SECRET 环境变量
+- 应用已开通 im:message 权限
+
+**请求参数:**
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| receive_id_type | string | 是 | 接收者 ID 类型:open_id、user_id、union_id、email、chat_id |
+| receive_id | string | 是 | 接收者 ID |
+| msg_type | string | 是 | 消息类型:text、post、interactive |
+| content | object/string | 是 | 消息内容 |
+
+**receive_id_type 说明:**
+
+- `open_id`: 用户的 Open ID(推荐,最常用)
+- `user_id`: 用户的 User ID(企业内部 ID)
+- `union_id`: 用户的 Union ID(跨应用唯一)
+- `email`: 用户的邮箱地址
+- `chat_id`: 群组 ID(发送到群组)
+
+**请求示例(发送文本消息):**
+
+```javascript
+fetch('http://your-server:3000/api/feishu/message/send', {
+  method: 'POST',
+  headers: { 'Content-Type': 'application/json' },
+  body: JSON.stringify({
+    receive_id_type: 'open_id',
+    receive_id: 'ou_xxxxxx',
+    msg_type: 'text',
+    content: { text: '您好,这是一条测试消息' }
+  })
+});
+```
+
+**请求示例(发送富文本消息):**
+
+```javascript
+fetch('http://your-server:3000/api/feishu/message/send', {
+  method: 'POST',
+  headers: { 'Content-Type': 'application/json' },
+  body: JSON.stringify({
+    receive_id_type: 'open_id',
+    receive_id: 'ou_xxxxxx',
+    msg_type: 'post',
+    content: {
+      zh_cn: {
+        title: '任务提醒',
+        content: [
+          [
+            { tag: 'text', text: '您有一个新任务:' },
+            { tag: 'a', text: '点击查看', href: 'https://example.com/task/123' }
+          ]
+        ]
+      }
+    }
+  })
+});
+```
+
+**响应示例:**
+
+```json
+{
+  "success": true,
+  "data": {
+    "message": "消息发送成功",
+    "feishu_response": {
+      "code": 0,
+      "msg": "success",
+      "data": {
+        "message_id": "om_xxx"
+      }
+    }
+  },
+  "timestamp": "2026-03-09T14:00:00.000Z"
+}
+```
+
+#### 2.2 发送文本消息到个人(简化版)
+
+**接口地址:** `POST /message/send/text`
+
+**功能说明:** 快速发送纯文本消息到个人聊天。
+
+**请求参数:**
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| receive_id_type | string | 是 | 接收者 ID 类型 |
+| receive_id | string | 是 | 接收者 ID |
+| text | string | 是 | 消息文本内容 |
+
+**请求示例:**
+
+```javascript
+fetch('http://your-server:3000/api/feishu/message/send/text', {
+  method: 'POST',
+  headers: { 'Content-Type': 'application/json' },
+  body: JSON.stringify({
+    receive_id_type: 'open_id',
+    receive_id: 'ou_xxxxxx',
+    text: '您好,这是一条测试消息'
+  })
+});
+```
+
+#### 2.3 批量发送消息到多人
+
+**接口地址:** `POST /message/send/batch`
+
+**功能说明:** 批量发送相同消息到多个用户。
+
+**请求参数:**
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| receive_id_type | string | 是 | 接收者 ID 类型 |
+| receive_ids | array | 是 | 接收者 ID 数组 |
+| msg_type | string | 是 | 消息类型 |
+| content | object/string | 是 | 消息内容 |
+
+**请求示例:**
+
+```javascript
+fetch('http://your-server:3000/api/feishu/message/send/batch', {
+  method: 'POST',
+  headers: { 'Content-Type': 'application/json' },
+  body: JSON.stringify({
+    receive_id_type: 'open_id',
+    receive_ids: ['ou_user1', 'ou_user2', 'ou_user3'],
+    msg_type: 'text',
+    content: { text: '会议将于 10 分钟后开始,请准时参加' }
+  })
+});
+```
+
+**响应示例:**
+
+```json
+{
+  "success": true,
+  "data": {
+    "message": "批量发送完成: 成功 3 条,失败 0 条",
+    "success_count": 3,
+    "failed_count": 0,
+    "failed_details": []
+  },
+  "timestamp": "2026-03-09T14:00:00.000Z"
+}
+```
+
+## 错误处理
+
+### 错误响应格式
+
+```json
+{
+  "success": false,
+  "error": "错误信息",
+  "timestamp": "2026-03-09T14:00:00.000Z"
+}
+```
+
+### 常见错误
+
+#### Webhook 方式错误
+
+| HTTP 状态码 | 错误信息 | 说明 | 解决方法 |
+|------------|---------|------|---------|
+| 400 | webhook_url 是必填项 | 未提供 webhook_url | 检查请求参数 |
+| 400 | webhook_url 格式不正确 | URL 格式错误 | 确认 URL 以 https://open.feishu.cn/open-apis/bot/v2/hook/ 开头 |
+| 500 | 飞书消息发送失败 | 飞书 API 返回错误 | 检查 Webhook URL 是否有效 |
+
+#### 开放平台 API 错误
+
+| HTTP 状态码 | 错误信息 | 说明 | 解决方法 |
+|------------|---------|------|---------|
+| 400 | receive_id_type 是必填项 | 未提供接收者类型 | 检查请求参数 |
+| 400 | receive_id 是必填项 | 未提供接收者 ID | 检查请求参数 |
+| 500 | 飞书配置无效 | 未配置环境变量 | 配置 FEISHU_APP_ID 和 FEISHU_APP_SECRET |
+| 500 | 获取飞书 token 失败 | Token 获取失败 | 检查 app_id 和 app_secret 是否正确 |
+| 500 | 飞书消息发送失败 | API 调用失败 | 检查应用权限和接收者 ID |
+
+## 使用场景对比
+
+### 何时使用 Webhook 方式?
+
+**适用场景:**
+- 推送到群组
+- 不需要推送到个人聊天
+- 简单快速,无需额外配置
+
+**优点:**
+- 配置简单,只需 Webhook URL
+- 无需管理 access token
+- 适合快速集成
+
+**缺点:**
+- 只能推送到群组
+- 无法推送到个人聊天
+
+### 何时使用开放平台 API?
+
+**适用场景:**
+- 需要推送到个人聊天
+- 需要批量推送到多人
+- 需要更精细的权限控制
+
+**优点:**
+- 可以推送到个人聊天
+- 支持批量推送
+- 功能更强大
+
+**缺点:**
+- 需要配置 app_id 和 app_secret
+- 需要管理 access token
+- 需要开通应用权限
+
+## 完整示例
+
+### 示例 1:系统告警(推送到群组)
+
+```javascript
+async function sendAlert(message) {
+  const response = await fetch('http://your-server:3000/api/feishu/send/post', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({
+      webhook_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+      title: '⚠️ 系统告警',
+      content: [
+        [
+          { tag: 'at', user_id: 'all' },
+          { tag: 'text', text: ' 系统检测到异常' }
+        ],
+        [
+          { tag: 'text', text: `错误信息:${message}` }
+        ]
+      ]
+    })
+  });
+  return response.json();
+}
+```
+
+### 示例 2:任务分配(推送到个人)
+
+```javascript
+async function assignTask(userId, taskName, taskUrl) {
+  const response = await fetch('http://your-server:3000/api/feishu/message/send', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({
+      receive_id_type: 'open_id',
+      receive_id: userId,
+      msg_type: 'post',
+      content: {
+        zh_cn: {
+          title: '📋 新任务分配',
+          content: [
+            [
+              { tag: 'text', text: `您有一个新任务:${taskName}` }
+            ],
+            [
+              { tag: 'text', text: '查看详情:' },
+              { tag: 'a', text: '点击这里', href: taskUrl }
+            ]
+          ]
+        }
+      }
+    })
+  });
+  return response.json();
+}
+```
+
+### 示例 3:批量通知(推送到多人)
+
+```javascript
+async function notifyMultipleUsers(userIds, message) {
+  const response = await fetch('http://your-server:3000/api/feishu/message/send/batch', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({
+      receive_id_type: 'open_id',
+      receive_ids: userIds,
+      msg_type: 'text',
+      content: { text: message }
+    })
+  });
+  return response.json();
+}
+
+// 使用示例
+notifyMultipleUsers(
+  ['ou_user1', 'ou_user2', 'ou_user3'],
+  '会议将于 10 分钟后开始,请准时参加'
+);
+```
+
+## 参考资料
+
+- [飞书开放平台 - 自定义机器人](https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot)
+- [飞书开放平台 - 发送消息](https://open.feishu.cn/document/server-docs/im-v1/message/create)
+- [ACCESS_TOKEN.md - Access Token 配置指南](./ACCESS_TOKEN.md)

+ 554 - 0
api/module/feishu/docs/README.md

@@ -0,0 +1,554 @@
+# 飞书机器人推送接口文档
+
+## 概述
+
+本模块提供了飞书机器人消息推送功能,支持向飞书群组或个人发送消息。通过简单的 HTTP 接口调用,前端可以轻松实现飞书消息推送。
+
+## 前置准备
+
+### 1. 创建飞书机器人
+
+在使用接口之前,需要先在飞书群组中创建自定义机器人:
+
+1. 打开飞书群组
+2. 点击群设置 → 群机器人 → 添加机器人
+3. 选择"自定义机器人"
+4. 配置机器人名称、描述等信息
+5. 获取 Webhook URL(格式:`https://open.feishu.cn/open-apis/bot/v2/hook/xxx`)
+
+**重要提示:**
+- 每个群组可以创建多个机器人
+- Webhook URL 包含机器人的访问凭证,请妥善保管
+- 机器人发送的消息会显示机器人的名称和头像
+
+### 2. 配置安全设置(可选)
+
+飞书机器人支持以下安全设置:
+- **签名验证**:通过签名验证请求来源
+- **IP 白名单**:限制只有特定 IP 可以调用
+- **关键词验证**:消息必须包含特定关键词
+
+本接口暂不支持签名验证,如需使用请在飞书后台配置 IP 白名单或关键词验证。
+
+## 接口列表
+
+### 基础 URL
+
+```
+http://your-server:3000/api/feishu
+```
+
+### 1. 通用消息发送接口
+
+**接口地址:** `POST /api/feishu/send`
+
+**功能说明:** 支持发送所有类型的飞书消息,灵活性最高。
+
+**请求参数:**
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| webhook_url | string | 是 | 飞书机器人的 Webhook URL |
+| msg_type | string | 是 | 消息类型:text(文本)、post(富文本)、interactive(卡片) |
+| content | object | 是 | 消息内容,根据 msg_type 不同而不同 |
+
+**请求示例:**
+
+```javascript
+// 发送文本消息
+fetch('http://your-server:3000/api/feishu/send', {
+  method: 'POST',
+  headers: {
+    'Content-Type': 'application/json',
+  },
+  body: JSON.stringify({
+    webhook_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+    msg_type: 'text',
+    content: {
+      text: '这是一条测试消息'
+    }
+  })
+})
+.then(res => res.json())
+.then(data => console.log(data));
+```
+
+**响应示例:**
+
+```json
+{
+  "success": true,
+  "data": {
+    "message": "消息发送成功",
+    "feishu_response": {
+      "code": 0,
+      "msg": "success"
+    }
+  },
+  "timestamp": "2026-03-09T11:45:00.000Z"
+}
+```
+
+### 2. 文本消息发送接口(简化版)
+
+**接口地址:** `POST /api/feishu/send/text`
+
+**功能说明:** 快速发送纯文本消息,接口更简洁。
+
+**请求参数:**
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| webhook_url | string | 是 | 飞书机器人的 Webhook URL |
+| text | string | 是 | 消息文本内容 |
+
+**请求示例:**
+
+```javascript
+fetch('http://your-server:3000/api/feishu/send/text', {
+  method: 'POST',
+  headers: {
+    'Content-Type': 'application/json',
+  },
+  body: JSON.stringify({
+    webhook_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+    text: '这是一条简单的文本消息'
+  })
+})
+.then(res => res.json())
+.then(data => console.log(data));
+```
+
+**响应示例:**
+
+```json
+{
+  "success": true,
+  "data": {
+    "message": "文本消息发送成功",
+    "feishu_response": {
+      "code": 0,
+      "msg": "success"
+    }
+  },
+  "timestamp": "2026-03-09T11:45:00.000Z"
+}
+```
+
+### 3. 富文本消息发送接口(支持 @用户)
+
+**接口地址:** `POST /api/feishu/send/post`
+
+**功能说明:** 发送富文本消息,支持标题、多段落、@用户、链接等丰富格式。
+
+**请求参数:**
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| webhook_url | string | 是 | 飞书机器人的 Webhook URL |
+| title | string | 否 | 消息标题 |
+| content | array | 是 | 消息内容数组,详见下方说明 |
+
+**content 格式说明:**
+
+content 是一个二维数组,第一层代表段落,第二层代表段落内的元素。
+
+支持的元素类型:
+- `text`:普通文本
+- `a`:链接
+- `at`:@用户
+
+**请求示例 1:发送带标题的富文本消息**
+
+```javascript
+fetch('http://your-server:3000/api/feishu/send/post', {
+  method: 'POST',
+  headers: {
+    'Content-Type': 'application/json',
+  },
+  body: JSON.stringify({
+    webhook_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+    title: '系统通知',
+    content: [
+      [
+        { tag: 'text', text: '您有一条新的任务待处理' }
+      ],
+      [
+        { tag: 'text', text: '任务详情:' },
+        { tag: 'a', text: '点击查看', href: 'https://example.com/task/123' }
+      ]
+    ]
+  })
+})
+.then(res => res.json())
+.then(data => console.log(data));
+```
+
+**请求示例 2:@单个用户**
+
+```javascript
+fetch('http://your-server:3000/api/feishu/send/post', {
+  method: 'POST',
+  headers: {
+    'Content-Type': 'application/json',
+  },
+  body: JSON.stringify({
+    webhook_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+    title: '任务提醒',
+    content: [
+      [
+        { tag: 'at', user_id: 'ou_xxxxxx' },
+        { tag: 'text', text: ' 您有一个新任务需要处理' }
+      ]
+    ]
+  })
+})
+.then(res => res.json())
+.then(data => console.log(data));
+```
+
+**请求示例 3:@所有人**
+
+```javascript
+fetch('http://your-server:3000/api/feishu/send/post', {
+  method: 'POST',
+  headers: {
+    'Content-Type': 'application/json',
+  },
+  body: JSON.stringify({
+    webhook_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+    title: '重要通知',
+    content: [
+      [
+        { tag: 'at', user_id: 'all' },
+        { tag: 'text', text: ' 系统将于今晚 22:00 进行维护,请提前保存工作' }
+      ]
+    ]
+  })
+})
+.then(res => res.json())
+.then(data => console.log(data));
+```
+
+**请求示例 4:@多个用户**
+
+```javascript
+fetch('http://your-server:3000/api/feishu/send/post', {
+  method: 'POST',
+  headers: {
+    'Content-Type': 'application/json',
+  },
+  body: JSON.stringify({
+    webhook_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+    title: '项目进度更新',
+    content: [
+      [
+        { tag: 'at', user_id: 'ou_user1' },
+        { tag: 'text', text: ' ' },
+        { tag: 'at', user_id: 'ou_user2' },
+        { tag: 'text', text: ' 请查看最新的项目进度' }
+      ],
+      [
+        { tag: 'text', text: '详情:' },
+        { tag: 'a', text: '项目看板', href: 'https://example.com/project' }
+      ]
+    ]
+  })
+})
+.then(res => res.json())
+.then(data => console.log(data));
+```
+
+**响应示例:**
+
+```json
+{
+  "success": true,
+  "data": {
+    "message": "富文本消息发送成功",
+    "feishu_response": {
+      "code": 0,
+      "msg": "success"
+    }
+  },
+  "timestamp": "2026-03-09T11:45:00.000Z"
+}
+```
+
+## 错误处理
+
+### 错误响应格式
+
+```json
+{
+  "success": false,
+  "error": "错误信息",
+  "timestamp": "2026-03-09T11:45:00.000Z"
+}
+```
+
+### 常见错误码
+
+| HTTP 状态码 | 错误信息 | 说明 |
+|------------|---------|------|
+| 400 | webhook_url 是必填项 | 未提供 webhook_url |
+| 400 | msg_type 是必填项 | 未提供 msg_type |
+| 400 | content 是必填项 | 未提供 content |
+| 400 | webhook_url 格式不正确 | webhook_url 不是有效的飞书 Webhook URL |
+| 500 | 飞书消息发送失败 | 飞书 API 返回错误 |
+| 500 | 消息发送失败 | 网络错误或其他异常 |
+
+## 使用场景示例
+
+### 场景 1:系统告警通知
+
+```javascript
+// 当系统检测到异常时,发送告警消息
+async function sendAlert(errorMessage) {
+  const response = await fetch('http://your-server:3000/api/feishu/send/post', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({
+      webhook_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+      title: '⚠️ 系统告警',
+      content: [
+        [
+          { tag: 'at', user_id: 'all' },
+          { tag: 'text', text: ' 系统检测到异常' }
+        ],
+        [
+          { tag: 'text', text: `错误信息:${errorMessage}` }
+        ],
+        [
+          { tag: 'text', text: `时间:${new Date().toLocaleString()}` }
+        ]
+      ]
+    })
+  });
+  return response.json();
+}
+```
+
+### 场景 2:任务分配通知
+
+```javascript
+// 当任务分配给用户时,发送通知
+async function notifyTaskAssignment(userId, taskName, taskUrl) {
+  const response = await fetch('http://your-server:3000/api/feishu/send/post', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({
+      webhook_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+      title: '📋 新任务分配',
+      content: [
+        [
+          { tag: 'at', user_id: userId },
+          { tag: 'text', text: ` 您有一个新任务:${taskName}` }
+        ],
+        [
+          { tag: 'text', text: '查看详情:' },
+          { tag: 'a', text: '点击这里', href: taskUrl }
+        ]
+      ]
+    })
+  });
+  return response.json();
+}
+```
+
+### 场景 3:批量通知
+
+```javascript
+// 向多个用户发送通知
+async function notifyMultipleUsers(userIds, message) {
+  // 构建 @用户 的内容
+  const atUsers = userIds.flatMap(userId => [
+    { tag: 'at', user_id: userId },
+    { tag: 'text', text: ' ' }
+  ]);
+
+  const response = await fetch('http://your-server:3000/api/feishu/send/post', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({
+      webhook_url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+      title: '通知',
+      content: [
+        atUsers,
+        [
+          { tag: 'text', text: message }
+        ]
+      ]
+    })
+  });
+  return response.json();
+}
+
+// 使用示例
+notifyMultipleUsers(
+  ['ou_user1', 'ou_user2', 'ou_user3'],
+  '会议将于 10 分钟后开始,请准时参加'
+);
+```
+
+## 注意事项
+
+1. **Webhook URL 安全**
+   - Webhook URL 包含访问凭证,请勿在前端代码中硬编码
+   - 建议将 Webhook URL 存储在后端配置或数据库中
+   - 可以为不同的群组或场景配置不同的机器人
+
+2. **消息频率限制**
+   - 飞书机器人有消息发送频率限制(通常为 20 条/分钟)
+   - 超过限制会返回错误,请合理控制发送频率
+   - 建议实现消息队列机制,避免短时间内大量发送
+
+3. **用户 ID 获取**
+   - @用户需要提供用户的 Open ID(格式:`ou_xxxxxx`)
+   - 可以通过飞书开放平台 API 获取用户 ID
+   - @所有人使用 `user_id: 'all'`
+
+4. **消息格式**
+   - 富文本消息的 content 必须是二维数组
+   - 每个段落是一个数组,包含多个元素
+   - 元素必须包含 `tag` 字段,根据不同 tag 提供对应的字段
+
+5. **错误处理**
+   - 建议在前端实现重试机制
+   - 记录发送失败的消息,便于排查问题
+   - 对用户友好地展示错误信息
+
+## 完整的前端封装示例
+
+```javascript
+// feishu-bot.js
+class FeishuBot {
+  constructor(baseUrl = 'http://your-server:3000/api/feishu') {
+    this.baseUrl = baseUrl;
+  }
+
+  /**
+   * 发送文本消息
+   */
+  async sendText(webhookUrl, text) {
+    return this._request('/send/text', {
+      webhook_url: webhookUrl,
+      text
+    });
+  }
+
+  /**
+   * 发送富文本消息
+   */
+  async sendPost(webhookUrl, title, content) {
+    return this._request('/send/post', {
+      webhook_url: webhookUrl,
+      title,
+      content
+    });
+  }
+
+  /**
+   * @单个用户
+   */
+  async mentionUser(webhookUrl, userId, message, title = '通知') {
+    return this.sendPost(webhookUrl, title, [
+      [
+        { tag: 'at', user_id: userId },
+        { tag: 'text', text: ` ${message}` }
+      ]
+    ]);
+  }
+
+  /**
+   * @所有人
+   */
+  async mentionAll(webhookUrl, message, title = '重要通知') {
+    return this.sendPost(webhookUrl, title, [
+      [
+        { tag: 'at', user_id: 'all' },
+        { tag: 'text', text: ` ${message}` }
+      ]
+    ]);
+  }
+
+  /**
+   * @多个用户
+   */
+  async mentionMultiple(webhookUrl, userIds, message, title = '通知') {
+    const atUsers = userIds.flatMap(userId => [
+      { tag: 'at', user_id: userId },
+      { tag: 'text', text: ' ' }
+    ]);
+
+    return this.sendPost(webhookUrl, title, [
+      atUsers,
+      [{ tag: 'text', text: message }]
+    ]);
+  }
+
+  /**
+   * 内部请求方法
+   */
+  async _request(endpoint, data) {
+    try {
+      const response = await fetch(`${this.baseUrl}${endpoint}`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(data)
+      });
+
+      const result = await response.json();
+
+      if (!result.success) {
+        throw new Error(result.error || '发送失败');
+      }
+
+      return result;
+    } catch (error) {
+      console.error('飞书消息发送失败:', error);
+      throw error;
+    }
+  }
+}
+
+// 使用示例
+const bot = new FeishuBot();
+
+// 发送文本消息
+bot.sendText(
+  'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+  '这是一条测试消息'
+);
+
+// @单个用户
+bot.mentionUser(
+  'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+  'ou_xxxxxx',
+  '您有新的任务'
+);
+
+// @所有人
+bot.mentionAll(
+  'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+  '系统维护通知'
+);
+
+// @多个用户
+bot.mentionMultiple(
+  'https://open.feishu.cn/open-apis/bot/v2/hook/xxx',
+  ['ou_user1', 'ou_user2'],
+  '会议即将开始'
+);
+```
+
+## 参考资料
+
+- [飞书开放平台 - 自定义机器人指南](https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot)
+- [飞书开放平台 - 消息类型说明](https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN)
+
+## 更新日志
+
+- 2026-03-09:初始版本,支持文本消息和富文本消息发送

+ 2 - 0
api/module/feishu/index.ts

@@ -0,0 +1,2 @@
+export { createFeishuRoutes } from './routes';
+export * from './types';

+ 364 - 0
api/module/feishu/routes.ts

@@ -0,0 +1,364 @@
+import { Router, Request, Response, NextFunction } from 'express';
+import { SendMessageRequest, SendToUserRequest, SendToMultipleUsersRequest, FeishuResponse } from './types';
+import { getTokenManager } from './token-manager';
+
+// 统一响应格式
+const sendResponse = (res: Response, data: any) => {
+  res.json({
+    success: true,
+    data,
+    timestamp: new Date().toISOString()
+  });
+};
+
+// 统一错误响应格式
+const sendError = (res: Response, message: string, statusCode: number = 400) => {
+  res.status(statusCode).json({
+    success: false,
+    error: message,
+    timestamp: new Date().toISOString()
+  });
+};
+
+const asyncHandler = (fn: (req: Request, res: Response, next: NextFunction) => Promise<any>) =>
+  (req: Request, res: Response, next: NextFunction) => {
+    Promise.resolve(fn(req, res, next)).catch(next);
+  };
+
+/**
+ * 发送消息到飞书机器人
+ */
+const sendToFeishu = async (webhook_url: string, msg_type: string, content: any): Promise<FeishuResponse> => {
+  const response = await fetch(webhook_url, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: JSON.stringify({
+      msg_type,
+      content
+    })
+  });
+
+  if (!response.ok) {
+    throw new Error(`飞书 API 请求失败: ${response.status} ${response.statusText}`);
+  }
+
+  return await response.json();
+};
+
+/**
+ * 创建飞书路由
+ */
+export const createFeishuRoutes = () => {
+  const router = Router();
+
+  // 发送消息到飞书
+  // POST /send
+  // Body: { webhook_url, msg_type, content }
+  router.post('/send', asyncHandler(async (req, res) => {
+    const { webhook_url, msg_type, content } = req.body as SendMessageRequest;
+
+    // 参数验证
+    if (!webhook_url) {
+      return sendError(res, 'webhook_url 是必填项');
+    }
+
+    if (!msg_type) {
+      return sendError(res, 'msg_type 是必填项');
+    }
+
+    if (!content) {
+      return sendError(res, 'content 是必填项');
+    }
+
+    // 验证 webhook_url 格式
+    if (!webhook_url.startsWith('https://open.feishu.cn/open-apis/bot/v2/hook/')) {
+      return sendError(res, 'webhook_url 格式不正确');
+    }
+
+    try {
+      const result = await sendToFeishu(webhook_url, msg_type, content);
+
+      // 检查飞书 API 返回的状态
+      if (result.code !== 0) {
+        return sendError(res, `飞书消息发送失败: ${result.msg}`, 500);
+      }
+
+      sendResponse(res, {
+        message: '消息发送成功',
+        feishu_response: result
+      });
+    } catch (error: any) {
+      return sendError(res, error.message || '消息发送失败', 500);
+    }
+  }));
+
+  // 发送文本消息(简化接口)
+  // POST /send/text
+  // Body: { webhook_url, text }
+  router.post('/send/text', asyncHandler(async (req, res) => {
+    const { webhook_url, text } = req.body;
+
+    if (!webhook_url) {
+      return sendError(res, 'webhook_url 是必填项');
+    }
+
+    if (!text) {
+      return sendError(res, 'text 是必填项');
+    }
+
+    try {
+      const result = await sendToFeishu(webhook_url, 'text', { text });
+
+      if (result.code !== 0) {
+        return sendError(res, `飞书消息发送失败: ${result.msg}`, 500);
+      }
+
+      sendResponse(res, {
+        message: '文本消息发送成功',
+        feishu_response: result
+      });
+    } catch (error: any) {
+      return sendError(res, error.message || '消息发送失败', 500);
+    }
+  }));
+
+  // 发送富文本消息(支持 @用户)
+  // POST /send/post
+  // Body: { webhook_url, title, content }
+  router.post('/send/post', asyncHandler(async (req, res) => {
+    const { webhook_url, title, content } = req.body;
+
+    if (!webhook_url) {
+      return sendError(res, 'webhook_url 是必填项');
+    }
+
+    if (!content) {
+      return sendError(res, 'content 是必填项');
+    }
+
+    try {
+      const postContent = {
+        zh_cn: {
+          title: title || '',
+          content
+        }
+      };
+
+      const result = await sendToFeishu(webhook_url, 'post', postContent);
+
+      if (result.code !== 0) {
+        return sendError(res, `飞书消息发送失败: ${result.msg}`, 500);
+      }
+
+      sendResponse(res, {
+        message: '富文本消息发送成功',
+        feishu_response: result
+      });
+    } catch (error: any) {
+      return sendError(res, error.message || '消息发送失败', 500);
+    }
+  }));
+
+  // ==========================================
+  // 使用 Access Token 的接口(发送消息到个人)
+  // ==========================================
+
+  /**
+   * 发送消息到个人(使用 Access Token)
+   * POST /message/send
+   * Body: { receive_id_type, receive_id, msg_type, content }
+   */
+  router.post('/message/send', asyncHandler(async (req, res) => {
+    const { receive_id_type, receive_id, msg_type, content } = req.body as SendToUserRequest;
+
+    // 参数验证
+    if (!receive_id_type) {
+      return sendError(res, 'receive_id_type 是必填项');
+    }
+
+    if (!receive_id) {
+      return sendError(res, 'receive_id 是必填项');
+    }
+
+    if (!msg_type) {
+      return sendError(res, 'msg_type 是必填项');
+    }
+
+    if (!content) {
+      return sendError(res, 'content 是必填项');
+    }
+
+    try {
+      // 获取 access token
+      const tokenManager = getTokenManager();
+      const accessToken = await tokenManager.getAccessToken();
+
+      // 调用飞书 API
+      const response = await fetch(`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receive_id_type}`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+          'Authorization': `Bearer ${accessToken}`
+        },
+        body: JSON.stringify({
+          receive_id,
+          msg_type,
+          content: typeof content === 'string' ? content : JSON.stringify(content)
+        })
+      });
+
+      if (!response.ok) {
+        throw new Error(`飞书 API 请求失败: ${response.status} ${response.statusText}`);
+      }
+
+      const result: FeishuResponse = await response.json();
+
+      if (result.code !== 0) {
+        return sendError(res, `飞书消息发送失败: ${result.msg}`, 500);
+      }
+
+      sendResponse(res, {
+        message: '消息发送成功',
+        feishu_response: result
+      });
+    } catch (error: any) {
+      return sendError(res, error.message || '消息发送失败', 500);
+    }
+  }));
+
+  /**
+   * 发送文本消息到个人(简化版)
+   * POST /message/send/text
+   * Body: { receive_id_type, receive_id, text }
+   */
+  router.post('/message/send/text', asyncHandler(async (req, res) => {
+    const { receive_id_type, receive_id, text } = req.body;
+
+    if (!receive_id_type) {
+      return sendError(res, 'receive_id_type 是必填项');
+    }
+
+    if (!receive_id) {
+      return sendError(res, 'receive_id 是必填项');
+    }
+
+    if (!text) {
+      return sendError(res, 'text 是必填项');
+    }
+
+    try {
+      const tokenManager = getTokenManager();
+      const accessToken = await tokenManager.getAccessToken();
+
+      const response = await fetch(`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receive_id_type}`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+          'Authorization': `Bearer ${accessToken}`
+        },
+        body: JSON.stringify({
+          receive_id,
+          msg_type: 'text',
+          content: JSON.stringify({ text })
+        })
+      });
+
+      if (!response.ok) {
+        throw new Error(`飞书 API 请求失败: ${response.status} ${response.statusText}`);
+      }
+
+      const result: FeishuResponse = await response.json();
+
+      if (result.code !== 0) {
+        return sendError(res, `飞书消息发送失败: ${result.msg}`, 500);
+      }
+
+      sendResponse(res, {
+        message: '文本消息发送成功',
+        feishu_response: result
+      });
+    } catch (error: any) {
+      return sendError(res, error.message || '消息发送失败', 500);
+    }
+  }));
+
+  /**
+   * 批量发送消息到多人
+   * POST /message/send/batch
+   * Body: { receive_id_type, receive_ids, msg_type, content }
+   */
+  router.post('/message/send/batch', asyncHandler(async (req, res) => {
+    const { receive_id_type, receive_ids, msg_type, content } = req.body as SendToMultipleUsersRequest;
+
+    if (!receive_id_type) {
+      return sendError(res, 'receive_id_type 是必填项');
+    }
+
+    if (!receive_ids || !Array.isArray(receive_ids) || receive_ids.length === 0) {
+      return sendError(res, 'receive_ids 必须是非空数组');
+    }
+
+    if (!msg_type) {
+      return sendError(res, 'msg_type 是必填项');
+    }
+
+    if (!content) {
+      return sendError(res, 'content 是必填项');
+    }
+
+    try {
+      const tokenManager = getTokenManager();
+      const accessToken = await tokenManager.getAccessToken();
+
+      // 批量发送消息
+      const results = await Promise.allSettled(
+        receive_ids.map(async (receive_id) => {
+          const response = await fetch(`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receive_id_type}`, {
+            method: 'POST',
+            headers: {
+              'Content-Type': 'application/json',
+              'Authorization': `Bearer ${accessToken}`
+            },
+            body: JSON.stringify({
+              receive_id,
+              msg_type,
+              content: typeof content === 'string' ? content : JSON.stringify(content)
+            })
+          });
+
+          if (!response.ok) {
+            throw new Error(`发送到 ${receive_id} 失败: ${response.status}`);
+          }
+
+          const result: FeishuResponse = await response.json();
+
+          if (result.code !== 0) {
+            throw new Error(`发送到 ${receive_id} 失败: ${result.msg}`);
+          }
+
+          return { receive_id, success: true };
+        })
+      );
+
+      // 统计结果
+      const successCount = results.filter(r => r.status === 'fulfilled').length;
+      const failedCount = results.filter(r => r.status === 'rejected').length;
+      const failedDetails = results
+        .filter(r => r.status === 'rejected')
+        .map((r: any) => r.reason?.message || '未知错误');
+
+      sendResponse(res, {
+        message: `批量发送完成: 成功 ${successCount} 条,失败 ${failedCount} 条`,
+        success_count: successCount,
+        failed_count: failedCount,
+        failed_details: failedDetails
+      });
+    } catch (error: any) {
+      return sendError(res, error.message || '批量发送失败', 500);
+    }
+  }));
+
+  return router;
+};

+ 222 - 0
api/module/feishu/token-manager.ts

@@ -0,0 +1,222 @@
+/**
+ * 飞书 Token 管理模块
+ * 负责获取、保存、刷新 tenant_access_token
+ */
+
+import { getFeishuConfig, validateConfig } from './config';
+
+// 飞书 API 端点
+const FEISHU_API_BASE = 'https://open.feishu.cn/open-apis';
+const TOKEN_API = `${FEISHU_API_BASE}/auth/v3/tenant_access_token/internal`;
+
+// Token 信息接口
+interface TokenInfo {
+  accessToken: string;
+  expireTime: number; // 过期时间戳(毫秒)
+}
+
+// 飞书 Token API 响应
+interface FeishuTokenResponse {
+  code: number;
+  msg: string;
+  tenant_access_token?: string;
+  expire?: number; // 有效期(秒)
+}
+
+/**
+ * Token 管理器类
+ */
+export class TokenManager {
+  private Parse: any;
+  private static instance: TokenManager;
+
+  constructor() {
+    // 使用全局的 Parse 实例
+    this.Parse = (globalThis as any).Parse;
+    if (!this.Parse) {
+      throw new Error('Parse is not initialized');
+    }
+  }
+
+  /**
+   * 获取单例实例
+   */
+  static getInstance(): TokenManager {
+    if (!TokenManager.instance) {
+      TokenManager.instance = new TokenManager();
+    }
+    return TokenManager.instance;
+  }
+
+  /**
+   * 从飞书 API 获取新的 access token
+   */
+  private async fetchTokenFromFeishu(): Promise<TokenInfo> {
+    const config = getFeishuConfig();
+
+    if (!validateConfig(config)) {
+      throw new Error('飞书配置无效:请设置 FEISHU_APP_ID 和 FEISHU_APP_SECRET 环境变量');
+    }
+
+    const response = await fetch(TOKEN_API, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({
+        app_id: config.app_id,
+        app_secret: config.app_secret
+      })
+    });
+
+    if (!response.ok) {
+      throw new Error(`获取飞书 token 失败: ${response.status} ${response.statusText}`);
+    }
+
+    const data: FeishuTokenResponse = await response.json();
+
+    if (data.code !== 0) {
+      throw new Error(`获取飞书 token 失败: ${data.msg}`);
+    }
+
+    if (!data.tenant_access_token || !data.expire) {
+      throw new Error('飞书 API 返回数据格式错误');
+    }
+
+    // 计算过期时间(提前 5 分钟过期,避免边界情况)
+    const expireTime = Date.now() + (data.expire - 300) * 1000;
+
+    return {
+      accessToken: data.tenant_access_token,
+      expireTime
+    };
+  }
+
+  /**
+   * 从数据库获取 Store 配置
+   */
+  private async getStoreConfig(): Promise<any> {
+    try {
+      const query = new this.Parse.Query('Store');
+      query.equalTo('objectId', 'config');
+      const store = await query.first({ useMasterKey: true });
+      return store;
+    } catch (error) {
+      console.error('获取 Store 配置失败:', error);
+      return null;
+    }
+  }
+
+  /**
+   * 从数据库读取 token
+   */
+  private async getTokenFromDatabase(): Promise<TokenInfo | null> {
+    try {
+      const store = await this.getStoreConfig();
+
+      if (!store) {
+        return null;
+      }
+
+      const config = store.get('config');
+      if (!config || !config.feishu || !config.feishu.accessToken || !config.feishu.expireTime) {
+        return null;
+      }
+
+      return {
+        accessToken: config.feishu.accessToken,
+        expireTime: config.feishu.expireTime
+      };
+    } catch (error) {
+      console.error('从数据库读取 token 失败:', error);
+      return null;
+    }
+  }
+
+  /**
+   * 保存 token 到数据库
+   */
+  private async saveTokenToDatabase(tokenInfo: TokenInfo): Promise<void> {
+    try {
+      let store = await this.getStoreConfig();
+
+      if (!store) {
+        // 创建新的 Store 记录
+        const StoreClass = this.Parse.Object.extend('Store');
+        store = new StoreClass();
+        store.id = 'config';
+      }
+
+      // 获取现有的 config
+      let config = store.get('config') || {};
+
+      // 更新 feishu 配置
+      config.feishu = {
+        accessToken: tokenInfo.accessToken,
+        expireTime: tokenInfo.expireTime
+      };
+
+      // 保存到数据库
+      store.set('config', config);
+      await store.save(null, { useMasterKey: true });
+
+      console.log('Token 已保存到数据库');
+    } catch (error) {
+      console.error('保存 token 到数据库失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 检查 token 是否过期
+   */
+  private isTokenExpired(tokenInfo: TokenInfo): boolean {
+    return Date.now() >= tokenInfo.expireTime;
+  }
+
+  /**
+   * 获取有效的 access token
+   * 优先从数据库读取,如果不存在或已过期则重新获取
+   */
+  async getAccessToken(): Promise<string> {
+    try {
+      // 1. 尝试从数据库读取
+      let tokenInfo = await this.getTokenFromDatabase();
+
+      // 2. 检查是否存在且未过期
+      if (tokenInfo && !this.isTokenExpired(tokenInfo)) {
+        console.log('使用数据库中的 token');
+        return tokenInfo.accessToken;
+      }
+
+      // 3. 从飞书 API 获取新 token
+      console.log('从飞书 API 获取新 token');
+      tokenInfo = await this.fetchTokenFromFeishu();
+
+      // 4. 保存到数据库
+      await this.saveTokenToDatabase(tokenInfo);
+
+      return tokenInfo.accessToken;
+    } catch (error) {
+      console.error('获取 access token 失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 强制刷新 token
+   */
+  async refreshToken(): Promise<string> {
+    console.log('强制刷新 token');
+    const tokenInfo = await this.fetchTokenFromFeishu();
+    await this.saveTokenToDatabase(tokenInfo);
+    return tokenInfo.accessToken;
+  }
+}
+
+/**
+ * 获取 Token 管理器实例
+ */
+export function getTokenManager(): TokenManager {
+  return TokenManager.getInstance();
+}

+ 53 - 0
api/module/feishu/types.ts

@@ -0,0 +1,53 @@
+// 飞书消息类型
+export type FeishuMsgType = 'text' | 'post' | 'interactive';
+
+// 接收者 ID 类型
+export type ReceiveIdType = 'open_id' | 'user_id' | 'union_id' | 'email' | 'chat_id';
+
+// 文本消息内容
+export interface TextContent {
+  text: string;
+}
+
+// 富文本消息内容
+export interface PostContent {
+  zh_cn?: {
+    title?: string;
+    content: Array<Array<{
+      tag: 'text' | 'a' | 'at';
+      text?: string;
+      href?: string;
+      user_id?: string;
+    }>>;
+  };
+}
+
+// 发送消息请求体(Webhook 方式)
+export interface SendMessageRequest {
+  webhook_url: string;
+  msg_type: FeishuMsgType;
+  content: TextContent | PostContent | any;
+}
+
+// 发送消息到个人请求体(API 方式)
+export interface SendToUserRequest {
+  receive_id_type: ReceiveIdType;
+  receive_id: string;
+  msg_type: FeishuMsgType;
+  content: any;
+}
+
+// 批量发送消息到多人请求体
+export interface SendToMultipleUsersRequest {
+  receive_id_type: ReceiveIdType;
+  receive_ids: string[];
+  msg_type: FeishuMsgType;
+  content: any;
+}
+
+// 飞书 API 响应
+export interface FeishuResponse {
+  code: number;
+  msg: string;
+  data?: any;
+}

+ 5 - 0
docs/task/202603091129.md

@@ -0,0 +1,5 @@
+> 我需要在api\module\feishu下新增飞书机器人推送接口,文档参考飞书开发机器人文档(参考https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot#478cb64f)。实现需求:前端请求接口发送固定格式信息推送到飞书个人、多人
+
+> 请分析官方文档中是否需要的请求头或者accesstoken之类的身份鉴权,如果需要请加入获取方式和请求中携带,请您封装好后根据过期时间,第一次保存到数据库Store表config<Object{"feishu":{"accessToken":"xxxx"}}>字段,后面每次请求获取优先从数据库获取,若过期重新获取且更新数据库。
+
+> 还缺少获取企业内成员接口和相关组织架构等企业内成员结构等接口,我需要这些接口才知道精准推送给谁

+ 5 - 1
server.ts

@@ -3,6 +3,7 @@ import express from 'express';
 import { readFileSync } from 'node:fs';
 const cors = require('cors');
 import { createTikHubCustomizeRoutes } from './modules/fmode-tikhub-server/src/mod';
+import { createFeishuRoutes } from './api/module/feishu';
 // import { createPinterestRouter } from './modules/fmode-brightdata-server/src/index';
 
 const Parse = require('parse/node');
@@ -65,7 +66,7 @@ app.get('/', (req: any, res: any) => {
     message: 'Fmode Server - Gongzuo API',
     version: '1.0.0',
     timestamp: new Date().toISOString(),
-    endpoints: ['/api', '/health', '/api/tikhub']
+    endpoints: ['/api', '/health', '/api/tikhub', '/api/feishu']
   });
 });
 
@@ -75,6 +76,9 @@ app.use('/api/tikhub', createTikHubCustomizeRoutes({
     apiKey: 'tKIbAsEM8X+GmE2vHqGW7D/ICwK1Q5V4viKFrWiPB6HholGdLFqZJmmyNw=='
   }));
 
+// 挂载飞书模块路由
+app.use('/api/feishu', createFeishuRoutes());
+
 // 挂载 Pinterest 模块路由
 // app.use('/api/pinterest', createPinterestRouter());