guanghulab/dingtalk-webhook-v3.js

166 lines
5.5 KiB
JavaScript
Raw Permalink Normal View History

// 钉钉Webhook处理器 v3.0 · dingtalk-webhook-v3.js
// HoloLake · M-DINGTALK Phase 8
// DEV-004 之之 × 秋秋
// v3.0 变化:
// + 真实事件处理(加密/解密/验证)
// + 接入conversation-manager上下文
// + 接入github-bridge
// + 更智能的回复格式
// ====================
var crypto = require('crypto');
var axios = require('axios');
var router = require('./message-router');
var eventHandler = require('./dingtalk-event-handler');
var DINGTALK_CONFIG = {
appKey: process.env.DINGTALK_APP_KEY || '',
appSecret: process.env.DINGTALK_APP_SECRET || '',
token: process.env.DINGTALK_TOKEN || '',
encodingAesKey: process.env.DINGTALK_ENCODING_AES_KEY || ''
};
// ====== 签名验证中间件 ======
function verifyMiddleware(req, res, next) {
var appSecret = DINGTALK_CONFIG.appSecret;
if (!appSecret || appSecret.startsWith('替换')) {
console.log('[DingTalk] ⚠️ APP_SECRET未配置跳过签名验证');
return next();
}
var timestamp = req.headers['timestamp'];
var sign = req.headers['sign'];
if (!timestamp || !sign) {
// 可能是事件订阅格式,不带签名头
return next();
}
if (Math.abs(Date.now() - parseInt(timestamp)) > 300000) {
return res.status(403).json({ error: '时间戳过期' });
}
var stringToSign = timestamp + '\n' + appSecret;
var hmac = crypto.createHmac('sha256', appSecret);
hmac.update(stringToSign);
var computed = hmac.digest('base64');
if (computed === sign) {
next();
} else {
return res.status(403).json({ error: '签名验证失败' });
}
}
// ====== v3.0 核心:真实回调处理 ======
async function handleCallback(req, res) {
var body = req.body || {};
// ====== 处理回调验证check_url ======
if (body.encrypt && !body.text) {
var verifyResult = eventHandler.handleVerify(body, DINGTALK_CONFIG);
return res.json(verifyResult);
}
if (body.EventType === 'check_url' || body.challenge) {
console.log('[DingTalk] 📋 回调验证 · 非加密模式');
return res.json({ success: true, challenge: body.challenge || 'ok' });
}
// ====== 快速返回200钉钉要求3秒内 ======
res.json({ success: true });
// ====== 解析消息 ======
var parsed = eventHandler.parseMessage(body, DINGTALK_CONFIG);
var msg = eventHandler.extractContent(parsed);
if (!msg.content) {
console.log('[DingTalk] ⚠️ 消息内容为空,跳过');
return;
}
var chatDesc = msg.isGroup ? ('群聊·' + msg.conversationTitle) : '单聊';
console.log(`[DingTalk] 📨 收到消息 | ${chatDesc} | ${msg.senderNick}: ${msg.content}`);
// ====== 路由处理 ======
var result = await router.routeMessage(
msg.content,
msg.senderNick,
msg.senderId,
msg.sessionWebhook
);
if (!result || !result.aiResponse) {
console.log('[DingTalk] ⚠️ 没有生成回复内容');
return;
}
// ====== 消息分类标记 ======
var classification = router.classifyMessage(msg.content);
// ====== 回复钉钉 ======
if (msg.sessionWebhook) {
try {
var replyText = '';
switch (classification.type) {
case 'syslog':
replyText = '### ✅ SYSLOG 已收到\n\n'
+ '**提交者**' + msg.senderNick + '\n'
+ (result.aiModel ? ('**AI处理**' + result.aiModel + '\n\n') : '\n')
+ (result.aiResponse || '') + '\n\n'
+ '> 🌊 HoloLake · Phase 8';
break;
case 'github':
replyText = result.aiResponse;
break;
case 'command':
replyText = result.aiResponse;
break;
default:
replyText = result.aiResponse
+ (result.aiModel ? ('\n\n> 🤖 ' + result.aiModel) : '')
+ '\n> 🌊 HoloLake · 秋秋';
}
await axios.post(msg.sessionWebhook, {
msgtype: 'markdown',
markdown: {
title: classification.type === 'syslog' ? '✅ SYSLOG已收到'
: classification.type === 'github' ? '🔀 GitHub'
: '💙 秋秋',
text: replyText.substring(0, 5000) // 钉钉消息长度限制
}
}, { timeout: 8000 });
console.log('[DingTalk] ✅ 回复已发送 · ' + replyText.length + '字');
} catch (err) {
console.error('[DingTalk] ❌ 回复失败: ' + err.message);
}
}
console.log('=================================\n');
}
// ====== 健康检查 ======
function healthCheck(req, res) {
res.json({
status: 'ok',
service: 'M-DINGTALK',
phase: 8,
version: 'v3.0',
features: ['智能分流v2', 'AI多轮对话', '上下文记忆', 'GitHub桥接', '事件加密处理', 'SYSLOG自动写入'],
uptime: process.uptime(),
time: new Date().toLocaleString('zh-CN')
});
}
module.exports = {
verifyMiddleware: verifyMiddleware,
handleCallback: handleCallback,
healthCheck: healthCheck
};