routes.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import { Router, Request, Response, NextFunction } from 'express';
  2. import { SendMessageRequest, SendToUserRequest, SendToMultipleUsersRequest, FeishuResponse } from './types';
  3. import { getTokenManager } from './token-manager';
  4. // 统一响应格式
  5. const sendResponse = (res: Response, data: any) => {
  6. res.json({
  7. success: true,
  8. data,
  9. timestamp: new Date().toISOString()
  10. });
  11. };
  12. // 统一错误响应格式
  13. const sendError = (res: Response, message: string, statusCode: number = 400) => {
  14. res.status(statusCode).json({
  15. success: false,
  16. error: message,
  17. timestamp: new Date().toISOString()
  18. });
  19. };
  20. const asyncHandler = (fn: (req: Request, res: Response, next: NextFunction) => Promise<any>) =>
  21. (req: Request, res: Response, next: NextFunction) => {
  22. Promise.resolve(fn(req, res, next)).catch(next);
  23. };
  24. /**
  25. * 发送消息到飞书机器人
  26. */
  27. const sendToFeishu = async (webhook_url: string, msg_type: string, content: any): Promise<FeishuResponse> => {
  28. const response = await fetch(webhook_url, {
  29. method: 'POST',
  30. headers: {
  31. 'Content-Type': 'application/json',
  32. },
  33. body: JSON.stringify({
  34. msg_type,
  35. content
  36. })
  37. });
  38. if (!response.ok) {
  39. throw new Error(`飞书 API 请求失败: ${response.status} ${response.statusText}`);
  40. }
  41. return await response.json();
  42. };
  43. /**
  44. * 创建飞书路由
  45. */
  46. export const createFeishuRoutes = () => {
  47. const router = Router();
  48. // 发送消息到飞书
  49. // POST /send
  50. // Body: { webhook_url, msg_type, content }
  51. router.post('/send', asyncHandler(async (req, res) => {
  52. const { webhook_url, msg_type, content } = req.body as SendMessageRequest;
  53. // 参数验证
  54. if (!webhook_url) {
  55. return sendError(res, 'webhook_url 是必填项');
  56. }
  57. if (!msg_type) {
  58. return sendError(res, 'msg_type 是必填项');
  59. }
  60. if (!content) {
  61. return sendError(res, 'content 是必填项');
  62. }
  63. // 验证 webhook_url 格式
  64. if (!webhook_url.startsWith('https://open.feishu.cn/open-apis/bot/v2/hook/')) {
  65. return sendError(res, 'webhook_url 格式不正确');
  66. }
  67. try {
  68. const result = await sendToFeishu(webhook_url, msg_type, content);
  69. // 检查飞书 API 返回的状态
  70. if (result.code !== 0) {
  71. return sendError(res, `飞书消息发送失败: ${result.msg}`, 500);
  72. }
  73. sendResponse(res, {
  74. message: '消息发送成功',
  75. feishu_response: result
  76. });
  77. } catch (error: any) {
  78. return sendError(res, error.message || '消息发送失败', 500);
  79. }
  80. }));
  81. // 发送文本消息(简化接口)
  82. // POST /send/text
  83. // Body: { webhook_url, text }
  84. router.post('/send/text', asyncHandler(async (req, res) => {
  85. const { webhook_url, text } = req.body;
  86. if (!webhook_url) {
  87. return sendError(res, 'webhook_url 是必填项');
  88. }
  89. if (!text) {
  90. return sendError(res, 'text 是必填项');
  91. }
  92. try {
  93. const result = await sendToFeishu(webhook_url, 'text', { text });
  94. if (result.code !== 0) {
  95. return sendError(res, `飞书消息发送失败: ${result.msg}`, 500);
  96. }
  97. sendResponse(res, {
  98. message: '文本消息发送成功',
  99. feishu_response: result
  100. });
  101. } catch (error: any) {
  102. return sendError(res, error.message || '消息发送失败', 500);
  103. }
  104. }));
  105. // 发送富文本消息(支持 @用户)
  106. // POST /send/post
  107. // Body: { webhook_url, title, content }
  108. router.post('/send/post', asyncHandler(async (req, res) => {
  109. const { webhook_url, title, content } = req.body;
  110. if (!webhook_url) {
  111. return sendError(res, 'webhook_url 是必填项');
  112. }
  113. if (!content) {
  114. return sendError(res, 'content 是必填项');
  115. }
  116. try {
  117. const postContent = {
  118. zh_cn: {
  119. title: title || '',
  120. content
  121. }
  122. };
  123. const result = await sendToFeishu(webhook_url, 'post', postContent);
  124. if (result.code !== 0) {
  125. return sendError(res, `飞书消息发送失败: ${result.msg}`, 500);
  126. }
  127. sendResponse(res, {
  128. message: '富文本消息发送成功',
  129. feishu_response: result
  130. });
  131. } catch (error: any) {
  132. return sendError(res, error.message || '消息发送失败', 500);
  133. }
  134. }));
  135. // ==========================================
  136. // 使用 Access Token 的接口(发送消息到个人)
  137. // ==========================================
  138. /**
  139. * 发送消息到个人(使用 Access Token)
  140. * POST /message/send
  141. * Body: { receive_id_type, receive_id, msg_type, content }
  142. */
  143. router.post('/message/send', asyncHandler(async (req, res) => {
  144. const { receive_id_type, receive_id, msg_type, content } = req.body as SendToUserRequest;
  145. // 参数验证
  146. if (!receive_id_type) {
  147. return sendError(res, 'receive_id_type 是必填项');
  148. }
  149. if (!receive_id) {
  150. return sendError(res, 'receive_id 是必填项');
  151. }
  152. if (!msg_type) {
  153. return sendError(res, 'msg_type 是必填项');
  154. }
  155. if (!content) {
  156. return sendError(res, 'content 是必填项');
  157. }
  158. try {
  159. // 获取 access token
  160. const tokenManager = getTokenManager();
  161. const accessToken = await tokenManager.getAccessToken();
  162. // 调用飞书 API
  163. const response = await fetch(`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receive_id_type}`, {
  164. method: 'POST',
  165. headers: {
  166. 'Content-Type': 'application/json',
  167. 'Authorization': `Bearer ${accessToken}`
  168. },
  169. body: JSON.stringify({
  170. receive_id,
  171. msg_type,
  172. content: typeof content === 'string' ? content : JSON.stringify(content)
  173. })
  174. });
  175. if (!response.ok) {
  176. throw new Error(`飞书 API 请求失败: ${response.status} ${response.statusText}`);
  177. }
  178. const result: FeishuResponse = await response.json();
  179. if (result.code !== 0) {
  180. return sendError(res, `飞书消息发送失败: ${result.msg}`, 500);
  181. }
  182. sendResponse(res, {
  183. message: '消息发送成功',
  184. feishu_response: result
  185. });
  186. } catch (error: any) {
  187. return sendError(res, error.message || '消息发送失败', 500);
  188. }
  189. }));
  190. /**
  191. * 发送文本消息到个人(简化版)
  192. * POST /message/send/text
  193. * Body: { receive_id_type, receive_id, text }
  194. */
  195. router.post('/message/send/text', asyncHandler(async (req, res) => {
  196. const { receive_id_type, receive_id, text } = req.body;
  197. if (!receive_id_type) {
  198. return sendError(res, 'receive_id_type 是必填项');
  199. }
  200. if (!receive_id) {
  201. return sendError(res, 'receive_id 是必填项');
  202. }
  203. if (!text) {
  204. return sendError(res, 'text 是必填项');
  205. }
  206. try {
  207. const tokenManager = getTokenManager();
  208. const accessToken = await tokenManager.getAccessToken();
  209. const response = await fetch(`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receive_id_type}`, {
  210. method: 'POST',
  211. headers: {
  212. 'Content-Type': 'application/json',
  213. 'Authorization': `Bearer ${accessToken}`
  214. },
  215. body: JSON.stringify({
  216. receive_id,
  217. msg_type: 'text',
  218. content: JSON.stringify({ text })
  219. })
  220. });
  221. if (!response.ok) {
  222. throw new Error(`飞书 API 请求失败: ${response.status} ${response.statusText}`);
  223. }
  224. const result: FeishuResponse = await response.json();
  225. if (result.code !== 0) {
  226. return sendError(res, `飞书消息发送失败: ${result.msg}`, 500);
  227. }
  228. sendResponse(res, {
  229. message: '文本消息发送成功',
  230. feishu_response: result
  231. });
  232. } catch (error: any) {
  233. return sendError(res, error.message || '消息发送失败', 500);
  234. }
  235. }));
  236. /**
  237. * 批量发送消息到多人
  238. * POST /message/send/batch
  239. * Body: { receive_id_type, receive_ids, msg_type, content }
  240. */
  241. router.post('/message/send/batch', asyncHandler(async (req, res) => {
  242. const { receive_id_type, receive_ids, msg_type, content } = req.body as SendToMultipleUsersRequest;
  243. if (!receive_id_type) {
  244. return sendError(res, 'receive_id_type 是必填项');
  245. }
  246. if (!receive_ids || !Array.isArray(receive_ids) || receive_ids.length === 0) {
  247. return sendError(res, 'receive_ids 必须是非空数组');
  248. }
  249. if (!msg_type) {
  250. return sendError(res, 'msg_type 是必填项');
  251. }
  252. if (!content) {
  253. return sendError(res, 'content 是必填项');
  254. }
  255. try {
  256. const tokenManager = getTokenManager();
  257. const accessToken = await tokenManager.getAccessToken();
  258. // 批量发送消息
  259. const results = await Promise.allSettled(
  260. receive_ids.map(async (receive_id) => {
  261. const response = await fetch(`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receive_id_type}`, {
  262. method: 'POST',
  263. headers: {
  264. 'Content-Type': 'application/json',
  265. 'Authorization': `Bearer ${accessToken}`
  266. },
  267. body: JSON.stringify({
  268. receive_id,
  269. msg_type,
  270. content: typeof content === 'string' ? content : JSON.stringify(content)
  271. })
  272. });
  273. if (!response.ok) {
  274. throw new Error(`发送到 ${receive_id} 失败: ${response.status}`);
  275. }
  276. const result: FeishuResponse = await response.json();
  277. if (result.code !== 0) {
  278. throw new Error(`发送到 ${receive_id} 失败: ${result.msg}`);
  279. }
  280. return { receive_id, success: true };
  281. })
  282. );
  283. // 统计结果
  284. const successCount = results.filter(r => r.status === 'fulfilled').length;
  285. const failedCount = results.filter(r => r.status === 'rejected').length;
  286. const failedDetails = results
  287. .filter(r => r.status === 'rejected')
  288. .map((r: any) => r.reason?.message || '未知错误');
  289. sendResponse(res, {
  290. message: `批量发送完成: 成功 ${successCount} 条,失败 ${failedCount} 条`,
  291. success_count: successCount,
  292. failed_count: failedCount,
  293. failed_details: failedDetails
  294. });
  295. } catch (error: any) {
  296. return sendError(res, error.message || '批量发送失败', 500);
  297. }
  298. }));
  299. return router;
  300. };