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