[铸渊修复] 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:
copilot-swe-agent[bot] 2026-03-15 10:28:30 +00:00
parent cef640d62d
commit 28b0e89cf7
7 changed files with 241 additions and 48 deletions

24
dingtalk-bot/.env.example Normal file
View File

@ -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
# ===== LLMAI回复配置 =====
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

View File

@ -1 +1,3 @@
.env
node_modules/
*.log

View File

@ -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
npm install
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`

View File

@ -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'

View File

@ -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_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, 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);
});

View File

@ -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",

View File

@ -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",