/** * 飞书 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() { this.appId = process.env.FEISHU_APP_ID; this.appSecret = process.env.FEISHU_APP_SECRET; 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 }); } // 获取访问令牌 async getAccessToken() { if (this.tokenCache && Date.now() < this.tokenExpiry) { return this.tokenCache; } try { const response = await axios.post( `${this.baseURL}/auth/v3/tenant_access_token/internal`, { app_id: this.appId, app_secret: this.appSecret }, { headers: { 'Content-Type': 'application/json' } } ); const { tenant_access_token, expire } = response.data; if (response.data.code !== 0) { throw new Error(`获取 token 失败:${response.data.msg}`); } this.tokenCache = tenant_access_token; this.tokenExpiry = Date.now() + (expire - 300) * 1000; // 提前 5 分钟过期 return tenant_access_token; } catch (error) { throw new Error(`飞书 API 错误:${error.message}`); } } // 发送文本消息 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?receive_id_type=${idType}`, { receive_id: chatId, msg_type: payload.msg_type, content: typeof payload.content === 'string' ? payload.content : JSON.stringify(payload.content) }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); 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); } // 下载文件 - 使用飞书 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()}_${path.basename(fileKey)}`); 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' } }); // SDK 返回一个带有 writeFile 方法的对象 await result.writeFile(tempFile); log('文件下载完成:', tempFile, fs.statSync(tempFile).size, 'bytes'); return tempFile; } catch (error) { log('下载错误:', error.message); throw new Error(`飞书文件下载失败:${error.message}`); } } // 回复消息 async replyMessage(messageId, payload) { const token = await this.getAccessToken(); try { const response = await axios.post( `${this.baseURL}/im/v1/messages`, { reply_id: messageId, msg_type: payload.msg_type, content: payload.content }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); return response.data; } catch (error) { throw new Error(`飞书回复失败:${error.message}`); } } } module.exports = { FeishuAPI };