✅ 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:
@@ -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_type:oc_开头的是 chat_id,ou_开头的是 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 API(OpenClaw 使用的方式)
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 回复消息
|
||||
|
||||
Reference in New Issue
Block a user