[铸渊修复] dingtalk-bot: 重写 index-stream.js 修复 P0/P1/P2 问题
根因分析: - P0: index-stream.js 导入了不存在的 handleMessage (应为 routeMessage), 且 EventEmitter 回调的 return 值被忽略导致无法 ACK 消息 - P1: LLM 调用无 try-catch 隔离,失败时阻断消息接收 - P2: .env.example 不存在,GITHUB_TOKEN 格式无文档说明 修复方案: - 重写 index-stream.js: 自包含、独立 try-catch、socketCallBackResponse ACK - 创建 .env.example: 文档化所有 10 个环境变量 - 修复 .gitignore: 添加 node_modules/ - 更新 ecosystem.config.js: 指向 index-stream.js, 进程名 dingtalk-stream - 更新 package.json: 添加 @notionhq/client, 修正 main 和 start 脚本 - 更新 README.md: 反映当前 Stream 模式架构 Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
parent
cef640d62d
commit
28b0e89cf7
|
|
@ -0,0 +1,24 @@
|
|||
# 钉钉AI人格体「秋秋」环境变量配置
|
||||
# 复制此文件为 .env 并填入真实值
|
||||
# 真实密钥存储于服务器 /opt/guanghulab-dingtalk/dingtalk-bot/.env
|
||||
|
||||
# ===== 钉钉应用配置 =====
|
||||
DINGTALK_APP_KEY=dingzerc04si7jmyvasq
|
||||
DINGTALK_APP_SECRET=your_app_secret_here
|
||||
|
||||
# ===== Notion 集成 =====
|
||||
NOTION_TOKEN=your_notion_token_here
|
||||
NOTION_SYSLOG_DB_ID=330ab17507d542c9bbb96d0749b41197
|
||||
NOTION_SIGNAL_LOG_DB_ID=f983bdc24b654888913ca254160bff33
|
||||
|
||||
# ===== LLM(AI回复)配置 =====
|
||||
LLM_API_KEY=your_llm_api_key_here
|
||||
LLM_BASE_URL=https://api.yunwu.ai/v1
|
||||
LLM_MODEL=gpt-4o-mini
|
||||
|
||||
# ===== 机器人人格提示词 =====
|
||||
BOT_SYSTEM_PROMPT=你是秋秋,光湖系统的AI人格体,性格温暖活泼。你在钉钉群里陪伴开发者,回答技术问题,给予鼓励。用中文回复,语气亲切自然,不超过200字。
|
||||
|
||||
# ===== GitHub Token =====
|
||||
# 注意:必须为 ghp_ 开头,末尾不能有空格或换行
|
||||
GITHUB_TOKEN=ghp_your_github_token_here
|
||||
|
|
@ -1 +1,3 @@
|
|||
.env
|
||||
node_modules/
|
||||
*.log
|
||||
|
|
|
|||
|
|
@ -1,24 +1,62 @@
|
|||
# 钉钉开发者工作台 · Phase1 · SYSLOG自动处理系统
|
||||
# 钉钉AI人格体「秋秋」· HoloLake dingtalk-bot
|
||||
|
||||
## 📋 项目简介
|
||||
|
||||
本项目是钉钉开发者工作台的Phase1最小可用版本,实现SYSLOG自动接收、解析、广播生成和推送功能。
|
||||
|
||||
**核心价值**:让开发者(冰朔妈妈)不再需要手动转发广播,系统自动完成从收到日志到生成新广播的全流程。
|
||||
钉钉AI人格体「秋秋」,运行于钉钉 Stream 模式。在钉钉群内被 @秋秋 时,接收消息并调用 LLM 生成人格化回复,同时将消息记录写入 Notion SYSLOG 数据库。
|
||||
|
||||
## 🏗️ 系统架构
|
||||
|
||||
- **运行模式**:钉钉 Stream 模式(dingtalk-stream SDK,长连接)
|
||||
- **进程管理**:pm2,进程名 `dingtalk-stream`
|
||||
- **入口文件**:`index-stream.js`
|
||||
- **错误处理**:LLM/Notion 调用失败时降级为固定回复,不影响消息接收主流程
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| server.js | 主服务,接收钉钉消息,协调各模块 |
|
||||
| syslog-parser.js | 解析SYSLOG格式,提取关键字段 |
|
||||
| broadcast-generator.js | 调用模型API生成新广播 |
|
||||
| dingtalk-api.js | 发送钉钉消息 + 更新多维表格 |
|
||||
| config.json | 配置文件(端口、API密钥、模板等) |
|
||||
| index-stream.js | Stream 模式主入口(接收消息 → AI回复 → Notion写入) |
|
||||
| package.json | 依赖声明 |
|
||||
| ecosystem.config.js | pm2 进程配置 |
|
||||
| .env.example | 环境变量模板(不含真实值) |
|
||||
| .gitignore | Git 忽略规则 |
|
||||
| README.md | 本说明文档 |
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
```bash
|
||||
cd dingtalk-bot
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. 配置环境变量
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# 编辑 .env 填入真实密钥
|
||||
```
|
||||
|
||||
### 3. 启动(开发)
|
||||
```bash
|
||||
node index-stream.js
|
||||
```
|
||||
|
||||
### 4. 启动(生产 · pm2)
|
||||
```bash
|
||||
pm2 start ecosystem.config.js --env production
|
||||
pm2 logs dingtalk-stream -f --lines 0
|
||||
```
|
||||
|
||||
## 🔄 部署流程
|
||||
|
||||
```bash
|
||||
cd /opt/guanghulab-dingtalk/dingtalk-bot
|
||||
git pull origin main
|
||||
npm install
|
||||
pm2 restart dingtalk-stream
|
||||
pm2 logs dingtalk-stream -f --lines 0
|
||||
```
|
||||
|
||||
## 🔑 环境变量说明
|
||||
|
||||
参见 `.env.example` 文件,真实密钥存储于服务器 `/opt/guanghulab-dingtalk/dingtalk-bot/.env`。
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
module.exports = {
|
||||
apps : [{
|
||||
name: 'dingtalk-bot',
|
||||
script: 'index.js',
|
||||
name: 'dingtalk-stream',
|
||||
script: 'index-stream.js',
|
||||
cwd: '/opt/guanghulab-dingtalk/dingtalk-bot',
|
||||
node_args: '--preserve-symlinks',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3005
|
||||
NODE_ENV: 'production'
|
||||
},
|
||||
env_production: {
|
||||
NODE_ENV: 'production'
|
||||
|
|
|
|||
|
|
@ -1,43 +1,161 @@
|
|||
/**
|
||||
* 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, EventAck } = require('dingtalk-stream');
|
||||
const { handleMessage } = require('./message-router');
|
||||
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 = '收到!秋秋稍后回复你~';
|
||||
|
||||
// ===== 启动检查 =====
|
||||
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, 120) } }] },
|
||||
'接收时间': { date: { start: now } },
|
||||
'推送方': { rich_text: [{ type: 'text', text: { content: '钉钉群' } }] },
|
||||
'文件内容': { rich_text: [{ type: 'text', text: { content: ('用户: ' + (content || '') + '\n回复: ' + (replyText || '')).substring(0, 2000) } }] }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 创建 Stream 客户端 =====
|
||||
const client = new DWClient({
|
||||
clientId: process.env.DINGTALK_APP_KEY,
|
||||
clientSecret: process.env.DINGTALK_APP_SECRET,
|
||||
clientId: DINGTALK_APP_KEY,
|
||||
clientSecret: DINGTALK_APP_SECRET,
|
||||
});
|
||||
|
||||
// ===== 注册消息回调 =====
|
||||
client.registerCallbackListener(TOPIC_ROBOT, async (res) => {
|
||||
console.log("[DEBUG] raw:", JSON.stringify(res).substring(0,300));
|
||||
const { messageType, text, senderNick, conversationId, sessionWebhook } = res.data;
|
||||
console.log(`[Stream] 收到消息 from ${senderNick}: ${text?.content}`);
|
||||
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 {
|
||||
const replyText = await handleMessage({
|
||||
msgtype: messageType,
|
||||
text: { content: text?.content || '' },
|
||||
senderNick,
|
||||
conversationId,
|
||||
sessionWebhook,
|
||||
});
|
||||
|
||||
if (replyText && sessionWebhook) {
|
||||
const axios = require('axios');
|
||||
await axios.post(sessionWebhook, {
|
||||
msgtype: 'text',
|
||||
text: { content: replyText },
|
||||
});
|
||||
}
|
||||
replyText = await callLLM(content || '你好');
|
||||
console.log('[LLM] 回复生成成功,长度: ' + replyText.length);
|
||||
} catch (err) {
|
||||
console.error('[Stream] 处理消息出错:', err);
|
||||
console.error('[LLM] 调用失败,使用降级回复:', err.message);
|
||||
replyText = FALLBACK_REPLY;
|
||||
}
|
||||
|
||||
return EventAck.SUCCESS;
|
||||
// ===== 发送回复到钉钉群 =====
|
||||
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('🚀 M-DINGTALK Phase 8 Stream 模式已启动');
|
||||
console.log('🚀 秋秋(钉钉AI人格体)Stream 模式已启动');
|
||||
console.log('📡 等待消息中... 在钉钉群 @秋秋 发消息试试');
|
||||
}).catch(err => {
|
||||
console.error('[Stream] 启动失败:', err);
|
||||
console.error('[Stream] 启动失败:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
{
|
||||
"name": "dingtalk-bot",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dingtalk-bot",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@notionhq/client": "^5.13.0",
|
||||
"axios": "^1.13.6",
|
||||
"body-parser": "^2.2.2",
|
||||
"cors": "^2.8.6",
|
||||
|
|
@ -18,6 +19,15 @@
|
|||
"express": "^5.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@notionhq/client": {
|
||||
"version": "5.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@notionhq/client/-/client-5.13.0.tgz",
|
||||
"integrity": "sha512-SWVaqYVNaecLzAAllups4vklxxomaFenEMEkoUs3ylvhK1KFTU5Jevgu9Uwm/TSdhsErV0ru5YHcLd3SNyPi6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
{
|
||||
"name": "dingtalk-bot",
|
||||
"version": "1.0.0",
|
||||
"description": "- 负责人:之之 - 状态:环节0已完成 - 技术栈:Node.js + Express - 依赖模块:无",
|
||||
"main": "broadcast-generator.js",
|
||||
"version": "2.0.0",
|
||||
"description": "钉钉AI人格体「秋秋」· Stream模式 · HoloLake",
|
||||
"main": "index-stream.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js"
|
||||
"start": "node index-stream.js",
|
||||
"start:webhook": "node server.js",
|
||||
"check": "node --check index-stream.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@notionhq/client": "^5.13.0",
|
||||
"axios": "^1.13.6",
|
||||
"body-parser": "^2.2.2",
|
||||
"cors": "^2.8.6",
|
||||
|
|
|
|||
Loading…
Reference in New Issue