v2 版本 - 飞书文件上传功能修复完成

主要修复:
1. 使用飞书 SDK im.messageResource.get API 下载文件(和 OpenClaw 一致)
2. 修复 sendMessage 方法,自动判断 receive_id_type(oc_=chat_id, ou_=open_id)
3. 修复 sendCard 方法,传递正确的 receive_id_type 参数
4. 修复事件类型识别,支持飞书 v2 schema(header.event_type)
5. 添加临时文件清理机制(每小时清理 1 小时前的文件)
6. 完善卡片交互(确认上传/取消按钮)
7. 完善错误处理和日志记录

功能:
-  飞书文件接收和卡片回复
-  卡片交互(确认/取消)
-  七牛云上传(支持多存储桶)
-  CDN 自动刷新
-  临时文件自动清理
-  配置管理命令(/config)

配置文件:
- config/qiniu-config.json - 七牛云配置
- .env - 飞书应用配置
This commit is contained in:
饭团
2026-03-06 08:38:52 +08:00
parent 3769d164b1
commit f7776aaf69
3 changed files with 369 additions and 305 deletions

View File

@@ -1,11 +1,18 @@
/**
* 飞书 API 封装
* 飞书 API 封装 v2 - 使用飞书 SDK
*/
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const https = require('https');
const lark = require('@larksuiteoapi/node-sdk');
// 日志函数
function log(...args) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [FeishuAPI]`, ...args);
}
class FeishuAPI {
constructor() {
@@ -14,6 +21,12 @@ class FeishuAPI {
this.baseURL = 'https://open.feishu.cn/open-apis';
this.tokenCache = null;
this.tokenExpiry = 0;
// 创建飞书 SDK 客户端
this.client = new lark.Client({
appId: this.appId,
appSecret: this.appSecret
});
}
// 获取访问令牌
@@ -50,16 +63,26 @@ class FeishuAPI {
}
// 发送文本消息
async sendMessage(chatId, payload) {
async sendMessage(chatId, payload, receiveIdType = 'chat_id') {
const token = await this.getAccessToken();
try {
// 判断 receive_id_typeoc_开头的是 chat_idou_开头的是 open_id
let idType = receiveIdType;
if (chatId.startsWith('oc_')) {
idType = 'chat_id';
} else if (chatId.startsWith('ou_')) {
idType = 'open_id';
}
log('发送消息到 chatId:', chatId, 'receive_id_type:', idType, 'msg_type:', payload.msg_type);
const response = await axios.post(
`${this.baseURL}/im/v1/messages`,
`${this.baseURL}/im/v1/messages?receive_id_type=${idType}`,
{
receive_id: chatId,
msg_type: payload.msg_type,
content: payload.content
content: typeof payload.content === 'string' ? payload.content : JSON.stringify(payload.content)
},
{
headers: {
@@ -69,56 +92,63 @@ class FeishuAPI {
}
);
log('消息发送响应:', response.data.code, response.data.msg || 'ok');
if (response.data.code !== 0) {
throw new Error(`发送消息失败:${response.data.msg}`);
}
return response.data;
} catch (error) {
log('消息发送错误:', error.message);
throw new Error(`飞书消息发送失败:${error.message}`);
}
}
// 发送卡片消息
async sendCard(chatId, card) {
// 判断 receive_id_type
const receiveIdType = chatId.startsWith('oc_') ? 'chat_id' : 'open_id';
return this.sendMessage(chatId, {
msg_type: 'interactive',
content: JSON.stringify(card)
});
}, receiveIdType);
}
// 下载文件
async downloadFile(fileKey) {
const token = await this.getAccessToken();
// 下载文件 - 使用飞书 SDK 的 messageResource APIOpenClaw 使用的方式)
async downloadFile(fileKey, messageId, chatId) {
const tempDir = path.join(process.cwd(), 'temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
const tempFile = path.join(tempDir, `feishu_${Date.now()}_${fileKey}`);
const tempFile = path.join(tempDir, `feishu_${Date.now()}_${path.basename(fileKey)}`);
return new Promise((resolve, reject) => {
const url = `${this.baseURL}/im/v1/files/${fileKey}/download`;
log('下载文件:', fileKey, 'messageId:', messageId);
try {
// 使用 SDK 的 im.messageResource.get 方法
const result = await this.client.im.messageResource.get({
path: {
message_id: messageId,
file_key: fileKey
},
params: {
type: 'file'
}
});
https.get(url, {
headers: {
'Authorization': `Bearer ${token}`
}
}, (res) => {
if (res.statusCode !== 200) {
reject(new Error(`下载失败:${res.statusCode}`));
return;
}
// SDK 返回一个带有 writeFile 方法的对象
await result.writeFile(tempFile);
log('文件下载完成:', tempFile, fs.statSync(tempFile).size, 'bytes');
return tempFile;
const file = fs.createWriteStream(tempFile);
res.pipe(file);
file.on('finish', () => {
file.close();
resolve(tempFile);
});
}).on('error', reject);
});
} catch (error) {
log('下载错误:', error.message);
throw new Error(`飞书文件下载失败:${error.message}`);
}
}
// 回复消息