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) => (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 => { 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; };