164 lines
5.9 KiB
JavaScript
164 lines
5.9 KiB
JavaScript
/**
|
||
* 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_ID,SYSLOG 写入已禁用');
|
||
}
|
||
|
||
// ===== 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);
|
||
});
|