guanghulab/dingtalk-bot/index-stream.js
2026-05-10 13:12:44 +08:00

164 lines
5.9 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.

/**
* index-stream.js · 钉钉AI人格体「秋秋」· Stream模式主入口
* HoloLake · dingtalk-bot
* 开发者之之DEV-004
*
* 功能:
* 1. 接收钉钉 Stream 消息(@机器人触发)
* 2. 调用 LLM 生成人格化回复(失败时降级为固定回复)
* 3. 每条消息写入 Notion SYSLOG 数据库
* 4. 断线自动重连,外部 API 失败不影响消息接收主流程
*/
require('dotenv').config();
const { DWClient, TOPIC_ROBOT } = require('dingtalk-stream');
const axios = require('axios');
// ===== 环境变量 =====
const DINGTALK_APP_KEY = process.env.DINGTALK_APP_KEY;
const DINGTALK_APP_SECRET = process.env.DINGTALK_APP_SECRET;
const NOTION_TOKEN = process.env.NOTION_TOKEN;
const NOTION_SYSLOG_DB_ID = process.env.NOTION_SYSLOG_DB_ID;
const LLM_API_KEY = process.env.LLM_API_KEY || '';
const LLM_BASE_URL = (process.env.LLM_BASE_URL || 'https://api.yunwu.ai/v1').replace(/\/$/, '');
const LLM_MODEL = process.env.LLM_MODEL || 'gpt-4o-mini';
const BOT_SYSTEM_PROMPT = process.env.BOT_SYSTEM_PROMPT ||
'你是秋秋光湖系统的AI人格体性格温暖活泼。你在钉钉群里陪伴开发者回答技术问题给予鼓励。用中文回复语气亲切自然不超过200字。';
const FALLBACK_REPLY = '收到!秋秋稍后回复你~';
const NOTION_TITLE_MAX = 120;
const NOTION_CONTENT_MAX = 2000;
// ===== 启动检查 =====
if (!DINGTALK_APP_KEY || !DINGTALK_APP_SECRET) {
console.error('[启动失败] 缺少 DINGTALK_APP_KEY 或 DINGTALK_APP_SECRET');
process.exit(1);
}
// ===== Notion 客户端(可选,延迟加载) =====
let notion = null;
if (NOTION_TOKEN && NOTION_SYSLOG_DB_ID) {
try {
const { Client: NotionClient } = require('@notionhq/client');
notion = new NotionClient({ auth: NOTION_TOKEN });
console.log('[Notion] SYSLOG 写入已启用');
} catch (err) {
console.log('[Notion] @notionhq/client 加载失败SYSLOG 写入已禁用:', err.message);
}
} else {
console.log('[Notion] 缺少 NOTION_TOKEN 或 NOTION_SYSLOG_DB_IDSYSLOG 写入已禁用');
}
// ===== LLM 调用(独立函数,失败返回降级回复) =====
async function callLLM(userMessage) {
const response = await axios.post(LLM_BASE_URL + '/chat/completions', {
model: LLM_MODEL,
max_tokens: 500,
messages: [
{ role: 'system', content: BOT_SYSTEM_PROMPT },
{ role: 'user', content: userMessage }
]
}, {
headers: {
'Authorization': 'Bearer ' + LLM_API_KEY,
'Content-Type': 'application/json'
},
timeout: 30000
});
const text = response.data
&& response.data.choices
&& response.data.choices[0]
&& response.data.choices[0].message
&& response.data.choices[0].message.content;
return text || FALLBACK_REPLY;
}
// ===== Notion SYSLOG 写入 =====
async function writeNotionSyslog(senderNick, content, replyText) {
if (!notion) return;
const now = new Date().toISOString();
await notion.pages.create({
parent: { database_id: NOTION_SYSLOG_DB_ID },
properties: {
'标题': { title: [{ type: 'text', text: { content: ('[钉钉] ' + senderNick + ' · ' + now.split('T')[0]).substring(0, NOTION_TITLE_MAX) } }] },
'接收时间': { date: { start: now } },
'推送方': { rich_text: [{ type: 'text', text: { content: '钉钉群' } }] },
'文件内容': { rich_text: [{ type: 'text', text: { content: ('用户: ' + (content || '') + '\n回复: ' + (replyText || '')).substring(0, NOTION_CONTENT_MAX) } }] }
}
});
}
// ===== 创建 Stream 客户端 =====
const client = new DWClient({
clientId: DINGTALK_APP_KEY,
clientSecret: DINGTALK_APP_SECRET,
});
// ===== 注册消息回调 =====
client.registerCallbackListener(TOPIC_ROBOT, async (res) => {
console.log('[Stream] 收到消息,原始数据:', JSON.stringify(res).substring(0, 300));
// 解析消息数据res.data 可能是 JSON 字符串或对象)
let msgData = res.data;
if (typeof msgData === 'string') {
try { msgData = JSON.parse(msgData); } catch (e) { msgData = {}; }
}
msgData = msgData || {};
const senderNick = msgData.senderNick || '未知用户';
const content = (msgData.text && msgData.text.content || '').trim();
const sessionWebhook = msgData.sessionWebhook;
const messageId = res.headers && res.headers.messageId;
console.log('[Stream] 发送者: ' + senderNick + ', 内容: ' + content);
let replyText = FALLBACK_REPLY;
// ===== LLM 调用(独立 try-catch失败不影响主流程 =====
try {
replyText = await callLLM(content || '你好');
console.log('[LLM] 回复生成成功,长度: ' + replyText.length);
} catch (err) {
console.error('[LLM] 调用失败,使用降级回复:', err.message);
replyText = FALLBACK_REPLY;
}
// ===== 发送回复到钉钉群 =====
if (sessionWebhook) {
try {
await axios.post(sessionWebhook, {
msgtype: 'text',
text: { content: replyText }
}, { timeout: 10000 });
console.log('[Stream] 回复发送成功');
} catch (err) {
console.error('[Stream] 回复发送失败:', err.message);
}
}
// ===== Notion SYSLOG 写入(独立 try-catch失败不影响主流程 =====
try {
await writeNotionSyslog(senderNick, content, replyText);
console.log('[Notion] SYSLOG 写入成功');
} catch (err) {
console.error('[Notion] SYSLOG 写入失败:', err.message);
}
// ===== 消息确认(防止服务端重试推送) =====
try {
if (messageId) {
client.socketCallBackResponse(messageId, { status: 'SUCCESS' });
}
} catch (err) {
console.error('[Stream] 消息确认失败:', err.message);
}
});
// ===== 启动连接 =====
client.connect().then(() => {
console.log('🚀 秋秋钉钉AI人格体Stream 模式已启动');
console.log('📡 等待消息中... 在钉钉群 @秋秋 发消息试试');
}).catch(err => {
console.error('[Stream] 启动失败:', err.message);
process.exit(1);
});