feat: 支持 HTTP 回调和 WebSocket 长连接两种模式

新增功能:
- 通过 FEISHU_MODE 配置切换模式 (http/websocket)
- 安装飞书 SDK (@larksuiteoapi/node-sdk)
- WebSocket 模式支持内网部署(无需公网 IP)
- 新增 WEBSOCKET.md 配置指南

更新:
- README.md 添加两种模式说明
- .env.example 添加 FEISHU_MODE 配置
- 健康检查返回当前模式信息
This commit is contained in:
饭团
2026-03-05 16:42:17 +08:00
parent 5294bf49d8
commit 3769d164b1
6 changed files with 499 additions and 11 deletions

View File

@@ -4,6 +4,11 @@ FEISHU_APP_SECRET=xxxxxxxxxxxxxx
FEISHU_VERIFICATION_TOKEN=xxxxxxxxxxxxxx
FEISHU_ENCRYPT_KEY=xxxxxxxxxxxxxx
# 飞书事件接收模式http (HTTP 回调) 或 websocket (WebSocket 长连接)
# HTTP 回调:需要公网 IP/域名,配置简单
# WebSocket不需要公网 IP内网可用需要飞书 SDK
FEISHU_MODE=http
# 七牛云配置(可选,也可通过卡片配置)
QINIU_ACCESS_KEY=YOUR_ACCESS_KEY
QINIU_SECRET_KEY=YOUR_SECRET_KEY

View File

@@ -46,13 +46,29 @@
### 3. 配置事件订阅
**方式一HTTP 回调(默认)**
1. 进入"事件订阅"页面
2. 开启"启用事件订阅"
3. 填写请求地址:`https://your-domain.com/feishu/event`
2. 选择 **"HTTP 回调"** 方式
3. 开启"启用事件订阅"
4. 填写请求地址:`https://your-domain.com/feishu/event`
5. 配置订阅事件:
- `im.message.receive_v1` - 接收消息
6. 保存后复制 Verification Token 和 Encrypt Key
**方式二WebSocket 长连接(内网推荐)**
1. 进入"事件订阅"页面
2. 选择 **"WebSocket 长连接"** 方式
3. 开启"启用事件订阅"
4. 配置订阅事件:
- `im.message.receive_v1` - 接收消息
- `im.message.group_at_msg.receive_v1` - 群组 @ 消息(可选)
5. 保存后复制 Verification Token 和 Encrypt Key
6.`.env` 中设置 `FEISHU_MODE=websocket`
> **💡 提示:** WebSocket 模式不需要公网 IP适合内网部署。
>
> 详细配置请查看 [`WEBSOCKET.md`](./WEBSOCKET.md)
### 4. 配置机器人
@@ -168,6 +184,11 @@ FEISHU_APP_SECRET=xxxxxxxxxxxxxx
FEISHU_VERIFICATION_TOKEN=xxxxxxxxxxxxxx
FEISHU_ENCRYPT_KEY=xxxxxxxxxxxxxx
# 飞书事件接收模式http (HTTP 回调) 或 websocket (WebSocket 长连接)
# - http: 需要公网 IP/域名,配置简单
# - websocket: 不需要公网 IP内网可用
FEISHU_MODE=http
# 七牛云配置(可选,也可通过卡片配置)
QINIU_ACCESS_KEY=xxxxxxxxxxxxxx
QINIU_SECRET_KEY=xxxxxxxxxxxxxx
@@ -241,7 +262,8 @@ qiniu-feishu-bot/
├── README.md # 项目说明
├── DEPLOY.md # 详细部署指南Linux/macOS/Windows
├── WINDOWS.md # Windows 专用指南
── NGINX.md # Nginx 反向代理部署指南
── NGINX.md # Nginx 反向代理部署指南
└── WEBSOCKET.md # WebSocket 长连接模式配置指南
```
---

213
WEBSOCKET.md Normal file
View File

@@ -0,0 +1,213 @@
# WebSocket 长连接模式配置指南
## 📡 两种模式对比
| 特性 | HTTP 回调 | WebSocket 长连接 |
|------|----------|-----------------|
| 公网 IP | ✅ 需要 | ❌ 不需要 |
| 域名 | ✅ 需要 | ❌ 不需要 |
| HTTPS | ✅ 推荐 | ❌ 不需要 |
| 内网部署 | ❌ 困难 | ✅ 简单 |
| 实时性 | 好 | 更好 |
| 配置复杂度 | 简单 | 中等 |
---
## 🚀 配置 WebSocket 模式
### 1⃣ 飞书开放平台配置
1. 访问 https://open.feishu.cn/
2. 进入你的应用管理页面
3. 点击"事件订阅"
4. **选择"WebSocket 长连接"方式**
5. 开启"启用事件订阅"
6. 添加订阅事件:
- `im.message.receive_v1` - 接收消息
7. 保存并复制:
- Verification Token
- Encrypt Key
### 2⃣ 修改配置文件
编辑 `.env` 文件:
```env
# 飞书配置
FEISHU_APP_ID=cli_xxxxxxxxxx
FEISHU_APP_SECRET=xxxxxxxxxxxxxx
FEISHU_VERIFICATION_TOKEN=xxxxxxxxxxxxxx
FEISHU_ENCRYPT_KEY=xxxxxxxxxxxxxx
# 设置为 WebSocket 模式
FEISHU_MODE=websocket
# 七牛云配置
QINIU_ACCESS_KEY=xxxxxxxxxxxxxx
QINIU_SECRET_KEY=xxxxxxxxxxxxxx
QINIU_BUCKET=your-bucket-name
QINIU_REGION=z0
QINIU_DOMAIN=https://your-cdn.com
# 服务配置
PORT=3030
NODE_ENV=production
```
### 3⃣ 安装依赖
```bash
cd /path/to/qiniu-feishu-bot
npm install
```
### 4⃣ 启动服务
```bash
# 使用 PM2
pm2 restart qiniu-bot
# 或直接启动
npm start
```
### 5⃣ 查看日志
```bash
# PM2 日志
pm2 logs qiniu-bot
# 应该看到:
# 🚀 七牛云上传机器人启动 (WebSocket 长连接模式)
# 📡 WebSocket 已启动
# ✅ WebSocket 连接成功
```
---
## 🔧 故障排查
### WebSocket 连接失败
**检查配置:**
```bash
# 验证 .env 配置
cat .env | grep FEISHU
# 确认 FEISHU_MODE=websocket
```
**查看日志:**
```bash
pm2 logs qiniu-bot --lines 100
```
**常见错误:**
1. **验证失败**
- 检查 Verification Token 是否正确
- 检查 Encrypt Key 是否正确
2. **连接被拒绝**
- 检查防火墙是否允许出站连接
- 确认服务器能访问外网
3. **认证失败**
- 检查 App ID 和 App Secret
---
## 📊 监控 WebSocket 状态
### 健康检查
```bash
curl http://localhost:3030/health
```
返回:
```json
{
"status": "ok",
"timestamp": "2026-03-05T08:00:00.000Z",
"mode": "websocket",
"port": 3030
}
```
### 连接状态
查看 PM2 日志中的连接状态:
```bash
pm2 logs qiniu-bot | grep -E "(WebSocket|连接|open|close)"
```
---
## 🔄 切换模式
### 从 HTTP 切换到 WebSocket
```bash
# 1. 修改 .env
nano .env
# 设置 FEISHU_MODE=websocket
# 2. 重启服务
pm2 restart qiniu-bot
# 3. 验证
pm2 logs qiniu-bot
```
### 从 WebSocket 切换到 HTTP
```bash
# 1. 修改 .env
nano .env
# 设置 FEISHU_MODE=http
# 2. 重启服务
pm2 restart qiniu-bot
# 3. 在飞书开放平台改回 HTTP 回调方式
```
---
## 💡 最佳实践
### WebSocket 模式适用场景
- ✅ 内网服务器(无公网 IP
- ✅ 开发测试环境
- ✅ 不想配置域名和 HTTPS
- ✅ 需要更好的实时性
### HTTP 回调模式适用场景
- ✅ 云服务器(有公网 IP
- ✅ 生产环境
- ✅ 已有域名和 HTTPS
- ✅ 需要更可控的连接管理
---
## 📝 注意事项
1. **WebSocket 长连接会保持在线状态**
- 确保服务器网络稳定
- 断线会自动重连5 秒间隔)
2. **两种方式不能同时使用**
- 通过 `FEISHU_MODE` 配置选择
- 飞书开放平台也要对应配置
3. **健康检查始终可用**
- 无论哪种模式,`/health` 端点都工作
- 可用于监控服务状态
---
**🍙 祝你使用愉快!**

164
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@larksuiteoapi/node-sdk": "^1.5.0",
"axios": "^1.6.0",
"dotenv": "^16.3.1",
"express": "^4.18.2"
@@ -17,6 +18,94 @@
"node": ">=18.0.0"
}
},
"node_modules/@larksuiteoapi/node-sdk": {
"version": "1.59.0",
"resolved": "https://registry.npmjs.org/@larksuiteoapi/node-sdk/-/node-sdk-1.59.0.tgz",
"integrity": "sha512-sBpkruTvZDOxnVtoTbepWKRX0j1Y1ZElQYu0x7+v088sI9pcpbVp6ZzCGn62dhrKPatzNyCJyzYCPXPYQWccrA==",
"license": "MIT",
"dependencies": {
"axios": "~1.13.3",
"lodash.identity": "^3.0.0",
"lodash.merge": "^4.6.2",
"lodash.pickby": "^4.6.0",
"protobufjs": "^7.2.6",
"qs": "^6.14.2",
"ws": "^8.19.0"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@types/node": {
"version": "25.3.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz",
"integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -563,6 +652,30 @@
"node": ">= 0.10"
}
},
"node_modules/lodash.identity": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash.identity/-/lodash.identity-3.0.0.tgz",
"integrity": "sha512-AupTIzdLQxJS5wIYUQlgGyk2XRTfGXA+MCghDHqZk0pzUNYvd3EESS6dkChNauNYVIutcb0dfHw1ri9Q1yPV8Q==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"license": "MIT"
},
"node_modules/lodash.pickby": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz",
"integrity": "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==",
"license": "MIT"
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -686,6 +799,30 @@
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/protobufjs": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -924,6 +1061,12 @@
"node": ">= 0.6"
}
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -950,6 +1093,27 @@
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@@ -18,6 +18,7 @@
"author": "饭团 🍙",
"license": "MIT",
"dependencies": {
"@larksuiteoapi/node-sdk": "^1.5.0",
"axios": "^1.6.0",
"dotenv": "^16.3.1",
"express": "^4.18.2"

View File

@@ -8,6 +8,7 @@
* 2. 支持交互式卡片上传
* 3. 支持命令触发上传
* 4. 配置管理
* 5. 支持 HTTP 回调和 WebSocket 长连接两种模式
*/
require('dotenv').config();
@@ -21,8 +22,14 @@ const { QiniuUploader } = require('./qiniu-uploader');
const { UploadCard } = require('./cards/upload-card');
const { ConfigCard } = require('./cards/config-card');
// 飞书 SDKWebSocket 模式)
const { Api, eventSubscription } = require('@larksuiteoapi/node-sdk');
const app = express();
const PORT = process.env.PORT || 3000;
const PORT = process.env.PORT || 3030;
// 运行模式:'http' 或 'websocket'
const MODE = (process.env.FEISHU_MODE || 'http').toLowerCase();
// 中间件
app.use(express.json());
@@ -299,13 +306,89 @@ app.post('/feishu/event', handleFeishuEvent);
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
mode: MODE,
port: PORT
});
});
// ============ 启动服务 ============
app.listen(PORT, () => {
log(`🚀 七牛云上传机器人启动`);
log(`📍 端口:${PORT}`);
log(`🔗 事件地址https://your-domain.com/feishu/event`);
});
function startHTTPMode() {
app.listen(PORT, () => {
log(`🚀 七牛云上传机器人启动 (HTTP 回调模式)`);
log(`📍 端口:${PORT}`);
log(`🔗 事件地址http://your-domain.com:${PORT}/feishu/event`);
log(`💡 提示:在飞书开放平台配置事件订阅地址为上述地址`);
});
}
function startWebSocketMode() {
log(`🚀 七牛云上传机器人启动 (WebSocket 长连接模式)`);
log(`💡 提示:在飞书开放平台选择 "WebSocket 长连接" 方式`);
// 创建飞书客户端
const client = new Api({
appId: process.env.FEISHU_APP_ID,
appSecret: process.env.FEISHU_APP_SECRET,
});
// 创建 WebSocket 长连接
const ws = eventSubscription({
appId: process.env.FEISHU_APP_ID,
appSecret: process.env.FEISHU_APP_SECRET,
encryptKey: process.env.FEISHU_ENCRYPT_KEY,
verificationToken: process.env.FEISHU_VERIFICATION_TOKEN,
logLevel: 'info',
});
// 监听消息事件
ws.on('im.message.receive_v1', async (data) => {
log('收到消息事件');
await handleMessage(data);
});
// 监听连接状态
ws.on('open', () => {
log('✅ WebSocket 连接成功');
});
ws.on('close', () => {
log('❌ WebSocket 连接关闭5 秒后重连...');
setTimeout(() => {
try {
ws.start();
} catch (e) {
log('重连失败:', e.message);
}
}, 5000);
});
ws.on('error', (error) => {
log('❌ WebSocket 错误:', error.message);
});
// 启动 WebSocket 连接
try {
ws.start();
log('📡 WebSocket 已启动');
} catch (error) {
log('❌ WebSocket 启动失败:', error.message);
log('💡 请检查飞书配置是否正确');
}
// HTTP 服务器仍然运行(用于健康检查)
app.listen(PORT, () => {
log(`📍 健康检查端口:${PORT}`);
log(`🔗 健康检查地址http://localhost:${PORT}/health`);
});
}
// 根据配置启动对应模式
if (MODE === 'websocket') {
startWebSocketMode();
} else {
startHTTPMode();
}