zhizhi/dingtalk-bot/index-stream.js

164 lines
5.9 KiB
JavaScript
Raw Permalink Normal View History

/**
* 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);
});