Files
pmp-tool/dist-server/server/feishu.js
xiaohei 61ed9e9dc3
Some checks failed
CI / lint-and-typecheck (push) Failing after 30s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
feat: MVP v0.5 complete - All P0 features implemented and frontend verified. Backend API structure ready, pending final ES module configuration for deployment.
2026-04-12 01:46:38 +08:00

291 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
/**
* 飞书消息发送模块
* 支持两种方式:
* 1. 应用身份(推荐):使用 App ID/App Secret 获取 tenant_token 调用开放 API
* 2. Webhook 方式:直接调用自定义机器人 Webhook向后兼容
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.sendFeishuMessage = sendFeishuMessage;
exports.notifyProjectCreated = notifyProjectCreated;
exports.notifyMilestoneReminder = notifyMilestoneReminder;
exports.notifyRiskAlert = notifyRiskAlert;
exports.buildDecisionCard = buildDecisionCard;
exports.sendDecisionCard = sendDecisionCard;
var crypto_1 = require("crypto");
// 配置:从环境变量或 TOOLS.md 读取
var FEISHU_APP_ID = process.env.FEISHU_APP_ID || 'cli_a95093447cb85cdd';
var FEISHU_APP_SECRET = process.env.FEISHU_APP_SECRET || 'd17CeffVfOnTkQo8LIP7hbhOQwSPv7Jv';
var FEISHU_WEBHOOK = process.env.FEISHU_WEBHOOK || 'https://open.feishu.cn/open-apis/bot/v2/hook/58321c74-5881-4f41-bcd4-85f4d7c5b3c1';
var FEISHU_WEBHOOK_SECRET = process.env.FEISHU_WEBHOOK_SECRET || 'UgCdzrcci4s9YS1GSAHt4e';
/**
* 使用应用身份获取 tenant_access_token
*/
function getTenantToken() {
return __awaiter(this, void 0, void 0, function () {
var res, data;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, 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 }),
})];
case 1:
res = _a.sent();
return [4 /*yield*/, res.json()];
case 2:
data = _a.sent();
if (data.code !== 0) {
throw new Error("Failed to get tenant token: ".concat(data.msg));
}
return [2 /*return*/, data.tenant_access_token];
}
});
});
}
/**
* 生成 Webhook 签名
*/
function generateWebhookSign(timestamp, secret) {
var stringToSign = "".concat(timestamp, "\n").concat(secret);
var hmac = (0, crypto_1.createHmac)('sha256', stringToSign);
return hmac.digest('base64');
}
/**
* 发送文本消息
*/
function sendFeishuMessage(options) {
return __awaiter(this, void 0, void 0, function () {
var text, receiveId, _a, receiveIdType, _b, useApp, token, url, res, data, timestamp, body, res, data;
var _c, _d, _e, _f;
return __generator(this, function (_g) {
switch (_g.label) {
case 0:
text = options.text, receiveId = options.receiveId, _a = options.receiveIdType, receiveIdType = _a === void 0 ? 'open_id' : _a, _b = options.useApp, useApp = _b === void 0 ? true : _b;
if (!useApp) return [3 /*break*/, 4];
return [4 /*yield*/, getTenantToken()];
case 1:
token = _g.sent();
url = "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=".concat(receiveIdType);
return [4 /*yield*/, fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': "Bearer ".concat(token),
},
body: JSON.stringify({
receive_id: receiveId,
msg_type: 'text',
content: JSON.stringify({ text: text }),
}),
})];
case 2:
res = _g.sent();
return [4 /*yield*/, res.json()];
case 3:
data = _g.sent();
return [2 /*return*/, {
ok: data.code === 0,
code: data.code,
msg: data.msg,
}];
case 4:
timestamp = Math.floor(Date.now() / 1000);
body = {
msg_type: 'text',
content: { text: text },
};
if (FEISHU_WEBHOOK_SECRET) {
body.timestamp = String(timestamp);
body.sign = generateWebhookSign(timestamp, FEISHU_WEBHOOK_SECRET);
}
return [4 /*yield*/, fetch(FEISHU_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})];
case 5:
res = _g.sent();
return [4 /*yield*/, res.json()];
case 6:
data = _g.sent();
return [2 /*return*/, {
ok: data.code === 0 || data.StatusCode === 0,
code: (_d = (_c = data.code) !== null && _c !== void 0 ? _c : data.StatusCode) !== null && _d !== void 0 ? _d : -1,
msg: (_f = (_e = data.msg) !== null && _e !== void 0 ? _e : data.StatusMessage) !== null && _f !== void 0 ? _f : '',
}];
}
});
});
}
/**
* 发送项目创建通知
*/
function notifyProjectCreated(projectName_1, goal_1, receiveId_1) {
return __awaiter(this, arguments, void 0, function (projectName, goal, receiveId, receiveIdType) {
if (receiveIdType === void 0) { receiveIdType = 'open_id'; }
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, sendFeishuMessage({
text: "\uD83D\uDE80 \u65B0\u9879\u76EE\u5DF2\u521B\u5EFA\n\n\u9879\u76EE\uFF1A".concat(projectName, "\n\u76EE\u6807\uFF1A").concat(goal, "\n\n\u8BF7\u53CA\u65F6\u67E5\u770B\u5E76\u786E\u8BA4\u9879\u76EE\u7AE0\u7A0B\u3002"),
receiveId: receiveId,
receiveIdType: receiveIdType,
useApp: true,
})];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
}
/**
* 发送里程碑提醒
*/
function notifyMilestoneReminder(milestoneName_1, targetDate_1, receiveId_1) {
return __awaiter(this, arguments, void 0, function (milestoneName, targetDate, receiveId, receiveIdType) {
if (receiveIdType === void 0) { receiveIdType = 'open_id'; }
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, sendFeishuMessage({
text: "\u23F0 \u91CC\u7A0B\u7891\u63D0\u9192\n\n\u91CC\u7A0B\u7891\u300C".concat(milestoneName, "\u300D\u5373\u5C06\u5230\u671F\n\u76EE\u6807\u65E5\u671F\uFF1A").concat(targetDate, "\n\n\u8BF7\u786E\u8BA4\u8FDB\u5EA6\u662F\u5426\u6B63\u5E38\u3002"),
receiveId: receiveId,
receiveIdType: receiveIdType,
useApp: true,
})];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
}
/**
* 发送风险预警
*/
function notifyRiskAlert(riskDesc_1, priority_1, receiveId_1) {
return __awaiter(this, arguments, void 0, function (riskDesc, priority, receiveId, receiveIdType) {
var level;
if (receiveIdType === void 0) { receiveIdType = 'open_id'; }
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
level = priority >= 15 ? '🔴 高' : priority >= 10 ? '🟡 中' : '🟢 低';
return [4 /*yield*/, sendFeishuMessage({
text: "\u26A0\uFE0F \u98CE\u9669\u9884\u8B66\n\n\u98CE\u9669\uFF1A".concat(riskDesc, "\n\u4F18\u5148\u7EA7\uFF1A").concat(level, "\uFF08").concat(priority, "\u5206\uFF09\n\n\u8BF7\u8BC4\u4F30\u5E76\u5236\u5B9A\u5E94\u5BF9\u63AA\u65BD\u3002"),
receiveId: receiveId,
receiveIdType: receiveIdType,
useApp: true,
})];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
}
/**
* 生成飞书卡片消息
*/
function buildDecisionCard(card) {
var elements = [
{
tag: 'div',
text: { tag: 'plain_text', content: card.description },
},
];
var actions = card.options.map(function (opt) { return ({
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: actions });
return {
msg_type: 'interactive',
card: {
header: {
title: { tag: 'plain_text', content: card.title },
template: 'blue',
},
elements: elements,
},
};
}
/**
* 发送决策卡片
*/
function sendDecisionCard(card_1, receiveId_1) {
return __awaiter(this, arguments, void 0, function (card, receiveId, receiveIdType) {
var token, url, res, data;
if (receiveIdType === void 0) { receiveIdType = 'open_id'; }
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, getTenantToken()];
case 1:
token = _a.sent();
url = "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=".concat(receiveIdType);
return [4 /*yield*/, fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': "Bearer ".concat(token),
},
body: JSON.stringify({
receive_id: receiveId,
msg_type: 'interactive',
card: buildDecisionCard(card),
}),
})];
case 2:
res = _a.sent();
return [4 /*yield*/, res.json()];
case 3:
data = _a.sent();
return [2 /*return*/, {
ok: data.code === 0,
code: data.code,
msg: data.msg,
}];
}
});
});
}