166 lines
5.5 KiB
JavaScript
166 lines
5.5 KiB
JavaScript
// 钉钉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
|
||
};
|