/** * 飞书消息发送模块 * 支持两种方式: * 1. 应用身份(推荐):使用 App ID/App Secret 获取 tenant_token 调用开放 API * 2. Webhook 方式:直接调用自定义机器人 Webhook(向后兼容) */ import { createHmac } from 'crypto'; // 配置:从环境变量或 TOOLS.md 读取 const FEISHU_APP_ID = process.env.FEISHU_APP_ID || 'cli_a95093447cb85cdd'; const FEISHU_APP_SECRET = process.env.FEISHU_APP_SECRET || 'd17CeffVfOnTkQo8LIP7hbhOQwSPv7Jv'; const FEISHU_WEBHOOK = process.env.FEISHU_WEBHOOK || 'https://open.feishu.cn/open-apis/bot/v2/hook/58321c74-5881-4f41-bcd4-85f4d7c5b3c1'; const FEISHU_WEBHOOK_SECRET = process.env.FEISHU_WEBHOOK_SECRET || 'UgCdzrcci4s9YS1GSAHt4e'; /** * 使用应用身份获取 tenant_access_token */ async function getTenantToken(): Promise { const res = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ app_id: FEISHU_APP_ID, app_secret: FEISHU_APP_SECRET }), }); const data = await res.json(); if (data.code !== 0) { throw new Error(`Failed to get tenant token: ${data.msg}`); } return data.tenant_access_token; } /** * 生成 Webhook 签名 */ function generateWebhookSign(timestamp: number, secret: string): string { const stringToSign = `${timestamp}\n${secret}`; const hmac = createHmac('sha256', stringToSign); return hmac.digest('base64'); } /** * 发送飞书文本消息(支持两种方式) */ export interface FeishuMessageOptions { /** 消息内容 */ text: string; /** 接收者 ID(open_id、user_id、chat_id 等) */ receiveId: string; /** 接收者 ID 类型:open_id、user_id、chat_id、email */ receiveIdType?: 'open_id' | 'user_id' | 'chat_id' | 'email'; /** 是否使用应用身份(默认 true),若为 false 则使用 webhook */ useApp?: boolean; } /** * 发送文本消息 */ export async function sendFeishuMessage(options: FeishuMessageOptions): Promise<{ ok: boolean; code: number; msg: string }> { const { text, receiveId, receiveIdType = 'open_id', useApp = true } = options; if (useApp) { // 使用应用身份 const token = await getTenantToken(); const url = `https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receiveIdType}`; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ receive_id: receiveId, msg_type: 'text', content: JSON.stringify({ text }), }), }); const data = await res.json(); return { ok: data.code === 0, code: data.code, msg: data.msg, }; } else { // 使用 webhook const timestamp = Math.floor(Date.now() / 1000); const body: Record = { msg_type: 'text', content: { text }, }; if (FEISHU_WEBHOOK_SECRET) { body.timestamp = String(timestamp); body.sign = generateWebhookSign(timestamp, FEISHU_WEBHOOK_SECRET); } const res = await fetch(FEISHU_WEBHOOK, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await res.json(); return { ok: data.code === 0 || data.StatusCode === 0, code: data.code ?? data.StatusCode ?? -1, msg: data.msg ?? data.StatusMessage ?? '', }; } } /** * 发送项目创建通知 */ export async function notifyProjectCreated(projectName: string, goal: string, receiveId: string, receiveIdType: 'open_id' | 'user_id' = 'open_id'): Promise { await sendFeishuMessage({ text: `🚀 新项目已创建\n\n项目:${projectName}\n目标:${goal}\n\n请及时查看并确认项目章程。`, receiveId, receiveIdType, useApp: true, }); } /** * 发送里程碑提醒 */ export async function notifyMilestoneReminder(milestoneName: string, targetDate: string, receiveId: string, receiveIdType: 'open_id' | 'user_id' = 'open_id'): Promise { await sendFeishuMessage({ text: `⏰ 里程碑提醒\n\n里程碑「${milestoneName}」即将到期\n目标日期:${targetDate}\n\n请确认进度是否正常。`, receiveId, receiveIdType, useApp: true, }); } /** * 发送风险预警 */ export async function notifyRiskAlert(riskDesc: string, priority: number, receiveId: string, receiveIdType: 'open_id' | 'user_id' = 'open_id'): Promise { const level = priority >= 15 ? '🔴 高' : priority >= 10 ? '🟡 中' : '🟢 低'; await sendFeishuMessage({ text: `⚠️ 风险预警\n\n风险:${riskDesc}\n优先级:${level}(${priority}分)\n\n请评估并制定应对措施。`, receiveId, receiveIdType, useApp: true, }); } /** * 发送决策请求(生成卡片消息) */ export interface DecisionCardOptions { title: string; description: string; options: Array<{ key: string; label: string; style?: 'primary' | 'danger' | 'default' }>; } /** * 生成飞书卡片消息 */ export function buildDecisionCard(card: DecisionCardOptions): Record { const elements: Record[] = [ { tag: 'div', text: { tag: 'plain_text', content: card.description }, }, ]; const actions = card.options.map((opt) => ({ tag: 'button', text: { tag: 'plain_text', content: opt.label }, type: opt.style === 'danger' ? 'danger' : opt.style === 'primary' ? 'primary' : 'default', value: { action: opt.key }, })); elements.push({ tag: 'action', actions }); return { msg_type: 'interactive', card: { header: { title: { tag: 'plain_text', content: card.title }, template: 'blue', }, elements, }, }; } /** * 发送决策卡片 */ export async function sendDecisionCard(card: DecisionCardOptions, receiveId: string, receiveIdType: 'open_id' | 'user_id' = 'open_id'): Promise<{ ok: boolean; code: number; msg: string }> { const token = await getTenantToken(); const url = `https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receiveIdType}`; const cardContent = { header: { title: { tag: 'plain_text', content: card.title }, template: 'blue', }, elements: [ { tag: 'div', text: { tag: 'plain_text', content: card.description } }, { tag: 'action', actions: card.options.map((opt) => ({ tag: 'button', text: { tag: 'plain_text', content: opt.label }, type: opt.style === 'danger' ? 'danger' : opt.style === 'primary' ? 'primary' : 'default', value: { action: opt.key }, })), }, ], }; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ receive_id: receiveId, msg_type: 'interactive', content: JSON.stringify(cardContent), }), }); const data = await res.json(); return { ok: data.code === 0, code: data.code, msg: data.msg, }; }