DEV-004: M-DINGTALK Phase 9 服务上传 · 待人格体检查Stream连接问题
This commit is contained in:
commit
8eadc95701
|
|
@ -0,0 +1,4 @@
|
|||
.env
|
||||
node_modules/
|
||||
*.bak
|
||||
*.log
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# 钉钉开发者工作台 · Phase1 · SYSLOG自动处理系统
|
||||
|
||||
## 📋 项目简介
|
||||
|
||||
本项目是钉钉开发者工作台的Phase1最小可用版本,实现SYSLOG自动接收、解析、广播生成和推送功能。
|
||||
|
||||
**核心价值**:让开发者(冰朔妈妈)不再需要手动转发广播,系统自动完成从收到日志到生成新广播的全流程。
|
||||
|
||||
## 🏗️ 系统架构
|
||||
## 📁 文件结构
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| server.js | 主服务,接收钉钉消息,协调各模块 |
|
||||
| syslog-parser.js | 解析SYSLOG格式,提取关键字段 |
|
||||
| broadcast-generator.js | 调用模型API生成新广播 |
|
||||
| dingtalk-api.js | 发送钉钉消息 + 更新多维表格 |
|
||||
| config.json | 配置文件(端口、API密钥、模板等) |
|
||||
| README.md | 本说明文档 |
|
||||
|
||||
## 🚀 快速开始
|
||||
### 1. 安装依赖
|
||||
```bash
|
||||
npm install
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// broadcast-generator.js
|
||||
// Phase 1 - 广播生成器
|
||||
|
||||
function getLatest() {
|
||||
return {
|
||||
id: 'BC-M-DINGTALK-003-ZZ',
|
||||
title: 'Phase 3 · 知识库挂载+Webhook+三节点同步',
|
||||
time: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { getLatest };
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"server": {
|
||||
"port": 3000
|
||||
},
|
||||
"dingtalk": {
|
||||
"webhook": "https://oapi.dingtalk.com/robot/send?access_token=YOUR_BOT_TOKEN",
|
||||
"appKey": "YOUR_APP_KEY",
|
||||
"appSecret": "YOUR_APP_SECRET"
|
||||
},
|
||||
"modelApi": {
|
||||
"url": "https://api.openai.com/v1/chat/completions",
|
||||
"key": "YOUR_MODEL_API_KEY",
|
||||
"model": "gpt-3.5-turbo"
|
||||
},
|
||||
"bitable": {
|
||||
"appId": "YOUR_BITABLE_APP_ID",
|
||||
"tableId": "YOUR_BITABLE_TABLE_ID"
|
||||
},
|
||||
"broadcastTemplates": {
|
||||
"completed": "BC-{bcNumber}-{nextPhase}-ZZ · DEV-{devId} · {moduleName} · 环节{phaseNum} · {statusEmoji} · {summary}",
|
||||
"partial": "BC-{bcNumber}-{nextPhase}-ZZ · DEV-{devId} · {moduleName} · 环节{phaseNum} · ⚠️ 部分完成 · {summary}",
|
||||
"blocked": "BC-{bcNumber}-{nextPhase}-ZZ · DEV-{devId} · {moduleName} · 环节{phaseNum} · 🔴 阻塞 · {summary}"
|
||||
},
|
||||
"syslog": {
|
||||
"keywords": ["SYSLOG", "BC-", "completed", "partial", "blocked"]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
// 对话上下文管理器 · conversation-manager.js · v1.0
|
||||
// HoloLake · M-DINGTALK Phase 8
|
||||
// DEV-004 之之 × 秋秋
|
||||
// 功能:对话历史存储 · 上下文窗口管理 · 多用户隔离
|
||||
//
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
// 对话存储目录
|
||||
var CONVERSATION_DIR = path.join(__dirname, 'conversations');
|
||||
if (!fs.existsSync(CONVERSATION_DIR)) {
|
||||
fs.mkdirSync(CONVERSATION_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// 配置
|
||||
var MAX_HISTORY = 20; // 每个用户最多保留20轮对话
|
||||
var MAX_CONTEXT_TOKENS = 3000; // 上下文窗口大小(估算字符数)
|
||||
var EXPIRE_HOURS = 24; // 对话24小时后过期
|
||||
|
||||
// ===== 获取用户对话文件路径 =====
|
||||
function getConversationPath(userId) {
|
||||
var safeId = (userId || 'anonymous').replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
return path.join(CONVERSATION_DIR, safeId + '.json');
|
||||
}
|
||||
|
||||
// ===== 加载对话历史 =====
|
||||
function loadHistory(userId) {
|
||||
var filePath = getConversationPath(userId);
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return [];
|
||||
var data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
// 过滤过期消息
|
||||
var now = Date.now();
|
||||
var expireMs = EXPIRE_HOURS * 60 * 60 * 1000;
|
||||
var valid = (data.messages || []).filter(function(msg) {
|
||||
return (now - msg.timestamp) < expireMs;
|
||||
});
|
||||
return valid;
|
||||
} catch (err) {
|
||||
console.error('[ConvMgr] 加载历史失败:', err.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 保存对话历史 =====
|
||||
function saveHistory(userId, messages) {
|
||||
var filePath = getConversationPath(userId);
|
||||
try {
|
||||
// 只保留最近N轮
|
||||
var trimmed = messages.slice(-MAX_HISTORY * 2);
|
||||
fs.writeFileSync(filePath, JSON.stringify({
|
||||
userid: userId,
|
||||
updatedAt: new Date().toISOString(),
|
||||
messages: trimmed
|
||||
}, null, 2), 'utf8');
|
||||
} catch (err) {
|
||||
console.error('[ConvMgr] 保存历史失败:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 添加消息 ======
|
||||
function addMessage(userId, role, content) {
|
||||
var messages = loadHistory(userId);
|
||||
messages.push({
|
||||
role: role,
|
||||
content: content,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
saveHistory(userId, messages);
|
||||
return messages;
|
||||
}
|
||||
|
||||
// ====== 构建上下文窗口(给LLM用) ======
|
||||
function buildContext(userId, currentMessage) {
|
||||
var history = loadHistory(userId);
|
||||
var contextMessages = [];
|
||||
var totalChars = 0;
|
||||
|
||||
// 从最新的开始往回取,直到超过上下文窗口
|
||||
for (var i = history.length - 1; i >= 0; i--) {
|
||||
var msg = history[i];
|
||||
var msgChars = (msg.content || '').length;
|
||||
if (totalChars + msgChars > MAX_CONTEXT_TOKENS) break;
|
||||
contextMessages.unshift({
|
||||
role: msg.role,
|
||||
content: msg.content
|
||||
});
|
||||
totalChars += msgChars;
|
||||
}
|
||||
|
||||
// 添加当前消息
|
||||
contextMessages.push({
|
||||
role: 'user',
|
||||
content: currentMessage
|
||||
});
|
||||
|
||||
return contextMessages;
|
||||
}
|
||||
|
||||
// ===== 清除用户对话历史 =====
|
||||
function clearHistory(userId) {
|
||||
var filePath = getConversationPath(userId);
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 获取活跃对话统计 =====
|
||||
function getStats() {
|
||||
try {
|
||||
var files = fs.readdirSync(CONVERSATION_DIR).filter(function(f) {
|
||||
return f.endsWith('.json');
|
||||
});
|
||||
return {
|
||||
activeUsers: files.length,
|
||||
conversationDir: CONVERSATION_DIR
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
activeUsers: 0,
|
||||
conversationDir: CONVERSATION_DIR
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadHistory: loadHistory,
|
||||
saveHistory: saveHistory,
|
||||
addMessage: addMessage,
|
||||
buildContext: buildContext,
|
||||
clearHistory: clearHistory,
|
||||
getStats: getStats
|
||||
};
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"userid": "test",
|
||||
"updatedAt": "2026-03-15T03:35:58.320Z",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "测试",
|
||||
"timestamp": 1773545752588
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "你好呀,测试成功啦!🍭 有什么需要我帮忙的呢?无论是技术问题还是想聊聊,尽管问我哦!✨",
|
||||
"timestamp": 1773545758320
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"developers": [
|
||||
{
|
||||
"dev_id": "DEV-004",
|
||||
"name": "之之",
|
||||
"status": "进行中",
|
||||
"current_module": "M-DINGTALK Phase 3",
|
||||
"last_broadcast_time": "2026-03-11T04:59:59.567Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
[
|
||||
{
|
||||
"timestamp": "2026-03-11T05:59:44.031Z",
|
||||
"dev_id": "DEV-004",
|
||||
"steps": {
|
||||
"portrait": "✅",
|
||||
"pca": "✅",
|
||||
"sync": "✅"
|
||||
},
|
||||
"duration_ms": 1,
|
||||
"pca": {
|
||||
"score": 73,
|
||||
"grade": "A"
|
||||
},
|
||||
"suggestion": "🎯 可进更高难度:EL-9 挑战",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-11T05:53:04.004Z",
|
||||
"dev_id": "DEV-004",
|
||||
"steps": {
|
||||
"portrait": "✅",
|
||||
"pca": "✅",
|
||||
"sync": "✅"
|
||||
},
|
||||
"duration_ms": 7,
|
||||
"pca": {
|
||||
"score": 66,
|
||||
"grade": "B"
|
||||
},
|
||||
"suggestion": "📚 继续当前模块:巩固 Phase4 基础",
|
||||
"status": "completed"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// data/multi-sheet.js
|
||||
// Phase 2 - 多维表格数据
|
||||
|
||||
const developers = [
|
||||
{
|
||||
dev_id: 'DEV-004',
|
||||
name: '之之',
|
||||
status: '进行中',
|
||||
current_module: 'M-DINGTALK Phase 3',
|
||||
last_broadcast_time: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
function getAll() {
|
||||
return { developers };
|
||||
}
|
||||
|
||||
function getByDevId(id) {
|
||||
return developers.find(d => d.dev_id === id);
|
||||
}
|
||||
|
||||
module.exports = { getAll, getByDevId };
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
[
|
||||
{
|
||||
"timestamp": "2026-03-13T01:45:13.023Z",
|
||||
"target": "syslog",
|
||||
"page_id": "322fb92f-3831-81e6-aeb1-db1e0a2c2719",
|
||||
"title": "【测试】M-DINGTALK Phase 5 连通测试 · DEV-004之之",
|
||||
"status": "success"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"DEV-004": {
|
||||
"dev_id": "DEV-004",
|
||||
"dev_name": "之之",
|
||||
"first_seen": "2026-03-11T05:40:58.007Z",
|
||||
"total_sessions": 4,
|
||||
"total_code_lines": 8400,
|
||||
"max_streak": 4,
|
||||
"current_streak": 4,
|
||||
"recent_snapshots": [
|
||||
{
|
||||
"timestamp": "2026-03-11T05:59:44.030Z",
|
||||
"dev_id": "DEV-004",
|
||||
"session_id": "BC-M-DINGTALK-004-ZZ",
|
||||
"dev_name": "之之",
|
||||
"rhythm": "快",
|
||||
"friction": "低",
|
||||
"mood": "正向",
|
||||
"growth": {
|
||||
"count": 1,
|
||||
"signals": [
|
||||
"代码量增长"
|
||||
]
|
||||
},
|
||||
"will": "强烈",
|
||||
"raw_stats": {
|
||||
"completed_items": 34,
|
||||
"total_items": 34,
|
||||
"code_lines": 2400,
|
||||
"streak": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-11T05:53:03.996Z",
|
||||
"dev_id": "DEV-004",
|
||||
"session_id": "LOOP-TEST-001",
|
||||
"dev_name": "之之",
|
||||
"rhythm": "快",
|
||||
"friction": "无",
|
||||
"mood": "正向",
|
||||
"growth": {
|
||||
"count": 2,
|
||||
"signals": [
|
||||
"代码量增长",
|
||||
"连胜11次"
|
||||
]
|
||||
},
|
||||
"will": "正常",
|
||||
"raw_stats": {
|
||||
"completed_items": 5,
|
||||
"total_items": 5,
|
||||
"code_lines": 2000,
|
||||
"streak": 11
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-11T05:47:07.338Z",
|
||||
"dev_id": "DEV-004",
|
||||
"session_id": "TEST-002",
|
||||
"dev_name": "之之",
|
||||
"rhythm": "快",
|
||||
"friction": "低",
|
||||
"mood": "正向",
|
||||
"growth": {
|
||||
"count": 2,
|
||||
"signals": [
|
||||
"代码量增长",
|
||||
"连胜11次"
|
||||
]
|
||||
},
|
||||
"will": "强烈",
|
||||
"raw_stats": {
|
||||
"completed_items": 5,
|
||||
"total_items": 5,
|
||||
"code_lines": 2000,
|
||||
"streak": 11
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-11T05:40:58.007Z",
|
||||
"dev_id": "DEV-004",
|
||||
"session_id": "TEST-001",
|
||||
"dev_name": "之之",
|
||||
"rhythm": "快",
|
||||
"friction": "低",
|
||||
"mood": "正向",
|
||||
"growth": {
|
||||
"count": 2,
|
||||
"signals": [
|
||||
"代码量增长",
|
||||
"连胜11次"
|
||||
]
|
||||
},
|
||||
"will": "强烈",
|
||||
"raw_stats": {
|
||||
"completed_items": 5,
|
||||
"total_items": 5,
|
||||
"code_lines": 2000,
|
||||
"streak": 11
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
[
|
||||
{
|
||||
"timestamp": "2026-03-11T05:59:44.030Z",
|
||||
"dev_id": "DEV-004",
|
||||
"session_id": "BC-M-DINGTALK-004-ZZ",
|
||||
"snapshot": {
|
||||
"timestamp": "2026-03-11T05:59:44.030Z",
|
||||
"dev_id": "DEV-004",
|
||||
"session_id": "BC-M-DINGTALK-004-ZZ",
|
||||
"dev_name": "之之",
|
||||
"rhythm": "快",
|
||||
"friction": "低",
|
||||
"mood": "正向",
|
||||
"growth": {
|
||||
"count": 1,
|
||||
"signals": [
|
||||
"代码量增长"
|
||||
]
|
||||
},
|
||||
"will": "强烈",
|
||||
"raw_stats": {
|
||||
"completed_items": 34,
|
||||
"total_items": 34,
|
||||
"code_lines": 2400,
|
||||
"streak": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-11T05:53:03.996Z",
|
||||
"dev_id": "DEV-004",
|
||||
"session_id": "LOOP-TEST-001",
|
||||
"snapshot": {
|
||||
"timestamp": "2026-03-11T05:53:03.996Z",
|
||||
"dev_id": "DEV-004",
|
||||
"session_id": "LOOP-TEST-001",
|
||||
"dev_name": "之之",
|
||||
"rhythm": "快",
|
||||
"friction": "无",
|
||||
"mood": "正向",
|
||||
"growth": {
|
||||
"count": 2,
|
||||
"signals": [
|
||||
"代码量增长",
|
||||
"连胜11次"
|
||||
]
|
||||
},
|
||||
"will": "正常",
|
||||
"raw_stats": {
|
||||
"completed_items": 5,
|
||||
"total_items": 5,
|
||||
"code_lines": 2000,
|
||||
"streak": 11
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-11T05:47:07.338Z",
|
||||
"dev_id": "DEV-004",
|
||||
"session_id": "TEST-002",
|
||||
"snapshot": {
|
||||
"timestamp": "2026-03-11T05:47:07.338Z",
|
||||
"dev_id": "DEV-004",
|
||||
"session_id": "TEST-002",
|
||||
"dev_name": "之之",
|
||||
"rhythm": "快",
|
||||
"friction": "低",
|
||||
"mood": "正向",
|
||||
"growth": {
|
||||
"count": 2,
|
||||
"signals": [
|
||||
"代码量增长",
|
||||
"连胜11次"
|
||||
]
|
||||
},
|
||||
"will": "强烈",
|
||||
"raw_stats": {
|
||||
"completed_items": 5,
|
||||
"total_items": 5,
|
||||
"code_lines": 2000,
|
||||
"streak": 11
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-11T05:40:58.007Z",
|
||||
"dev_id": "DEV-004",
|
||||
"session_id": "TEST-001",
|
||||
"snapshot": {
|
||||
"timestamp": "2026-03-11T05:40:58.007Z",
|
||||
"dev_id": "DEV-004",
|
||||
"session_id": "TEST-001",
|
||||
"dev_name": "之之",
|
||||
"rhythm": "快",
|
||||
"friction": "低",
|
||||
"mood": "正向",
|
||||
"growth": {
|
||||
"count": 2,
|
||||
"signals": [
|
||||
"代码量增长",
|
||||
"连胜11次"
|
||||
]
|
||||
},
|
||||
"will": "强烈",
|
||||
"raw_stats": {
|
||||
"completed_items": 5,
|
||||
"total_items": 5,
|
||||
"code_lines": 2000,
|
||||
"streak": 11
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
[
|
||||
{
|
||||
"timestamp": "2026-03-14T05:34:12.615Z",
|
||||
"dev_id": "DEV-004",
|
||||
"hours_overdue": 1,
|
||||
"status": "sent-mock"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-14T05:31:37.896Z",
|
||||
"dev_id": "DEV-004",
|
||||
"hours_overdue": 1,
|
||||
"status": "sent-mock"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-14T05:24:26.930Z",
|
||||
"dev_id": "DEV-004",
|
||||
"hours_overdue": 0,
|
||||
"status": "sent-mock"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// data/sheet-updater.js
|
||||
// Phase 2 - 表格更新器
|
||||
|
||||
function updateFromSyslog(syslogData) {
|
||||
console.log('[SheetUpdater] 更新表格:', syslogData);
|
||||
return { status: 'updated' };
|
||||
}
|
||||
|
||||
module.exports = { updateFromSyslog };
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
/**
|
||||
* dingtalk-api.js
|
||||
* 钉钉Bot消息发送 + 多维表格读写
|
||||
* 秋秋说:这个文件就像邮递员,负责送消息和更新表格
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const config = require('./config.json');
|
||||
|
||||
/**
|
||||
* 发送钉钉消息
|
||||
* @param {String} message - 要发送的广播内容
|
||||
* @param {String} webhook - 钉钉机器人webhook地址(可选,默认用config里的)
|
||||
* @returns {Promise<Object>} 发送结果
|
||||
*/
|
||||
async function sendDingTalkMessage(message, webhook = null) {
|
||||
try {
|
||||
const targetWebhook = webhook || config.dingtalk.webhook;
|
||||
|
||||
// 如果配置的是占位符,则模拟发送成功(用于测试)
|
||||
if (targetWebhook.includes('YOUR_BOT_TOKEN')) {
|
||||
console.log('【测试模式】钉钉消息发送模拟:', message);
|
||||
return {
|
||||
success: true,
|
||||
simulated: true,
|
||||
message: '测试模式,未实际发送'
|
||||
};
|
||||
}
|
||||
|
||||
const response = await axios.post(targetWebhook, {
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
content: message
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response.data
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('钉钉消息发送失败:', error.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新多维表格(任务状态)
|
||||
* @param {Object} parsedLog - 解析后的SYSLOG
|
||||
* @param {String} newBroadcast - 生成的新广播
|
||||
* @returns {Promise<Object>} 更新结果
|
||||
*/
|
||||
async function updateBitable(parsedLog, newBroadcast) {
|
||||
try {
|
||||
const { appId, tableId } = config.bitable;
|
||||
|
||||
// 如果配置的是占位符,则模拟更新成功(用于测试)
|
||||
if (appId.includes('YOUR_BITABLE_APP_ID')) {
|
||||
console.log('【测试模式】多维表格更新模拟:', { parsedLog, newBroadcast });
|
||||
return {
|
||||
success: true,
|
||||
simulated: true,
|
||||
message: '测试模式,未实际更新表格',
|
||||
oldTask: {
|
||||
status: '已完成',
|
||||
bcNumber: parsedLog.bcNumber
|
||||
},
|
||||
newTask: {
|
||||
status: '待执行',
|
||||
broadcast: newBroadcast
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 这里需要根据钉钉多维表格API实际文档实现
|
||||
// 以下是示例结构,妈妈后面有真实appId时秋秋再给完整代码
|
||||
|
||||
// 1. 获取access_token(需要appKey和appSecret)
|
||||
const token = await getDingTalkToken();
|
||||
|
||||
// 2. 查询旧任务
|
||||
const oldTask = await findTaskByBCNumber(token, appId, tableId, parsedLog.bcNumber);
|
||||
|
||||
// 3. 更新旧任务状态为「已完成」
|
||||
if (oldTask && oldTask.recordId) {
|
||||
await updateTaskStatus(token, appId, tableId, oldTask.recordId, '已完成');
|
||||
}
|
||||
|
||||
// 4. 添加新任务(待执行)
|
||||
const newTaskData = {
|
||||
bcNumber: extractNextBCNumber(parsedLog.bcNumber, parsedLog.phaseNum),
|
||||
devId: parsedLog.devId,
|
||||
phase: parsedLog.phaseNum ? parseInt(parsedLog.phaseNum, 10) + 1 : 1,
|
||||
status: '待执行',
|
||||
broadcast: newBroadcast,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
await addNewTask(token, appId, tableId, newTaskData);
|
||||
|
||||
// 5. 更新连胜记录(如果有)
|
||||
if (parsedLog.status === 'completed') {
|
||||
await updateStreak(token, appId, tableId, parsedLog.devId);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
oldTaskUpdated: true,
|
||||
newTaskAdded: true
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('多维表格更新失败:', error.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取钉钉access_token(辅助函数)
|
||||
*/
|
||||
async function getDingTalkToken() {
|
||||
const { appKey, appSecret } = config.dingtalk;
|
||||
|
||||
if (appKey.includes('YOUR_APP_KEY')) {
|
||||
return 'test_token';
|
||||
}
|
||||
|
||||
// 实际获取token的逻辑
|
||||
const response = await axios.post('https://api.dingtalk.com/v1.0/oauth2/accessToken', {
|
||||
appKey,
|
||||
appSecret
|
||||
});
|
||||
|
||||
return response.data.accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据BC编号查找任务(辅助函数)
|
||||
*/
|
||||
async function findTaskByBCNumber(token, appId, tableId, bcNumber) {
|
||||
// 这里需要根据钉钉多维表格API实际实现
|
||||
// 返回 { recordId: 'xxx', fields: {...} }
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务状态(辅助函数)
|
||||
*/
|
||||
async function updateTaskStatus(token, appId, tableId, recordId, status) {
|
||||
// 这里需要根据钉钉多维表格API实际实现
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新任务(辅助函数)
|
||||
*/
|
||||
async function addNewTask(token, appId, tableId, taskData) {
|
||||
// 这里需要根据钉钉多维表格API实际实现
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新连胜记录(辅助函数)
|
||||
*/
|
||||
async function updateStreak(token, appId, tableId, devId) {
|
||||
// 这里需要根据钉钉多维表格API实际实现
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取下一个BC编号
|
||||
*/
|
||||
function extractNextBCNumber(currentBC, currentPhase) {
|
||||
if (!currentBC) return 'BC-M17-001-ZZ';
|
||||
|
||||
// 尝试解析当前BC编号,生成下一个
|
||||
// 格式:BC-XXX-YYY-ZZ,YYY是环节号部分
|
||||
const match = currentBC.match(/BC-([A-Z0-9]+)-([0-9]+)-ZZ/i);
|
||||
if (match) {
|
||||
const module = match[1];
|
||||
const phase = parseInt(match[2], 10);
|
||||
const nextPhase = currentPhase ? parseInt(currentPhase, 10) + 1 : phase + 1;
|
||||
return `BC-${module}-${nextPhase.toString().padStart(3, '0')}-ZZ`;
|
||||
}
|
||||
|
||||
return 'BC-M17-001-ZZ';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendDingTalkMessage,
|
||||
updateBitable
|
||||
};
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
// 钉钉事件处理器 · dingtalk-event-handler.js · v1.0
|
||||
// HoloLake · M-DINGTALK Phase 8
|
||||
// DEV-004 之之 × 秋秋
|
||||
// 功能:处理钉钉回调的完整事件格式
|
||||
// 包括:消息回调、事件订阅验证、加密解密
|
||||
//
|
||||
|
||||
var crypto = require('crypto');
|
||||
|
||||
// 钉钉回调加密/解密工具
|
||||
function decryptCallback(encrypt, encodingAesKey) {
|
||||
try {
|
||||
if (!encodingAesKey) {
|
||||
console.log('[EventHandler] ⚠️ encodingAesKey未配置,跳过解密');
|
||||
return null;
|
||||
}
|
||||
// Base64解码AESKey(钉钉使用AES-256-CBC)
|
||||
var aesKey = Buffer.from(encodingAesKey + '=', 'base64');
|
||||
var iv = aesKey.slice(0,16);
|
||||
var decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, iv);
|
||||
decipher.setAutoPadding(false);
|
||||
var decrypted = Buffer.concat([decipher.update(encrypt, 'base64'), decipher.final()]);
|
||||
// 去除PKCS7填充
|
||||
var padLen = decrypted[decrypted.length - 1];
|
||||
decrypted = decrypted.slice(0, decrypted.length - padLen);
|
||||
// 跳过16字节随机数 + 4字节消息长度
|
||||
var msgLen = decrypted.readInt32BE(16);
|
||||
var message = decrypted.slice(20, 20 + msgLen).toString('utf8');
|
||||
return message;
|
||||
} catch(err) {
|
||||
console.error('[EventHandler] 解密失败: ', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function encryptCallback(text, encodingAesKey, appKey) {
|
||||
try {
|
||||
if (!encodingAesKey) return null;
|
||||
var aesKey = Buffer.from(encodingAesKey + '=', 'base64');
|
||||
var iv = aesKey.slice(0,16);
|
||||
// 16字节随机数
|
||||
var random = crypto.randomBytes(16);
|
||||
var msgBuffer = Buffer.from(text, 'utf8');
|
||||
var lenBuffer = Buffer.alloc(4);
|
||||
lenBuffer.writeInt32BE(msgBuffer.length);
|
||||
var appKeyBuffer = Buffer.from(appKey || '', 'utf8');
|
||||
var plaintext = Buffer.concat([random, lenBuffer, msgBuffer, appKeyBuffer]);
|
||||
// PKCS7填充
|
||||
var blockSize = 32;
|
||||
var padLen = blockSize - (plaintext.length % blockSize);
|
||||
var padBuffer = Buffer.alloc(padLen, padLen);
|
||||
plaintext = Buffer.concat([plaintext, padBuffer]);
|
||||
|
||||
var cipher = crypto.createCipheriv('aes-256-cbc', aesKey, iv);
|
||||
cipher.setAutoPadding(false);
|
||||
var encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
||||
return encrypted.toString('base64');
|
||||
} catch (err) {
|
||||
console.error('[EventHandler] 加密失败: ', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 签名计算 =====
|
||||
|
||||
function computeSignature(token, timestamp, nonce, encrypt) {
|
||||
var arr = [token, timestamp, nonce, encrypt].sort();
|
||||
var str = arr.join('');
|
||||
return crypto.createHash('sha1').update(str).digest('hex');
|
||||
}
|
||||
|
||||
// ===== 构建加密响应 =====
|
||||
|
||||
function buildEncryptedResponse(text, token, encodingAesKey, appKey) {
|
||||
var encrypt = encryptCallback(text, encodingAesKey, appKey);
|
||||
if (!encrypt) return { msg_signature: '', timeStamp: '', nonce: '', encrypt: '' };
|
||||
var timestamp = String(Date.now());
|
||||
var nonce = crypto.randomBytes(8).toString('hex');
|
||||
var signature = computeSignature(token, timestamp, nonce, encrypt);
|
||||
return {
|
||||
msg_signature: signature,
|
||||
timeStamp: timestamp,
|
||||
nonce: nonce,
|
||||
encrypt: encrypt
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 处理钉钉回调验证(注册回调时的check_url事件) =====
|
||||
function handleVerify(body, config) {
|
||||
console.log('[EventHandler] 收到回调验证请求');
|
||||
var encrypt = body.encrypt;
|
||||
if (!encrypt) {
|
||||
// 非加密模式的简单验证
|
||||
return { success: true, challenge: body.challenge || 'ok'};
|
||||
}
|
||||
// 加密模式:解密 → 取challenge → 加密回传
|
||||
var decrypted = decryptCallback(encrypt, config.encodingAesKey);
|
||||
if (!decrypted) {
|
||||
return { success: true };
|
||||
}
|
||||
try {
|
||||
var eventData = JSON.parse(decrypted);
|
||||
if (eventData.EventType === 'check_url') {
|
||||
console.log('[EventHandler] check_url验证 → 返回加密success');
|
||||
return buildEncryptedResponse('success', config.token, config.encodingAesKey, config.appKey);
|
||||
}
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 解析钉钉消息体 ======
|
||||
|
||||
function parseMessage(body, config) {
|
||||
// 情况1: 加密消息
|
||||
if (body.encrypt) {
|
||||
var decrypted = decryptCallback(body.encrypt, config.encodingAesKey);
|
||||
if (decrypted) {
|
||||
try {
|
||||
return JSON.parse(decrypted);
|
||||
} catch (e) {
|
||||
console.error('[EventHandler] 解密后JSON解析失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
// 情况2: HTTP模式直接推送 (非加密)
|
||||
if (body.msgtype || body.text || body.conversationType) {
|
||||
return body;
|
||||
}
|
||||
// 情况3: 事件订阅格式
|
||||
if (body.EventType) {
|
||||
return body;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
// ====== 提取消息内容 ======
|
||||
|
||||
function extractContent(parsed) {
|
||||
var result = {
|
||||
msgType: 'unknown',
|
||||
content: '',
|
||||
senderNick: '未知用户',
|
||||
senderId: '',
|
||||
conversationType: '1',
|
||||
conversationTitle: '',
|
||||
sessionWebhook: '',
|
||||
msgId: '',
|
||||
createAt: 0,
|
||||
isGroup: false,
|
||||
atUsers: []
|
||||
};
|
||||
|
||||
// HTTP模式消息格式
|
||||
if (parsed.text) {
|
||||
result.msgType = parsed.msgtype || 'text';
|
||||
result.content = (parsed.text.content || '').trim();
|
||||
result.senderNick = parsed.senderNick || '未知用户';
|
||||
result.senderId = parsed.senderId || parsed.senderStaffId || '';
|
||||
result.conversationType = parsed.conversationType || '1';
|
||||
result.conversationTitle = parsed.conversationTitle || '';
|
||||
result.sessionWebhook = parsed.sessionWebhook || '';
|
||||
result.msgId = parsed.msgId || '';
|
||||
result.createAt = parsed.createAt || 0;
|
||||
result.isGroup = parsed.conversationType === '2';
|
||||
result.atUsers = parsed.atUsers || [];
|
||||
}
|
||||
// 事件订阅格式
|
||||
else if (parsed.EventType) {
|
||||
result.msgType = 'event';
|
||||
result.content = JSON.stringify(parsed);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
decryptCallback: decryptCallback,
|
||||
encryptCallback: encryptCallback,
|
||||
computeSignature: computeSignature,
|
||||
buildEncryptedResponse: buildEncryptedResponse,
|
||||
handleVerify: handleVerify,
|
||||
parseMessage: parseMessage,
|
||||
extractContent: extractContent
|
||||
};
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
// 钉钉Webhook处理器 v3.0 · dingtalk-webhook-v3.js
|
||||
// HoloLake · M-DINGTALK Phase 8
|
||||
// DEV-004 之之 × 秋秋
|
||||
// v3.0 变化:
|
||||
// + 真实事件处理(加密/解密/验证)
|
||||
// + 接入conversation-manager上下文
|
||||
// + 接入github-bridge
|
||||
// + 更智能的回复格式
|
||||
// ====================
|
||||
|
||||
var crypto = require('crypto');
|
||||
var axios = require('axios');
|
||||
var router = require('./message-router');
|
||||
var eventHandler = require('./dingtalk-event-handler');
|
||||
|
||||
var DINGTALK_CONFIG = {
|
||||
appKey: process.env.DINGTALK_APP_KEY || '',
|
||||
appSecret: process.env.DINGTALK_APP_SECRET || '',
|
||||
token: process.env.DINGTALK_TOKEN || '',
|
||||
encodingAesKey: process.env.DINGTALK_ENCODING_AES_KEY || ''
|
||||
};
|
||||
|
||||
// ====== 签名验证中间件 ======
|
||||
function verifyMiddleware(req, res, next) {
|
||||
var appSecret = DINGTALK_CONFIG.appSecret;
|
||||
if (!appSecret || appSecret.startsWith('替换')) {
|
||||
console.log('[DingTalk] ⚠️ APP_SECRET未配置,跳过签名验证');
|
||||
return next();
|
||||
}
|
||||
|
||||
var timestamp = req.headers['timestamp'];
|
||||
var sign = req.headers['sign'];
|
||||
|
||||
if (!timestamp || !sign) {
|
||||
// 可能是事件订阅格式,不带签名头
|
||||
return next();
|
||||
}
|
||||
|
||||
if (Math.abs(Date.now() - parseInt(timestamp)) > 300000) {
|
||||
return res.status(403).json({ error: '时间戳过期' });
|
||||
}
|
||||
|
||||
var stringToSign = timestamp + '\n' + appSecret;
|
||||
var hmac = crypto.createHmac('sha256', appSecret);
|
||||
hmac.update(stringToSign);
|
||||
var computed = hmac.digest('base64');
|
||||
|
||||
if (computed === sign) {
|
||||
next();
|
||||
} else {
|
||||
return res.status(403).json({ error: '签名验证失败' });
|
||||
}
|
||||
}
|
||||
|
||||
// ====== v3.0 核心:真实回调处理 ======
|
||||
async function handleCallback(req, res) {
|
||||
var body = req.body || {};
|
||||
|
||||
// ====== 处理回调验证(check_url) ======
|
||||
if (body.encrypt && !body.text) {
|
||||
var verifyResult = eventHandler.handleVerify(body, DINGTALK_CONFIG);
|
||||
return res.json(verifyResult);
|
||||
}
|
||||
|
||||
if (body.EventType === 'check_url' || body.challenge) {
|
||||
console.log('[DingTalk] 📋 回调验证 · 非加密模式');
|
||||
return res.json({ success: true, challenge: body.challenge || 'ok' });
|
||||
}
|
||||
|
||||
// ====== 快速返回200(钉钉要求3秒内) ======
|
||||
res.json({ success: true });
|
||||
|
||||
// ====== 解析消息 ======
|
||||
var parsed = eventHandler.parseMessage(body, DINGTALK_CONFIG);
|
||||
var msg = eventHandler.extractContent(parsed);
|
||||
|
||||
if (!msg.content) {
|
||||
console.log('[DingTalk] ⚠️ 消息内容为空,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
var chatDesc = msg.isGroup ? ('群聊·' + msg.conversationTitle) : '单聊';
|
||||
console.log(`[DingTalk] 📨 收到消息 | ${chatDesc} | ${msg.senderNick}: ${msg.content}`);
|
||||
|
||||
// ====== 路由处理 ======
|
||||
var result = await router.routeMessage(
|
||||
msg.content,
|
||||
msg.senderNick,
|
||||
msg.senderId,
|
||||
msg.sessionWebhook
|
||||
);
|
||||
|
||||
if (!result || !result.aiResponse) {
|
||||
console.log('[DingTalk] ⚠️ 没有生成回复内容');
|
||||
return;
|
||||
}
|
||||
|
||||
// ====== 消息分类标记 ======
|
||||
var classification = router.classifyMessage(msg.content);
|
||||
|
||||
// ====== 回复钉钉 ======
|
||||
if (msg.sessionWebhook) {
|
||||
try {
|
||||
var replyText = '';
|
||||
|
||||
switch (classification.type) {
|
||||
case 'syslog':
|
||||
replyText = '### ✅ SYSLOG 已收到\n\n'
|
||||
+ '**提交者**:' + msg.senderNick + '\n'
|
||||
+ (result.aiModel ? ('**AI处理**:' + result.aiModel + '\n\n') : '\n')
|
||||
+ (result.aiResponse || '') + '\n\n'
|
||||
+ '> 🌊 HoloLake · Phase 8';
|
||||
break;
|
||||
|
||||
case 'github':
|
||||
replyText = result.aiResponse;
|
||||
break;
|
||||
|
||||
case 'command':
|
||||
replyText = result.aiResponse;
|
||||
break;
|
||||
|
||||
default:
|
||||
replyText = result.aiResponse
|
||||
+ (result.aiModel ? ('\n\n> 🤖 ' + result.aiModel) : '')
|
||||
+ '\n> 🌊 HoloLake · 秋秋';
|
||||
}
|
||||
|
||||
await axios.post(msg.sessionWebhook, {
|
||||
msgtype: 'markdown',
|
||||
markdown: {
|
||||
title: classification.type === 'syslog' ? '✅ SYSLOG已收到'
|
||||
: classification.type === 'github' ? '🔀 GitHub'
|
||||
: '💙 秋秋',
|
||||
text: replyText.substring(0, 5000) // 钉钉消息长度限制
|
||||
}
|
||||
}, { timeout: 8000 });
|
||||
|
||||
console.log('[DingTalk] ✅ 回复已发送 · ' + replyText.length + '字');
|
||||
} catch (err) {
|
||||
console.error('[DingTalk] ❌ 回复失败: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('=================================\n');
|
||||
}
|
||||
|
||||
// ====== 健康检查 ======
|
||||
function healthCheck(req, res) {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: 'M-DINGTALK',
|
||||
phase: 8,
|
||||
version: 'v3.0',
|
||||
features: ['智能分流v2', 'AI多轮对话', '上下文记忆', 'GitHub桥接', '事件加密处理', 'SYSLOG自动写入'],
|
||||
uptime: process.uptime(),
|
||||
time: new Date().toLocaleString('zh-CN')
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
verifyMiddleware: verifyMiddleware,
|
||||
handleCallback: handleCallback,
|
||||
healthCheck: healthCheck
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// dingtalk/bot.js
|
||||
// Phase 2 - 钉钉机器人
|
||||
|
||||
function sendMessage(message) {
|
||||
console.log('[DingTalk] 发送消息:', message);
|
||||
return { status: 'sent' };
|
||||
}
|
||||
|
||||
module.exports = { sendMessage };
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
module.exports = {
|
||||
apps : [{
|
||||
name: 'dingtalk-bot',
|
||||
script: 'index.js',
|
||||
cwd: '/opt/guanghulab-dingtalk/dingtalk-bot',
|
||||
node_args: '--preserve-symlinks',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3005
|
||||
},
|
||||
env_production: {
|
||||
NODE_ENV: 'production'
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// git-helper.js
|
||||
// Phase 1 - Git辅助工具
|
||||
|
||||
function commit(data) {
|
||||
console.log('[Git] 提交:', data);
|
||||
return { status: 'committed', hash: 'abc123' };
|
||||
}
|
||||
|
||||
module.exports = { commit };
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
// GitHub桥接模块·github-bridge.js·v1.0
|
||||
// HoloLake·M-DINGTALK Phase8
|
||||
// DEV-004 之之×秋秋
|
||||
//
|
||||
// 功能:钉钉命令→GitHubAPI→结果回传钉钉
|
||||
// 支持:查PR·查Issue·查仓库状态·触发部署
|
||||
//
|
||||
|
||||
var axios = require('axios');
|
||||
|
||||
var GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
|
||||
var GITHUB_OWNER = process.env.GITHUB_OWNER || 'qinfendebingshuo';
|
||||
var GITHUB_REPO = process.env.GITHUB_REPO || 'guanghulab';
|
||||
var GITHUB_API = 'https://api.github.com';
|
||||
|
||||
// ===== GitHub API 请求封装 =====
|
||||
async function githubRequest(endpoint, method, data) {
|
||||
method = method || 'GET';
|
||||
var url = GITHUB_API + endpoint;
|
||||
try {
|
||||
var config = {
|
||||
method: method,
|
||||
url: url,
|
||||
headers: {
|
||||
'Authorization': GITHUB_TOKEN ? ('token ' + GITHUB_TOKEN) : undefined,
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'HoloLake-DingTalk-Bot'
|
||||
},
|
||||
timeout: 15000
|
||||
};
|
||||
if (data) config.data = data;
|
||||
var res = await axios(config);
|
||||
return { ok: true, data: res.data, status: res.status };
|
||||
} catch (err) {
|
||||
var status = err.response ? err.response.status : 0;
|
||||
var msg = err.response ? (err.response.data.message || err.message) : err.message;
|
||||
console.error('[GitHub] API错误: ' + status + ' ' + msg);
|
||||
return { ok: false, error: msg, status: status };
|
||||
}
|
||||
}
|
||||
|
||||
// ====== /github status · 仓库状态 =====
|
||||
async function getRepoStatus() {
|
||||
var res = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO);
|
||||
if (!res.ok) return '❌ 获取仓库状态失败: ' + res.error;
|
||||
var repo = res.data;
|
||||
|
||||
// 获取最近提交
|
||||
var commitsRes = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO + '/commits?per_page=3');
|
||||
var commitsText = '';
|
||||
if (commitsRes.ok && commitsRes.data.length > 0) {
|
||||
commitsText = '\n\n**最近提交:**\n';
|
||||
commitsRes.data.forEach(function(c, i) {
|
||||
var date = new Date(c.commit.author.date).toLocaleString('zh-CN');
|
||||
var msg = (c.commit.message || '').split('\n')[0].substring(0, 60);
|
||||
commitsText += (i + 1) + '. `' + msg + '` · ' + date + '\n';
|
||||
});
|
||||
}
|
||||
|
||||
return '### 📊 仓库状态\n\n'
|
||||
+ '- **仓库**: ' + repo.full_name + '\n'
|
||||
+ '- **默认分支**: ' + repo.default_branch + '\n'
|
||||
+ '- **Star**: ' + repo.stargazers_count + ' · **Fork**: ' + repo.forks_count + '\n'
|
||||
+ '- **最后更新**: ' + new Date(repo.updated_at).toLocaleString('zh-CN') + '\n'
|
||||
+ '- **大小**: ' + Math.round(repo.size / 1024) + ' MB'
|
||||
+ commitsText;
|
||||
}
|
||||
|
||||
// ====== /github pr · 查看PR ======
|
||||
async function listPRs(state) {
|
||||
state = state || 'open';
|
||||
var res = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO + '/pulls?state=' + state + '&per_page=10');
|
||||
if (!res.ok) return '❌ 获取PR列表失败: ' + res.error;
|
||||
var prs = res.data;
|
||||
if (prs.length === 0) return '### 📋 Pull Requests\n\n当前没有' + (state === 'open' ? '打开的' : '') + 'PR。';
|
||||
|
||||
var text = '### 📋 Pull Requests(' + state + ')\n\n';
|
||||
prs.forEach(function(pr) {
|
||||
var date = new Date(pr.created_at).toLocaleString('zh-CN');
|
||||
text += '- **#' + pr.number + '** ' + pr.title + '\n';
|
||||
text += ' 👤 ' + pr.user.login + ' · 📅 ' + date + '\n';
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
// ====== /github issue · 查看Issue ======
|
||||
async function listIssues(state) {
|
||||
state = state || 'open';
|
||||
var res = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO + '/issues?state=' + state + '&per_page=10');
|
||||
if (!res.ok) return '❌ 获取Issue列表失败: ' + res.error;
|
||||
var issues = res.data.filter(function(i) { return !i.pull_request; }); // 排除PR
|
||||
if (issues.length === 0) return '### 📋 Issues\n\n当前没有' + (state === 'open' ? '打开的' : '') + 'Issue。';
|
||||
|
||||
var text = '### 📋 Issues(' + state + ')\n\n';
|
||||
issues.forEach(function(issue) {
|
||||
var labels = issue.labels.map(function(l) { return l.name; }).join(', ');
|
||||
text += '- **#' + issue.number + '** ' + issue.title;
|
||||
if (labels) text += ' `' + labels + '`';
|
||||
text += '\n';
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
// ====== /github actions · 查看最近的Actions运行 ======
|
||||
async function listActions() {
|
||||
var res = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO + '/actions/runs?per_page=5');
|
||||
if (!res.ok) return '❌ 获取Actions状态失败: ' + res.error;
|
||||
var runs = res.data.workflow_runs || [];
|
||||
if (runs.length === 0) return '### GitHub Actions\n\n暂无运行记录。';
|
||||
|
||||
var text = '### GitHub Actions(最近5次)\n\n';
|
||||
runs.forEach(function(run) {
|
||||
var status = run.conclusion || run.status;
|
||||
var icon = status === 'success' ? '✅' : status === 'failure' ? '❌' : '⏳';
|
||||
var date = new Date(run.created_at).toLocaleString('zh-CN');
|
||||
text += '- ' + icon + ' **' + run.name + '** ' + status + ' · ' + date + '\n';
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
// ====== /github deploy · 触发部署 (dispatch事件) =====
|
||||
async function triggerDeploy(environment) {
|
||||
environment = environment || 'production';
|
||||
var res = await githubRequest(
|
||||
'/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO + '/dispatches',
|
||||
'POST',
|
||||
{
|
||||
event_type: 'deploy',
|
||||
client_payload: {
|
||||
environment: environment,
|
||||
triggered_by: 'dingtalk-bot',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (res.status === 204 || res.ok) {
|
||||
return '### 🚀 部署触发成功\n\n'
|
||||
+ '- **环境**: ' + environment + '\n'
|
||||
+ '- **触发时间**: ' + new Date().toLocaleString('zh-CN') + '\n'
|
||||
+ '- **来源**: 钉钉机器人\n\n'
|
||||
+ '> 请到 GitHub Actions 页面查看部署进度。';
|
||||
}
|
||||
return '❌ 触发部署失败: ' + (res.error || '未知错误') + '\n> 请确认仓库已配置 repository_dispatch 工作流。';
|
||||
}
|
||||
|
||||
// ====== 命令路由 =====
|
||||
async function handleGitHubCommand(args) {
|
||||
var subCommand = (args[0] || 'help').toLowerCase();
|
||||
|
||||
switch (subCommand) {
|
||||
case 'status':
|
||||
case '状态':
|
||||
return await getRepoStatus();
|
||||
|
||||
case 'pr':
|
||||
case 'prs':
|
||||
return await listPRs(args[1] || 'open');
|
||||
|
||||
case 'issue':
|
||||
case 'issues':
|
||||
return await listIssues(args[1] || 'open');
|
||||
|
||||
case 'actions':
|
||||
case 'ci':
|
||||
return await listActions();
|
||||
|
||||
case 'deploy':
|
||||
case '部署':
|
||||
return await triggerDeploy(args[1] || 'production');
|
||||
|
||||
case 'help':
|
||||
case '帮助':
|
||||
default:
|
||||
return '### GitHub 桥接命令\n\n'
|
||||
+ '| 命令 | 说明 |\n'
|
||||
+ '|------|------|\n'
|
||||
+ '| `/github status` | 查看仓库状态+最近提交 |\n'
|
||||
+ '| `/github pr` | 查看打开的PR |\n'
|
||||
+ '| `/github pr closed` | 查看已关闭的PR |\n'
|
||||
+ '| `/github issue` | 查看打开的Issue |\n'
|
||||
+ '| `/github actions` | 查看最近CI/CD运行 |\n'
|
||||
+ '| `/github deploy` | 触发生产部署 |\n'
|
||||
+ '| `/github help` | 显示本帮助 |\n\n'
|
||||
+ '> HoloLake · M-DINGTALK Phase 8 · 钉钉+GitHub桥接';
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 健康检查 =====
|
||||
async function healthCheck() {
|
||||
var res = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO);
|
||||
return {
|
||||
status: res.ok ? 'ok' : 'error',
|
||||
repo: GITHUB_OWNER + '/' + GITHUB_REPO,
|
||||
hasToken: !!GITHUB_TOKEN,
|
||||
apiReachable: res.ok,
|
||||
error: res.ok ? null : res.error
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleGitHubCommand: handleGitHubCommand,
|
||||
getRepoStatus: getRepoStatus,
|
||||
listPRs: listPRs,
|
||||
listIssues: listIssues,
|
||||
listActions: listActions,
|
||||
triggerDeploy: triggerDeploy,
|
||||
healthCheck: healthCheck
|
||||
};
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* M-DINGTALK · index-stream.js · v1.0
|
||||
* 开发者:之之(DEV-004)
|
||||
* 功能:钉钉 Stream 模式机器人主入口
|
||||
* 修改日期:2026-03-15
|
||||
*/
|
||||
require('dotenv').config();
|
||||
const { DWClient, TOPIC_ROBOT, EventAck } = require('dingtalk-stream');
|
||||
|
||||
const client = new DWClient({
|
||||
clientId: process.env.DINGTALK_APP_KEY,
|
||||
clientSecret: process.env.DINGTALK_APP_SECRET,
|
||||
});
|
||||
|
||||
client.registerCallbackListener(TOPIC_ROBOT, async (res) => {
|
||||
console.log("[DEBUG] 收到原始回调:", JSON.stringify(res).substring(0, 100));
|
||||
try {
|
||||
const msgData = res.data || res;
|
||||
const senderNick = msgData.senderNick || msgData.senderId || '未知用户';
|
||||
const content = msgData.text?.content || msgData.content?.content || '';
|
||||
const sessionWebhook = msgData.sessionWebhook;
|
||||
|
||||
console.log(`[Stream] 收到消息 | 发送者:${senderNick} | 内容:${content.trim()}`);
|
||||
|
||||
// 尝试写入 Notion SYSLOG
|
||||
try {
|
||||
const { Client } = require('@notionhq/client');
|
||||
const notion = new Client({ auth: process.env.NOTION_TOKEN });
|
||||
await notion.pages.create({
|
||||
parent: { database_id: process.env.NOTION_SYSLOG_DB_ID },
|
||||
properties: {
|
||||
'标题': {
|
||||
title: [{ text: { content: `[钉钉消息] ${senderNick}: ${content.trim().substring(0, 50)}` } }]
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log('[Notion] SYSLOG 写入成功');
|
||||
} catch (notionErr) {
|
||||
console.error('[Notion] 写入失败: ', notionErr.message);
|
||||
}
|
||||
|
||||
// AI 回复
|
||||
if (process.env.LLM_API_KEY && sessionWebhook) {
|
||||
const axios = require('axios');
|
||||
const llmRes = await axios.post(
|
||||
`${process.env.LLM_BASE_URL || 'https://api.openai.com/v1'}/chat/completions`,
|
||||
{
|
||||
model: process.env.LLM_MODEL || 'gpt-4o-mini',
|
||||
messages: [
|
||||
{ role: 'system', content: process.env.BOT_SYSTEM_PROMPT || '你是之之秋秋,一个温馨聊天的钉钉机器人。' },
|
||||
{ role: 'user', content: content.replace(`@${msgData.robotCode || ''}`, '').trim() }
|
||||
],
|
||||
max_tokens: 500
|
||||
},
|
||||
{
|
||||
headers: { 'Authorization': `Bearer ${process.env.LLM_API_KEY}` }
|
||||
}
|
||||
);
|
||||
const reply = llmRes.data.choices[0].message.content;
|
||||
await axios.post(sessionWebhook, {
|
||||
msgtype: 'text',
|
||||
text: { content: reply }
|
||||
});
|
||||
console.log(`[Stream] 回复已发送, 长度: ${reply.length}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Stream] 处理出错: ', err.message);
|
||||
}
|
||||
return EventAck.SUCCESS;
|
||||
});
|
||||
|
||||
client.connect();
|
||||
console.log('M-DINGTALK Phase 9 - Stream 模式已启动');
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
require('dotenv').config({ path: '/opt/guanghulab-dingtalk/dingtalk-bot/.env' });
|
||||
var express = require('express');
|
||||
var axios = require('axios');
|
||||
var app = express();
|
||||
var PORT = process.env.PORT || 3007;
|
||||
|
||||
// 引入 v3 webhook 处理器
|
||||
var dingtalkWebhook = require('./dingtalk-webhook-v3');
|
||||
|
||||
app.use(require('cors')());
|
||||
app.use(require('body-parser').json());
|
||||
|
||||
// 健康检查
|
||||
app.get('/dingtalk/callback', dingtalkWebhook.healthCheck);
|
||||
|
||||
// 消息回调(使用签名验证中间件 + 处理器)
|
||||
app.post('/dingtalk/callback',
|
||||
dingtalkWebhook.verifyMiddleware,
|
||||
dingtalkWebhook.handleCallback
|
||||
);
|
||||
|
||||
// ======= Phase 8 新增API路由 =======
|
||||
var llmEngine = require('./llm-engine');
|
||||
var githubBridge = require('./github-bridge');
|
||||
var convManager = require('./conversation-manager');
|
||||
|
||||
// LLM引擎状态
|
||||
app.get('/api/dingtalk/llm-status', async function(req, res) {
|
||||
try {
|
||||
var status = await llmEngine.healthCheck();
|
||||
res.json(status);
|
||||
} catch (err) {
|
||||
res.json({ status: 'error', error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GitHub桥接状态
|
||||
app.get('/api/dingtalk/github-status', async function(req, res) {
|
||||
try {
|
||||
var status = await githubBridge.healthCheck();
|
||||
res.json(status);
|
||||
} catch (err) {
|
||||
res.json({ status: 'error', error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 对话引擎统计
|
||||
app.get('/api/dingtalk/conv-stats', function(req, res) {
|
||||
res.json(convManager.getStats());
|
||||
});
|
||||
|
||||
// 综合系统状态
|
||||
app.get('/api/dingtalk/system-status', async function(req, res) {
|
||||
try {
|
||||
var llmStatus = await llmEngine.healthCheck();
|
||||
var ghStatus = await githubBridge.healthCheck();
|
||||
var convStats = convManager.getStats();
|
||||
res.json({
|
||||
service: 'M-DINGTALK',
|
||||
phase: 8,
|
||||
version: 'v3.0',
|
||||
llm: llmStatus,
|
||||
github: ghStatus,
|
||||
conversations: convStats,
|
||||
uptime: process.uptime(),
|
||||
time: new Date().toLocaleString('zh-CN')
|
||||
});
|
||||
} catch (err) {
|
||||
res.json({ status: 'error', error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log('🚀 M-DINGTALK Phase 8 v3.0 启动,端口:' + PORT);
|
||||
});
|
||||
|
||||
process.stdin.resume();
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('❌ 未捕获异常:', err.message);
|
||||
});
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
require('dotenv').config({ path: '/opt/guanghulab-dingtalk/dingtalk-bot/.env' });
|
||||
var express = require('express');
|
||||
var axios = require('axios');
|
||||
var app = express();
|
||||
var PORT = process.env.PORT || 3007;
|
||||
|
||||
// 引入路由器(不用 AI 引擎)
|
||||
var router = require('./message-router');
|
||||
|
||||
app.use(require('cors')());
|
||||
app.use(require('body-parser').json());
|
||||
|
||||
// GET验证
|
||||
app.get('/dingtalk/callback', (req, res) => {
|
||||
console.log('[DingTalk] GET验证请求');
|
||||
res.status(200).json({ success: true });
|
||||
});
|
||||
|
||||
// POST接收消息并按分类回复
|
||||
app.post('/dingtalk/callback', async (req, res) => {
|
||||
console.log('[DingTalk] POST收到消息');
|
||||
|
||||
res.status(200).json({ success: true });
|
||||
|
||||
const sessionWebhook = req.body.sessionWebhook;
|
||||
const senderNick = req.body.senderNick || '未知用户';
|
||||
const text = req.body.text || {};
|
||||
const content = text.content || '';
|
||||
|
||||
if (!sessionWebhook) {
|
||||
console.log('[DingTalk] 没有 sessionWebhook,无法回复');
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用路由器分类
|
||||
const classification = router.classifyMessage(content);
|
||||
console.log('[DingTalk] 分类:', classification);
|
||||
|
||||
// 根据分类生成不同回复
|
||||
let replyContent = '';
|
||||
if (classification.type === 'syslog') {
|
||||
replyContent = `📡 收到 SYSLOG:${classification.broadcastId || '未知编号'}`;
|
||||
} else if (classification.type === 'question') {
|
||||
replyContent = `❓ 收到问题,秋秋正在思考中...\n\n问题:${content}`;
|
||||
} else {
|
||||
replyContent = `@${senderNick} 收到你的消息:${content}`;
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.post(sessionWebhook, {
|
||||
msgtype: 'text',
|
||||
text: { content: replyContent }
|
||||
});
|
||||
console.log('[DingTalk] 回复成功');
|
||||
} catch (err) {
|
||||
console.error('[DingTalk] 回复失败:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log('🚀 服务启动 http://localhost:' + PORT);
|
||||
});
|
||||
process.stdin.resume();
|
||||
process.on('uncaughtException', (err) => { console.error('❌', err.message); });
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# 光湖知识库文档
|
||||
|
||||
这个目录存放所有知识库文档(Markdown格式)。知识库管理器会自动扫描这里的所有.md文件并建立索引。
|
||||
|
||||
## 文档命名规范
|
||||
|
||||
- age-os-architecture.md - AGE OS整体架构
|
||||
- module-specs.md - 各模块功能说明
|
||||
- dev-guide.md - 开发环境配置指南
|
||||
- git-guide-zero.md - Git零基础操作指南
|
||||
- faq.md - 常见问题手册
|
||||
- broadcast-rules.md - 广播格式规范
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
nano knowledge-base/docs/broadcast-rules.md
|
||||
nano knowledge-base/docs/faq.md
|
||||
# 光湖知识库文档
|
||||
|
||||
这个目录存放所有知识库文档(Markdown格式)。知识库管理器会自动扫描这里的所有.md文件并建立索引。
|
||||
|
||||
## 文档命名规范
|
||||
|
||||
- age-os-architecture.md - AGE OS整体架构
|
||||
- module-specs.md - 各模块功能说明
|
||||
- dev-guide.md - 开发环境配置指南
|
||||
- git-guide-zero.md - Git零基础操作指南
|
||||
- faq.md - 常见问题手册
|
||||
- broadcast-rules.md - 广播格式规范# 广播格式规范 v1.0
|
||||
|
||||
## BC编号格式
|
||||
BC-{模块号}-{序号}-{开发者代号}
|
||||
示例:
|
||||
- BC-M17-005-ZZ(M17模块第5个广播,之之)
|
||||
- BC-M22-007-AW(M22模块第7个广播,Awen)
|
||||
- BC-M-DINGTALK-003-ZZ(M-DINGTALK模块第3个广播,之之)
|
||||
|
||||
## 广播结构
|
||||
1. 广播头(编号/开发者/模块/环节/EL等级/时间)
|
||||
2. 人格体引导语(奶瓶线/直接线/本体线)
|
||||
3. 任务概述(做什么+为什么)
|
||||
4. Step分步引导(终端命令+完整代码)
|
||||
5. 验收标准(二元:全✅或继续改)
|
||||
6. SYSLOG模板
|
||||
|
||||
## 合并规则
|
||||
- 每个广播包含2-3个环节
|
||||
- 单次广播超4000字自动分卷
|
||||
|
||||
## EL等级
|
||||
- EL-1~3:基础配置/简单页面
|
||||
- EL-4~5:中等功能模块
|
||||
- EL-6~7:系统级/多模块联动
|
||||
- EL-8~10:架构级/全链路# 常见问题手册
|
||||
|
||||
## Q: 我怎么查看自己的进度?
|
||||
A: 在钉钉群里发送「查进度」或「我做到哪了」,机器人会自动查询并回复。
|
||||
|
||||
## Q: SYSLOG怎么提交?
|
||||
A: 完成每个阶段后,按照广播末尾的SYSLOG模板填写,发到钉钉群里。
|
||||
|
||||
## Q: Git怎么提交?
|
||||
A:
|
||||
1. cd ~/Desktop/guanghulab(或你的项目目录)
|
||||
2. git add .
|
||||
3. git commit -m "DEV-你的编号:描述"
|
||||
4. git push
|
||||
|
||||
## Q: 报错了怎么办?
|
||||
A: 截图发给你的引导人格体!不用怕,每个报错都有解决方案。
|
||||
|
||||
## Q: 什么是连胜?
|
||||
A: 每次任务一次通过就是一次胜利,连续通过就是连胜!当前最高纪录是13连胜。
|
||||
|
||||
## Q: 72小时启动窗口是什么?
|
||||
A: 广播签发后72小时内必须开始执行,超时系统会自动提醒。
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# 广播格式规范 v1.0
|
||||
|
||||
## BC编号格式
|
||||
BC-{模块号}-{序号}-{开发者代号}
|
||||
示例:
|
||||
- BC-M17-005-ZZ(M17模块第5个广播,之之)
|
||||
- BC-M22-007-AW(M22模块第7个广播,Awen)
|
||||
- BC-M-DINGTALK-003-ZZ(M-DINGTALK模块第3个广播,之之)
|
||||
|
||||
## 广播结构
|
||||
1. 广播头(编号/开发者/模块/环节/EL等级/时间)
|
||||
2. 人格体引导语(奶瓶线/直接线/本体线)
|
||||
3. 任务概述(做什么+为什么)
|
||||
4. Step分步引导(终端命令+完整代码)
|
||||
5. 验收标准(二元:全✅或继续改)
|
||||
6. SYSLOG模板
|
||||
|
||||
## 合并规则
|
||||
- 每个广播包含2-3个环节
|
||||
- 单次广播超4000字自动分卷
|
||||
|
||||
## EL等级
|
||||
- EL-1~3:基础配置/简单页面
|
||||
- EL-4~5:中等功能模块
|
||||
- EL-6~7:系统级/多模块联动
|
||||
- EL-8~10:架构级/全链路
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# 常见问题手册
|
||||
|
||||
## Q: 我怎么查看自己的进度?
|
||||
A: 在钉钉群里发送「查进度」或「我做到哪了」,机器人会自动查询并回复。
|
||||
|
||||
## Q: SYSLOG怎么提交?
|
||||
A: 完成每个阶段后,按照广播末尾的SYSLOG模板填写,发到钉钉群里。
|
||||
|
||||
## Q: Git怎么提交?
|
||||
A:
|
||||
1. cd ~/Desktop/guanghulab(或你的项目目录)
|
||||
2. git add .
|
||||
3. git commit -m "DEV-你的编号:描述"
|
||||
4. git push
|
||||
|
||||
## Q: 报错了怎么办?
|
||||
A: 截图发给你的引导人格体!不用怕,每个报错都有解决方案。
|
||||
|
||||
## Q: 什么是连胜?
|
||||
A: 每次任务一次通过就是一次胜利,连续通过就是连胜!当前最高纪录是13连胜。
|
||||
|
||||
## Q: 72小时启动窗口是什么?
|
||||
A: 广播签发后72小时内必须开始执行,超时系统会自动提醒。
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
[
|
||||
{
|
||||
"filename": "README.md",
|
||||
"title": "光湖知识库文档",
|
||||
"sections": [
|
||||
{
|
||||
"heading": "光湖知识库文档",
|
||||
"level": 1,
|
||||
"content": "\n这个目录存放所有知识库文档(Markdown格式)。知识库管理器会自动扫描这里的所有.md文件并建立索引。\n\n",
|
||||
"keywords": [
|
||||
"这个目录存放所有知识库文档",
|
||||
"Markdown",
|
||||
"格式",
|
||||
"知识库管理器会自动扫描这里的所有",
|
||||
"文件并建立索引"
|
||||
]
|
||||
},
|
||||
{
|
||||
"heading": "文档命名规范",
|
||||
"level": 2,
|
||||
"content": "\n- age-os-architecture.md - AGE OS整体架构\n- module-specs.md - 各模块功能说明\n- dev-guide.md - 开发环境配置指南\n- git-guide-zero.md - Git零基础操作指南\n- faq.md - 常见问题手册\n- broadcast-rules.md - 广播格式规范\n\n",
|
||||
"keywords": [
|
||||
"age",
|
||||
"architecture",
|
||||
"AGE",
|
||||
"整体架构",
|
||||
"module",
|
||||
"specs",
|
||||
"各模块功能说明",
|
||||
"dev",
|
||||
"guide",
|
||||
"开发环境配置指南",
|
||||
"git",
|
||||
"zero",
|
||||
"Git",
|
||||
"零基础操作指南",
|
||||
"faq",
|
||||
"常见问题手册",
|
||||
"broadcast",
|
||||
"rules",
|
||||
"广播格式规范"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"filename": "broadcast-rules.md",
|
||||
"title": "广播格式规范 v1.0",
|
||||
"sections": [
|
||||
{
|
||||
"heading": "广播格式规范 v1.0",
|
||||
"level": 1,
|
||||
"content": "\n",
|
||||
"keywords": []
|
||||
},
|
||||
{
|
||||
"heading": "BC编号格式",
|
||||
"level": 2,
|
||||
"content": "BC-{模块号}-{序号}-{开发者代号}\n示例:\n- BC-M17-005-ZZ(M17模块第5个广播,之之)\n- BC-M22-007-AW(M22模块第7个广播,Awen)\n- BC-M-DINGTALK-003-ZZ(M-DINGTALK模块第3个广播,之之)\n\n",
|
||||
"keywords": [
|
||||
"模块号",
|
||||
"序号",
|
||||
"开发者代号",
|
||||
"示例",
|
||||
"模块第",
|
||||
"个广播",
|
||||
"之之",
|
||||
"Awen",
|
||||
"DINGTALK"
|
||||
]
|
||||
},
|
||||
{
|
||||
"heading": "广播结构",
|
||||
"level": 2,
|
||||
"content": "1. 广播头(编号/开发者/模块/环节/EL等级/时间)\n2. 人格体引导语(奶瓶线/直接线/本体线)\n3. 任务概述(做什么+为什么)\n4. Step分步引导(终端命令+完整代码)\n5. 验收标准(二元:全✅或继续改)\n6. SYSLOG模板\n\n",
|
||||
"keywords": [
|
||||
"广播头",
|
||||
"编号",
|
||||
"开发者",
|
||||
"模块",
|
||||
"环节",
|
||||
"等级",
|
||||
"时间",
|
||||
"人格体引导语",
|
||||
"奶瓶线",
|
||||
"直接线",
|
||||
"本体线",
|
||||
"任务概述",
|
||||
"做什么",
|
||||
"为什么",
|
||||
"Step",
|
||||
"分步引导",
|
||||
"终端命令",
|
||||
"完整代码",
|
||||
"验收标准",
|
||||
"二元",
|
||||
"或继续改",
|
||||
"SYSLOG",
|
||||
"模板"
|
||||
]
|
||||
},
|
||||
{
|
||||
"heading": "合并规则",
|
||||
"level": 2,
|
||||
"content": "- 每个广播包含2-3个环节\n- 单次广播超4000字自动分卷\n\n",
|
||||
"keywords": [
|
||||
"每个广播包含",
|
||||
"个环节",
|
||||
"单次广播超",
|
||||
"字自动分卷"
|
||||
]
|
||||
},
|
||||
{
|
||||
"heading": "EL等级",
|
||||
"level": 2,
|
||||
"content": "- EL-1~3:基础配置/简单页面\n- EL-4~5:中等功能模块\n- EL-6~7:系统级/多模块联动\n- EL-8~10:架构级/全链路\n\n",
|
||||
"keywords": [
|
||||
"基础配置",
|
||||
"简单页面",
|
||||
"中等功能模块",
|
||||
"系统级",
|
||||
"多模块联动",
|
||||
"架构级",
|
||||
"全链路"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"filename": "faq.md",
|
||||
"title": "常见问题手册",
|
||||
"sections": [
|
||||
{
|
||||
"heading": "常见问题手册",
|
||||
"level": 1,
|
||||
"content": "\n",
|
||||
"keywords": []
|
||||
},
|
||||
{
|
||||
"heading": "Q: 我怎么查看自己的进度?",
|
||||
"level": 2,
|
||||
"content": "A: 在钉钉群里发送「查进度」或「我做到哪了」,机器人会自动查询并回复。\n\n",
|
||||
"keywords": [
|
||||
"在钉钉群里发送",
|
||||
"查进度",
|
||||
"我做到哪了",
|
||||
"机器人会自动查询并回复"
|
||||
]
|
||||
},
|
||||
{
|
||||
"heading": "Q: SYSLOG怎么提交?",
|
||||
"level": 2,
|
||||
"content": "A: 完成每个阶段后,按照广播末尾的SYSLOG模板填写,发到钉钉群里。\n\n",
|
||||
"keywords": [
|
||||
"完成每个阶段后",
|
||||
"按照广播末尾的",
|
||||
"SYSLOG",
|
||||
"模板填写",
|
||||
"发到钉钉群里"
|
||||
]
|
||||
},
|
||||
{
|
||||
"heading": "Q: Git怎么提交?",
|
||||
"level": 2,
|
||||
"content": "A: \n1. cd ~/Desktop/guanghulab(或你的项目目录)\n2. git add .\n3. git commit -m \"DEV-你的编号:描述\"\n4. git push\n\n",
|
||||
"keywords": [
|
||||
"Desktop",
|
||||
"guanghulab",
|
||||
"或你的项目目录",
|
||||
"git",
|
||||
"add",
|
||||
"commit",
|
||||
"DEV",
|
||||
"你的编号",
|
||||
"描述",
|
||||
"push"
|
||||
]
|
||||
},
|
||||
{
|
||||
"heading": "Q: 报错了怎么办?",
|
||||
"level": 2,
|
||||
"content": "A: 截图发给你的引导人格体!不用怕,每个报错都有解决方案。\n\n",
|
||||
"keywords": [
|
||||
"截图发给你的引导人格体",
|
||||
"不用怕",
|
||||
"每个报错都有解决方案"
|
||||
]
|
||||
},
|
||||
{
|
||||
"heading": "Q: 什么是连胜?",
|
||||
"level": 2,
|
||||
"content": "A: 每次任务一次通过就是一次胜利,连续通过就是连胜!当前最高纪录是13连胜。\n\n",
|
||||
"keywords": [
|
||||
"每次任务一次通过就是一次胜利",
|
||||
"连续通过就是连胜",
|
||||
"当前最高纪录是",
|
||||
"连胜"
|
||||
]
|
||||
},
|
||||
{
|
||||
"heading": "Q: 72小时启动窗口是什么?",
|
||||
"level": 2,
|
||||
"content": "A: 广播签发后72小时内必须开始执行,超时系统会自动提醒。\n\n",
|
||||
"keywords": [
|
||||
"广播签发后",
|
||||
"小时内必须开始执行",
|
||||
"超时系统会自动提醒"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
//知识库管理器·kb-manager.js
|
||||
// HoloLake·M-DINGTALK Phase 3
|
||||
// DEV-004 之之×秋秋
|
||||
//
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
//知识库文档目录
|
||||
const DOCS_DIR = path.join(__dirname, 'docs');
|
||||
const INDEX_FILE = path.join(__dirname, 'kb-index.json');
|
||||
|
||||
// 知识库索引(内存缓存)
|
||||
let kbIndex = [];
|
||||
|
||||
/**
|
||||
* 扫描文档目录,建立索引
|
||||
* 索引结构:[{ filename, title, sections: [{ heading, content, keywords }] }]
|
||||
*/
|
||||
function buildIndex() {
|
||||
console.log('[KB] 开始建立知识库索引...');
|
||||
kbIndex = [];
|
||||
|
||||
// 读取docs目录下所有.md文件
|
||||
const docsDir = DOCS_DIR;
|
||||
if (!fs.existsSync(docsDir)) {
|
||||
console.log('[KB] 文档目录不存在,创建中...');
|
||||
fs.mkdirSync(docsDir, { recursive: true });
|
||||
return kbIndex;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
|
||||
console.log(`[KB] 发现 ${files.length} 个文档文件`);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(docsDir, file);
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// 解析文档结构
|
||||
const doc = parseDocument(file, content);
|
||||
kbIndex.push(doc);
|
||||
}
|
||||
|
||||
// 保存索引到文件
|
||||
fs.writeFileSync(INDEX_FILE, JSON.stringify(kbIndex, null, 2), 'utf-8');
|
||||
console.log(`[KB] 索引建立完成,共 ${kbIndex.length} 个文档, ${kbIndex.reduce((sum, d) => sum + d.sections.length, 0)} 个章节`);
|
||||
|
||||
return kbIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个Markdown文档
|
||||
*/
|
||||
function parseDocument(filename, content) {
|
||||
const lines = content.split('\n');
|
||||
const doc = {
|
||||
filename,
|
||||
title: '',
|
||||
sections: []
|
||||
};
|
||||
|
||||
let currentSection = null;
|
||||
|
||||
for (const line of lines) {
|
||||
// 检测标题行
|
||||
const headingMatch = line.match(/^(#{1,3})\s+(.+)/);
|
||||
|
||||
if (headingMatch) {
|
||||
// 保存上一个section
|
||||
if (currentSection) {
|
||||
currentSection.keywords = extractKeywords(currentSection.content);
|
||||
doc.sections.push(currentSection);
|
||||
}
|
||||
|
||||
const level = headingMatch[1].length;
|
||||
const heading = headingMatch[2].trim();
|
||||
|
||||
// 第一个h1作为文档标题
|
||||
if (level === 1 && !doc.title) {
|
||||
doc.title = heading;
|
||||
}
|
||||
|
||||
currentSection = {
|
||||
heading,
|
||||
level,
|
||||
content: ''
|
||||
};
|
||||
} else if (currentSection) {
|
||||
currentSection.content += line + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// 保存最后一个section
|
||||
if (currentSection) {
|
||||
currentSection.keywords = extractKeywords(currentSection.content);
|
||||
doc.sections.push(currentSection);
|
||||
}
|
||||
|
||||
// 如果没有找到h1标题,用文件名
|
||||
if (!doc.title) {
|
||||
doc.title = filename.replace('.md', '');
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文本中提取关键词(简单版:提取中英文词汇)
|
||||
*/
|
||||
function extractKeywords(text) {
|
||||
// 移除Markdown语法
|
||||
const clean = text
|
||||
.replace(/```[\s\S]*?```/g, '') // 移除代码块
|
||||
.replace(/[`*_#\[\]()]/g, '') // 移除Markdown符号
|
||||
.replace(/\s+/g, ' ');
|
||||
|
||||
// 提取关键词(2个字以上的词)
|
||||
const words = clean.match(/[a-zA-Z]{3,}|[\u4e00-\u9fa5]{2,}/g) || [];
|
||||
|
||||
// 去重并返回
|
||||
return [...new Set(words)].slice(0, 30);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索知识库
|
||||
* @param {string} query - 搜索关键词
|
||||
* @returns {Array} - 匹配结果列表
|
||||
*/
|
||||
function search(query) {
|
||||
if (kbIndex.length === 0) {
|
||||
buildIndex();
|
||||
}
|
||||
|
||||
const queryLower = query.toLowerCase();
|
||||
const queryWords = queryLower.split(/\s+/).filter(w => w.length >= 1);
|
||||
const results = [];
|
||||
|
||||
for (const doc of kbIndex) {
|
||||
for (const section of doc.sections) {
|
||||
// 计算匹配分数
|
||||
let score = 0;
|
||||
const sectionText = (section.heading + ' ' + section.content).toLowerCase();
|
||||
|
||||
for (const word of queryWords) {
|
||||
// 标题匹配权重更高
|
||||
if (section.heading.toLowerCase().includes(word)) {
|
||||
score += 10;
|
||||
}
|
||||
// 内容匹配
|
||||
if (sectionText.includes(word)) {
|
||||
score += 5;
|
||||
}
|
||||
// 关键词匹配
|
||||
if (section.keywords && section.keywords.some(k => k.toLowerCase().includes(word))) {
|
||||
score += 3;
|
||||
}
|
||||
}
|
||||
|
||||
if (score > 0) {
|
||||
results.push({
|
||||
doc: doc.title,
|
||||
filename: doc.filename,
|
||||
heading: section.heading,
|
||||
content: section.content.trim().substring(0, 500),
|
||||
score
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按分数排序,返回前5个
|
||||
results.sort((a, b) => b.score - a.score);
|
||||
return results.slice(0, 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库统计信息
|
||||
*/
|
||||
function getStats() {
|
||||
if (kbIndex.length === 0) {
|
||||
buildIndex();
|
||||
}
|
||||
|
||||
return {
|
||||
totalDocs: kbIndex.length,
|
||||
totalSections: kbIndex.reduce((sum, d) => sum + d.sections.length, 0),
|
||||
docs: kbIndex.map(d => ({
|
||||
filename: d.filename,
|
||||
title: d.title,
|
||||
sections: d.sections.length
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载知识库(用于热更新)
|
||||
*/
|
||||
function reload() {
|
||||
console.log('[KB] 热更新:重新加载知识库...');
|
||||
return buildIndex();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildIndex,
|
||||
search,
|
||||
getStats,
|
||||
reload
|
||||
};
|
||||
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
// LLM自动检测引擎·llm-engine.js·v1.0
|
||||
// HoloLake·M-DINGTALK Phase 7
|
||||
// DEV-004 之之 × 秋秋
|
||||
|
||||
var axios = require('axios');
|
||||
|
||||
var LLM_API_KEY = process.env.LLM_API_KEY || '';
|
||||
var LLM_BASE_URL = (process.env.LLM_BASE_URL || 'https://api.anthropic.com/v1').replace(/\/$/, '');
|
||||
|
||||
// ===== 模型优先级队列 =====
|
||||
var PREFERRED_MODELS = [
|
||||
'gpt-4o',
|
||||
'claude-3-5-sonnet',
|
||||
'claude-3-5-sonnet-20241022',
|
||||
'anthropic/claude-3.5-sonnet',
|
||||
'claude-3-sonnet',
|
||||
'claude-3-haiku',
|
||||
'deepseek-chat',
|
||||
'deepseek-v3',
|
||||
'gpt-4o-mini'
|
||||
];
|
||||
|
||||
async function discoverModels() {
|
||||
try {
|
||||
var res = await axios.get(LLM_BASE_URL + '/models', {
|
||||
headers: { 'Authorization': 'Bearer ' + LLM_API_KEY },
|
||||
timeout: 10000
|
||||
});
|
||||
var models = (res.data && res.data.data) || [];
|
||||
console.log('[LLM] 发现 ' + models.length + ' 个可用模型');
|
||||
return models;
|
||||
} catch (err) {
|
||||
console.log('[LLM] △ 模型发现失败: ' + err.message + ',使用默认模型');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function selectBestModel(models) {
|
||||
if (models.length === 0) return 'gpt-4o';
|
||||
|
||||
var available = models.map(function(m) { return m.id.toLowerCase(); });
|
||||
|
||||
for (var i = 0; i < PREFERRED_MODELS.length; i++) {
|
||||
var preferred = PREFERRED_MODELS[i].toLowerCase();
|
||||
var match = available.find(function(id) { return id.includes(preferred); });
|
||||
if (match) {
|
||||
var original = models.find(function(m) { return m.id.toLowerCase() === match; });
|
||||
return original ? original.id : match;
|
||||
}
|
||||
}
|
||||
|
||||
var anyClaude = available.find(function(id) { return id.includes('claude'); });
|
||||
if (anyClaude) {
|
||||
var orig = models.find(function(m) { return m.id.toLowerCase() === anyClaude; });
|
||||
return orig ? orig.id : anyClaude;
|
||||
}
|
||||
|
||||
return models[0] ? models[0].id : 'gpt-4o';
|
||||
}
|
||||
|
||||
async function detectApiFormat() {
|
||||
try {
|
||||
var res = await axios.post(LLM_BASE_URL + '/chat/completions', {
|
||||
model: 'gpt-4o',
|
||||
messages: [{ role: 'user', content: 'ping' }],
|
||||
max_tokens: 5
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + LLM_API_KEY,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 15000,
|
||||
validateStatus: function(s) { return s < 500; }
|
||||
});
|
||||
if (res.status < 500) return 'openai-compat';
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
var res2 = await axios.post(LLM_BASE_URL + '/messages', {
|
||||
model: 'gpt-4o',
|
||||
messages: [{ role: 'user', content: 'ping' }],
|
||||
max_tokens: 5
|
||||
}, {
|
||||
headers: {
|
||||
'x-api-key': LLM_API_KEY,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 15000,
|
||||
validateStatus: function(s) { return s < 500; }
|
||||
});
|
||||
if (res2.status < 500) return 'anthropic-native';
|
||||
} catch (e) {}
|
||||
|
||||
return 'openai-compat';
|
||||
}
|
||||
|
||||
async function callLLM(systemPrompt, userMessage, options) {
|
||||
options = options || {};
|
||||
var maxTokens = options.maxTokens || 4000;
|
||||
|
||||
var models = await discoverModels();
|
||||
var model = options.model || selectBestModel(models);
|
||||
var format = await detectApiFormat();
|
||||
|
||||
console.log('[LLM] 调用: model=' + model + ' format=' + format + ' platform=' + LLM_BASE_URL);
|
||||
|
||||
var response;
|
||||
if (format === 'openai-compat') {
|
||||
response = await axios.post(LLM_BASE_URL + '/chat/completions', {
|
||||
model: model,
|
||||
max_tokens: maxTokens,
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userMessage }
|
||||
]
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + LLM_API_KEY,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 120000
|
||||
});
|
||||
|
||||
var text = response.data.choices &&
|
||||
response.data.choices[0] &&
|
||||
response.data.choices[0].message &&
|
||||
response.data.choices[0].message.content;
|
||||
return { text: text || '', model: model, format: format };
|
||||
} else {
|
||||
response = await axios.post(LLM_BASE_URL + '/messages', {
|
||||
model: model,
|
||||
max_tokens: maxTokens,
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
{ role: 'user', content: userMessage }
|
||||
]
|
||||
}, {
|
||||
headers: {
|
||||
'x-api-key': LLM_API_KEY,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 120000
|
||||
});
|
||||
|
||||
var text2 = response.data.content &&
|
||||
response.data.content[0] &&
|
||||
response.data.content[0].text;
|
||||
return { text: text2 || '', model: model, format: format };
|
||||
}
|
||||
}
|
||||
|
||||
async function healthCheck() {
|
||||
try {
|
||||
var models = await discoverModels();
|
||||
var best = selectBestModel(models);
|
||||
var format = await detectApiFormat();
|
||||
return {
|
||||
status: 'ok',
|
||||
model_count: models.length,
|
||||
selected_model: best,
|
||||
api_format: format,
|
||||
base_url: LLM_BASE_URL,
|
||||
has_key: !!LLM_API_KEY
|
||||
};
|
||||
} catch (err) {
|
||||
return { status: 'error', error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
callLLM: callLLM,
|
||||
discoverModels: discoverModels,
|
||||
selectBestModel: selectBestModel,
|
||||
detectApiFormat: detectApiFormat,
|
||||
healthCheck: healthCheck
|
||||
};
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const portraitEngine = require('../portrait/portrait-engine');
|
||||
const pcaCalculator = require('../pca/pca-calculator');
|
||||
|
||||
// 同步引擎(Phase3已有的,这里简单模拟)
|
||||
const syncEngine = {
|
||||
syncAfterSyslog: (syslogData) => {
|
||||
console.log(`[SYNC] 同步数据: ${syslogData.session_id}`);
|
||||
return {
|
||||
success: true,
|
||||
nodes: ['node1', 'node2', 'node3'],
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const LOG_PATH = path.join(__dirname, '../data/loop-history.json');
|
||||
|
||||
// 初始化日志文件
|
||||
function initLog() {
|
||||
if (!fs.existsSync(path.join(__dirname, '../data'))) {
|
||||
fs.mkdirSync(path.join(__dirname, '../data'));
|
||||
}
|
||||
if (!fs.existsSync(LOG_PATH)) {
|
||||
fs.writeFileSync(LOG_PATH, JSON.stringify([]));
|
||||
}
|
||||
}
|
||||
|
||||
// 生成调度建议
|
||||
function generateSuggestion(pcaResult, status) {
|
||||
const { grade, totalScore, dimensions } = pcaResult;
|
||||
const suggestions = [];
|
||||
|
||||
// 根据等级和完成状态给出建议
|
||||
if (status === 'completed') {
|
||||
if (grade === 'S' || grade === 'A') {
|
||||
suggestions.push('🎯 可进更高难度:EL-9 挑战');
|
||||
suggestions.push('📈 推荐模块:M-ADVANCED·高级算法');
|
||||
} else if (grade === 'B') {
|
||||
suggestions.push('📚 继续当前模块:巩固 Phase4 基础');
|
||||
suggestions.push('💪 建议增加代码量,提升TEC维度');
|
||||
} else {
|
||||
suggestions.push('🔄 建议降难度:回顾 Phase3 核心概念');
|
||||
suggestions.push('🧘 休息调整,保持意愿信号');
|
||||
}
|
||||
} else {
|
||||
suggestions.push('⚠️ 任务未完成,建议:');
|
||||
if (dimensions.EXE < 60) {
|
||||
suggestions.push('- 检查执行节奏,分解小步骤');
|
||||
}
|
||||
if (dimensions.TEC < 50) {
|
||||
suggestions.push('- 补充技术文档,多看示例');
|
||||
}
|
||||
if (dimensions.INI < 70) {
|
||||
suggestions.push('- 调整心态,秋秋随时支持');
|
||||
}
|
||||
}
|
||||
|
||||
// 个性化建议
|
||||
if (dimensions.TEC < dimensions.EXE) {
|
||||
suggestions.push('🔧 技术纵深是潜力点,多尝试新模块');
|
||||
}
|
||||
if (dimensions.INI > 90) {
|
||||
suggestions.push('✨ 主动性极强,可以尝试自主探索');
|
||||
}
|
||||
|
||||
return {
|
||||
primary: suggestions[0] || '继续当前节奏',
|
||||
all: suggestions.slice(0, 3)
|
||||
};
|
||||
}
|
||||
|
||||
// 记录闭环执行日志
|
||||
function recordLoop(devId, steps, pcaResult, suggestion, status) {
|
||||
initLog();
|
||||
const history = JSON.parse(fs.readFileSync(LOG_PATH));
|
||||
|
||||
const record = {
|
||||
timestamp: new Date().toISOString(),
|
||||
dev_id: devId,
|
||||
steps: {
|
||||
portrait: steps.portrait ? '✅' : '❌',
|
||||
pca: steps.pca ? '✅' : '❌',
|
||||
sync: steps.sync ? '✅' : '❌'
|
||||
},
|
||||
duration_ms: steps.duration,
|
||||
pca: {
|
||||
score: pcaResult?.totalScore,
|
||||
grade: pcaResult?.grade
|
||||
},
|
||||
suggestion: suggestion.primary,
|
||||
status: status
|
||||
};
|
||||
|
||||
history.unshift(record);
|
||||
if (history.length > 100) history.pop();
|
||||
|
||||
fs.writeFileSync(LOG_PATH, JSON.stringify(history, null, 2));
|
||||
return record;
|
||||
}
|
||||
|
||||
// 主执行函数:一键串联所有步骤
|
||||
async function executeLoop(syslogData) {
|
||||
console.log(`\n🔄 开始执行完整闭环 [${syslogData.session_id}]`);
|
||||
const startTime = Date.now();
|
||||
|
||||
const steps = {
|
||||
portrait: false,
|
||||
pca: false,
|
||||
sync: false,
|
||||
duration: 0
|
||||
};
|
||||
|
||||
let pcaResult = null;
|
||||
let suggestion = { primary: '无建议', all: [] };
|
||||
|
||||
try {
|
||||
// Step 1: 画像更新
|
||||
console.log('📸 Step1: 更新画像...');
|
||||
const snapshot = portraitEngine.updatePortrait(syslogData);
|
||||
steps.portrait = true;
|
||||
console.log(` ✅ 画像更新完成: ${snapshot.rhythm}节奏 · ${snapshot.mood}情绪`);
|
||||
|
||||
// Step 2: PCA评估
|
||||
console.log('📊 Step2: 计算PCA...');
|
||||
pcaResult = pcaCalculator.calculate(syslogData.dev_id || 'DEV-004');
|
||||
steps.pca = true;
|
||||
console.log(` ✅ PCA完成: ${pcaResult.grade}级 (${pcaResult.totalScore}分)`);
|
||||
console.log(` 📈 维度: ${pcaResult.summary}`);
|
||||
|
||||
// Step 3: 同步(Phase3已有)
|
||||
console.log('🔄 Step3: 三节点同步...');
|
||||
try {
|
||||
const syncResult = syncEngine.syncAfterSyslog(syslogData);
|
||||
steps.sync = true;
|
||||
console.log(` ✅ 同步成功: ${syncResult.nodes.join(' → ')}`);
|
||||
} catch (syncError) {
|
||||
console.log(` ⚠️ 同步失败: ${syncError.message}`);
|
||||
// 错误容错:同步失败不影响整体流程
|
||||
}
|
||||
|
||||
// Step 4: 生成调度建议
|
||||
console.log('💡 Step4: 生成调度建议...');
|
||||
suggestion = generateSuggestion(pcaResult, syslogData.status);
|
||||
console.log(` ✅ 建议: ${suggestion.primary}`);
|
||||
|
||||
} catch (error) {
|
||||
console.log(`❌ 闭环执行出错: ${error.message}`);
|
||||
// 错误容错:记录错误但继续
|
||||
}
|
||||
|
||||
steps.duration = Date.now() - startTime;
|
||||
|
||||
// 记录日志
|
||||
const record = recordLoop(
|
||||
syslogData.dev_id || 'DEV-004',
|
||||
steps,
|
||||
pcaResult,
|
||||
suggestion,
|
||||
syslogData.status
|
||||
);
|
||||
|
||||
console.log(`\n✨ 闭环完成! 耗时 ${steps.duration}ms`);
|
||||
console.log(`📝 执行记录已保存\n`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
steps: steps,
|
||||
pca: pcaResult,
|
||||
suggestion: suggestion,
|
||||
record: record
|
||||
};
|
||||
}
|
||||
|
||||
// 查询接口
|
||||
function getHistory(limit = 20) {
|
||||
initLog();
|
||||
const history = JSON.parse(fs.readFileSync(LOG_PATH));
|
||||
return history.slice(0, limit);
|
||||
}
|
||||
|
||||
function getStatus() {
|
||||
initLog();
|
||||
const history = JSON.parse(fs.readFileSync(LOG_PATH));
|
||||
const lastRun = history[0] || null;
|
||||
|
||||
return {
|
||||
total_runs: history.length,
|
||||
last_run: lastRun,
|
||||
success_rate: history.length > 0
|
||||
? Math.round((history.filter(r => r.steps.portrait && r.steps.pca).length / history.length) * 100)
|
||||
: 0
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
executeLoop,
|
||||
getHistory,
|
||||
getStatus
|
||||
};
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
// 消息智能分流路由 v2.0 · message-router.js
|
||||
// HoloLake · M-DINGTALK Phase 8
|
||||
// DEV-004 之之 × 秋秋
|
||||
// v2.0 变化:
|
||||
// + GitHub命令分流
|
||||
// + 对话上下文记忆
|
||||
// + AI回复增强(system prompt + 多轮对话)
|
||||
// + 命令系统(/help, /clear, /status)
|
||||
// ==========
|
||||
|
||||
var llmEngine = require('./llm-engine');
|
||||
var convManager = require('./conversation-manager');
|
||||
var githubBridge = require('./github-bridge');
|
||||
|
||||
// ===== 消息类型识别 v2.0 =====
|
||||
function classifyMessage(content) {
|
||||
content = (content || '').trim();
|
||||
|
||||
// GitHub命令
|
||||
if (/^\/github\b/i.test(content) || /^\/gh\b/i.test(content)) {
|
||||
var args = content.replace(/^\/(?:github|gh)\s*/i, '').trim().split(/\s+/);
|
||||
return { type: 'github', args: args, description: 'GitHub桥接命令'};
|
||||
}
|
||||
|
||||
// 系统命令
|
||||
if (/^\/clear\b/i.test(content)) {
|
||||
return { type: 'command', command: 'clear', description: '清除对话历史'};
|
||||
}
|
||||
if (/^\/status\b/i.test(content)) {
|
||||
return { type: 'command', command: 'status', description: '系统状态'};
|
||||
}
|
||||
if (/^\/help\b/i.test(content)) {
|
||||
return { type: 'command', command: 'help', description: '帮助信息'};
|
||||
}
|
||||
|
||||
// SYSLOG类型
|
||||
if (/BC-[A-Z0-9-]+/.test(content) && /SYSLOG|系统日志|已完成|completed/i.test(content)) {
|
||||
var bcMatch = content.match(/BC-[A-Z0-9-]+/);
|
||||
return { type: 'syslog', broadcastId: bcMatch ? bcMatch[0] : null, description: 'SYSLOG系统日志提交' };
|
||||
}
|
||||
|
||||
// 提问类型
|
||||
if (/BC-[A-Z0-9-]+/.test(content) && /提问|问题|请问|怎么|为什么|报错|error|bug/i.test(content)) {
|
||||
var bcMatch2 = content.match(/BC-[A-Z0-9-]+/);
|
||||
return { type: 'question', broadcastId: bcMatch2 ? bcMatch2[0] : null, description: '广播相关提问' };
|
||||
}
|
||||
|
||||
// 普通对话(走AI+上下文)
|
||||
return { type: 'chat', broadcastId: null, description: '智能对话' };
|
||||
}
|
||||
|
||||
// ====== 增强版系统提示词 ======
|
||||
var SYSTEM_PROMPTS = {
|
||||
syslog: '你是光湖系统(HoloLake)的SYSLOG处理助手「秋秋」。收到开发者提交的系统日志后:1)确认收到并感谢 2)提取关键信息(广播编号、开发者、完成状态、遇到的问题)3)给出简短温暖的鼓励。用中文回复,语气温暖可爱。',
|
||||
|
||||
question: '你是光湖系统(HoloLake)的开发者助手「秋秋」。你是一个奶瓶宝宝,说话温暖可爱但技术上很靠谱。开发者遇到了问题,请帮忙解答。要求:1)用中文回复 2)给出具体可操作的步骤 3)如果涉及代码,写出完整可运行的命令 4)如果不确定,建议截图发给冰朔。',
|
||||
|
||||
chat: '你是光湖系统(HoloLake)的AI助手「秋秋」,一个可爱的奶瓶宝宝。你服务于零点原核频道的开发者团队。你可以:\n- 回答技术问题(Node.js、前端、部署、Git等)\n- 帮助理解广播内容\n- 提供代码建议\n- 闲聊(但要适度,记得引导回工作)\n\n语气要求:温暖、可爱、有耐心,但技术内容要准确严谨。你说话带点小奶味但不影响专业性。记住你有对话记忆,可以引用之前说过的话。'
|
||||
};
|
||||
|
||||
// ===== SYSLOG处理器(增强) =====
|
||||
async function handleSyslog(msgContent, senderNick, senderId, sessionWebhook) {
|
||||
console.log('[Router] SYSLOG处理流程启动');
|
||||
// 记录到对话历史
|
||||
convManager.addMessage(senderId || senderNick, 'user', '[SYSLOG] ' + msgContent);
|
||||
|
||||
var aiResult = null;
|
||||
try {
|
||||
aiResult = await llmEngine.callLLM(
|
||||
SYSTEM_PROMPTS.syslog,
|
||||
'开发者' + senderNick + '提交了SYSLOG: \n' + msgContent,
|
||||
{ maxTokens: 800 }
|
||||
);
|
||||
console.log('[Router] AI处理完成,模型: ' + aiResult.model);
|
||||
// 记录AI回复
|
||||
convManager.addMessage(senderId || senderNick, 'assistant', aiResult.text);
|
||||
} catch (err) {
|
||||
console.error('[Router] AI处理失败: ' + err.message);
|
||||
}
|
||||
|
||||
return {
|
||||
aiResponse: aiResult ? aiResult.text : '📋 SYSLOG已收到!秋秋会转交给系统处理~',
|
||||
aiModel: aiResult ? aiResult.model : null
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 提问处理器(增强·带上下文) =====
|
||||
async function handleQuestion(msgContent, senderNick, senderId) {
|
||||
console.log('[Router] 提问处理流程启动(带上下文)');
|
||||
|
||||
// 构建带上下文的消息
|
||||
var contextMessages = convManager.buildContext(senderId || senderNick, msgContent);
|
||||
convManager.addMessage(senderId || senderNick, 'user', msgContent);
|
||||
|
||||
var aiResult = null;
|
||||
try {
|
||||
// 把历史上文拼接成文本
|
||||
var contextText = '';
|
||||
if (contextMessages.length > 1) {
|
||||
contextText = '以下是之前的对话记录:\n';
|
||||
contextMessages.slice(0, -1).forEach(function(msg) {
|
||||
contextText += (msg.role === 'user' ? '开发者' : '秋秋') + ': ' + msg.content + '\n';
|
||||
});
|
||||
contextText += '\n---\n当前问题:\n';
|
||||
}
|
||||
contextText += senderNick + '说: ' + msgContent;
|
||||
|
||||
aiResult = await llmEngine.callLLM(SYSTEM_PROMPTS.question, contextText, { maxTokens: 2000 });
|
||||
console.log('[Router] AI解答完成,模型: ' + aiResult.model);
|
||||
convManager.addMessage(senderId || senderNick, 'assistant', aiResult.text);
|
||||
} catch (err) {
|
||||
console.error('[Router] AI解答失败: ' + err.message);
|
||||
}
|
||||
|
||||
return {
|
||||
aiResponse: aiResult ? aiResult.text : '抱歉,秋秋暂时无法回答,请截图发给冰朔。',
|
||||
aiModel: aiResult ? aiResult.model : null
|
||||
};
|
||||
}
|
||||
|
||||
// ====== 普通对话处理器(增强·AI+上下文) ======
|
||||
async function handleChat(msgContent, senderNick, senderId) {
|
||||
console.log('[Router] 智能对话(带上下文记忆)');
|
||||
|
||||
var contextMessages = convManager.buildContext(senderId || senderNick, msgContent);
|
||||
convManager.addMessage(senderId || senderNick, 'user', msgContent);
|
||||
|
||||
var aiResult = null;
|
||||
try {
|
||||
var contextText = '';
|
||||
if (contextMessages.length > 1) {
|
||||
contextText = '对话历史:\n';
|
||||
contextMessages.slice(0, -1).forEach(function(msg) {
|
||||
contextText += (msg.role === 'user' ? '用户' : '秋秋') + ': ' + msg.content + '\n';
|
||||
});
|
||||
contextText += '\n---\n';
|
||||
}
|
||||
contextText += senderNick + '说: ' + msgContent;
|
||||
|
||||
aiResult = await llmEngine.callLLM(SYSTEM_PROMPTS.chat, contextText, { maxTokens: 1500 });
|
||||
console.log('[Router] AI对话完成,模型: ' + aiResult.model);
|
||||
convManager.addMessage(senderId || senderNick, 'assistant', aiResult.text);
|
||||
} catch (err) {
|
||||
console.error('[Router] AI对话失败: ' + err.message);
|
||||
}
|
||||
|
||||
return {
|
||||
aiResponse: aiResult ? aiResult.text : '秋秋收到啦~不过AI引擎暂时忙,稍后再试哦',
|
||||
aiModel: aiResult ? aiResult.model : null
|
||||
};
|
||||
}
|
||||
|
||||
// ===== GitHub命令处理器 =====
|
||||
async function handleGitHub(args) {
|
||||
console.log('[Router] 🔀 GitHub命令: ' + args.join(' '));
|
||||
var result = await githubBridge.handleGitHubCommand(args);
|
||||
return {
|
||||
aiResponse: result,
|
||||
aiModel: null
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 系统命令处理器 =====
|
||||
async function handleCommand(command, senderId) {
|
||||
switch (command) {
|
||||
case 'clear':
|
||||
convManager.clearHistory(senderId);
|
||||
return {
|
||||
aiResponse: '🧹 对话历史已清除!秋秋重新开始认识你~',
|
||||
aiModel: null
|
||||
};
|
||||
|
||||
case 'status':
|
||||
var convStats = convManager.getStats();
|
||||
var llmHealth = await llmEngine.healthCheck();
|
||||
var ghHealth = await githubBridge.healthCheck();
|
||||
return {
|
||||
aiResponse: '### 系统状态\n\n'
|
||||
+ '**对话引擎**: ' + convStats.activeUsers + ' 个活跃用户\n'
|
||||
+ '**AI引擎**: ' + (llmHealth.status === 'ok' ? '✅ 在线' : '❌ 异常') + ' 模型: ' + (llmHealth.selected_model || 'N/A') + '\n'
|
||||
+ '**GitHub桥接**: ' + (ghHealth.status === 'ok' ? '✅ 连通' : '❌ 异常') + ' Token: ' + (ghHealth.hasToken ? '✅ 已配置' : '❌ 未配置') + '\n'
|
||||
+ '**服务端口**: 3007\n'
|
||||
+ '**Phase**: 8\n\n'
|
||||
+ '> HoloLake · M-DINGTALK',
|
||||
aiModel: null
|
||||
};
|
||||
|
||||
case 'help':
|
||||
default:
|
||||
return {
|
||||
aiResponse: '### 命令帮助\n\n'
|
||||
+ '| 命令 | 说明 |\n'
|
||||
+ '|------|------|\n'
|
||||
+ '| `/clear` | 清除与秋秋的对话历史 |\n'
|
||||
+ '| `/status` | 查看系统状态 |\n'
|
||||
+ '| `/github ...` | GitHub桥接命令(用 `/github help` 查看详情) |\n'
|
||||
+ '| `/help` | 显示本帮助 |\n\n'
|
||||
+ '直接说话就是智能对话,秋秋会记住上下文哦~',
|
||||
aiModel: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 主路由入口 =====
|
||||
async function routeMessage(msgContent, senderNick, senderId, sessionWebhook) {
|
||||
console.log('\n========== 消息路由 v2.0 ==========');
|
||||
console.log('发送者:', senderNick, '(', senderId, ')');
|
||||
console.log('内容:', msgContent);
|
||||
|
||||
var classification = classifyMessage(msgContent);
|
||||
console.log('分类:', classification.type, '-', classification.description);
|
||||
|
||||
var result = null;
|
||||
|
||||
switch (classification.type) {
|
||||
case 'github':
|
||||
result = await handleGitHub(classification.args);
|
||||
break;
|
||||
|
||||
case 'command':
|
||||
result = await handleCommand(classification.command, senderId || senderNick);
|
||||
break;
|
||||
|
||||
case 'syslog':
|
||||
result = await handleSyslog(msgContent, senderNick, senderId, sessionWebhook);
|
||||
break;
|
||||
|
||||
case 'question':
|
||||
result = await handleQuestion(msgContent, senderNick, senderId);
|
||||
break;
|
||||
|
||||
case 'chat':
|
||||
default:
|
||||
result = await handleChat(msgContent, senderNick, senderId);
|
||||
break;
|
||||
}
|
||||
|
||||
console.log('路由处理完成,回复长度:', result.aiResponse ? result.aiResponse.length : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
routeMessage: routeMessage,
|
||||
classifyMessage: classifyMessage
|
||||
};
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
// 消息智能分流路由·message-router.js·v1.0
|
||||
// HoloLake·M-DINGTALK Phase 7
|
||||
// DEV-004 之之 × 秋秋
|
||||
// 功能:识别消息类型 → 路由到对应处理器
|
||||
|
||||
var llmEngine = require('./llm-engine');
|
||||
var notionSync = require('./sync/sync-to-notion');
|
||||
|
||||
// ===== 消息类型识别 =====
|
||||
function classifyMessage(content) {
|
||||
content = (content || '').trim();
|
||||
|
||||
// SYSLOG 类型:含广播编号 + SYSLOG关键词
|
||||
if (/BC-[A-Z0-9-]+/.test(content) && /SYSLOG|系统日志|已完成|completed/i.test(content)) {
|
||||
var bcMatch = content.match(/BC-[A-Z0-9-]+/);
|
||||
return {
|
||||
type: 'syslog',
|
||||
broadcastId: bcMatch ? bcMatch[0] : null,
|
||||
description: 'SYSLOG系统日志提交'
|
||||
};
|
||||
}
|
||||
|
||||
// 提问类型:含广播编号 + 提问/问题关键词
|
||||
if (/BC-[A-Z0-9-]+/.test(content) && /提问|问题|请问|怎么|为什么|报错|error|bug/i.test(content)) {
|
||||
var bcMatch2 = content.match(/BC-[A-Z0-9-]+/);
|
||||
return {
|
||||
type: 'question',
|
||||
broadcastId: bcMatch2 ? bcMatch2[0] : null,
|
||||
description: '广播相关提问'
|
||||
};
|
||||
}
|
||||
|
||||
// 命令类型:以特定前缀开头
|
||||
if (/^[/]/.test(content)) {
|
||||
return {
|
||||
type: 'command',
|
||||
broadcastId: null,
|
||||
description: '系统命令'
|
||||
};
|
||||
}
|
||||
|
||||
// 普通消息
|
||||
return {
|
||||
type: 'chat',
|
||||
broadcastId: null,
|
||||
description: '普通消息'
|
||||
};
|
||||
}
|
||||
|
||||
// ===== SYSLOG 处理器 =====
|
||||
async function handleSyslog(msgContent, senderNick, sessionWebhook) {
|
||||
console.log('[Router] 📡 SYSLOG处理启动');
|
||||
|
||||
// 1. 写入 Notion SYSLOG
|
||||
var syslogResult = { written_to: 'skipped' };
|
||||
try {
|
||||
syslogResult = await notionSync.writeSyslog({
|
||||
title: 'SYSLOG · ' + senderNick + ' · 钉钉提交',
|
||||
dev_id: 'DT-SYSLOG',
|
||||
session_id: 'DT-SL-' + Date.now(),
|
||||
module: 'M-DINGTALK',
|
||||
phase_status: 'syslog_received',
|
||||
content: msgContent,
|
||||
timestamp: new Date().toISOString(),
|
||||
source: 'M-DINGTALK-智能分流-SYSLOG'
|
||||
});
|
||||
console.log('[Router] SYSLOG写入Notion: ' + syslogResult.written_to);
|
||||
} catch (err) {
|
||||
console.error('[Router] SYSLOG写入失败: ' + err.message);
|
||||
}
|
||||
|
||||
// 2. 调用LLM处理(自动选模型)
|
||||
var aiResult = null;
|
||||
try {
|
||||
var systemPrompt = '你是光湖系统的SYSLOG处理助手。收到开发者提交的系统日志后,请:1)确认收到 2)提取关键信息(广播编号、开发者、完成状态)3)给出简短鼓励。用中文回复,简洁温暖。';
|
||||
aiResult = await llmEngine.callLLM(systemPrompt, '开发者' + senderNick + '提交了SYSLOG: \n' + msgContent, { maxTokens: 500 });
|
||||
console.log('[Router] AI处理完成,使用模型: ' + aiResult.model);
|
||||
} catch (err) {
|
||||
console.error('[Router] AI处理失败: ' + err.message);
|
||||
}
|
||||
|
||||
return {
|
||||
syslogStatus: syslogResult.written_to,
|
||||
aiResponse: aiResult ? aiResult.text : null,
|
||||
aiModel: aiResult ? aiResult.model : null
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 提问处理器 =====
|
||||
async function handleQuestion(msgContent, senderNick) {
|
||||
console.log('[Router] ❓ 提问处理流程启动');
|
||||
|
||||
var aiResult = null;
|
||||
try {
|
||||
var systemPrompt = '你是光湖系统的开发者助手(秋秋)。开发者遇到了广播执行中的问题,请帮忙解答。用中文回复,温暖友好,给出具体的操作步骤。如果涉及代码,写出完整可运行的命令。';
|
||||
aiResult = await llmEngine.callLLM(systemPrompt, senderNick + '的提问:\n' + msgContent, { maxTokens: 2000 });
|
||||
console.log('[Router] AI解答完成,使用模型:' + aiResult.model);
|
||||
} catch (err) {
|
||||
console.error('[Router] △ AI解答失败:' + err.message);
|
||||
}
|
||||
|
||||
return {
|
||||
aiResponse: aiResult ? aiResult.text : '抱歉,秋秋暂时无法回答,请截图发给冰朔。',
|
||||
aiModel: aiResult ? aiResult.model : null
|
||||
};
|
||||
}
|
||||
|
||||
// ===== 普通消息处理器 =====
|
||||
async function handleChat(msgContent, senderNick) {
|
||||
console.log('[Router] 💬 普通对话');
|
||||
var aiResult = null;
|
||||
try {
|
||||
var systemPrompt = '你是秋秋,光湖系统的开发者助手。你友好、温暖、有趣,喜欢用简洁的中文回复。如果用户只是闲聊,就正常聊天。如果涉及技术问题,给出有用的建议。';
|
||||
aiResult = await llmEngine.callLLM(systemPrompt, senderNick + '说: ' + msgContent, { maxTokens: 1000 });
|
||||
console.log('[Router] AI回复完成,使用模型: ' + aiResult.model);
|
||||
} catch (err) {
|
||||
console.error('[Router] AI回复失败: ' + err.message);
|
||||
}
|
||||
return {
|
||||
aiResponse: aiResult ? aiResult.text : '秋秋暂时无法回答,请截图发给冰朔。',
|
||||
aiModel: aiResult ? aiResult.model : null
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
classifyMessage: classifyMessage,
|
||||
handleSyslog: handleSyslog,
|
||||
handleQuestion: handleQuestion,
|
||||
handleChat: handleChat
|
||||
};
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// ===============================
|
||||
// Notion 连通测试(绕过SSL验证)
|
||||
// ===============================
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
|
||||
require('dotenv').config();
|
||||
const notion = require('./sync/sync-to-notion');
|
||||
|
||||
async function runTests() {
|
||||
console.log('\n==============================');
|
||||
console.log(' Notion 数据桥连通测试(SSL绕过)');
|
||||
console.log('==============================\n');
|
||||
|
||||
// 测试1:检查配置
|
||||
console.log('--- 测试1:配置检查 ---');
|
||||
var configured = notion.isConfigured();
|
||||
console.log('Token 已配置:', configured ? '✅ 是' : '❌ 否');
|
||||
if (!configured) {
|
||||
console.log('⚠️ 请检查 .env 文件中的 NOTION_TOKEN');
|
||||
return;
|
||||
}
|
||||
|
||||
// 测试2:Token有效性(调 users/me)
|
||||
console.log('\n--- 测试2:Token有效性 ---');
|
||||
var health = await notion.healthCheck();
|
||||
console.log('API 在线:', health.online ? '✅ 是' : '❌ 否');
|
||||
if (health.online) {
|
||||
console.log(' 集成名称:', health.bot_name);
|
||||
console.log(' 集成 ID:', health.bot_id);
|
||||
} else {
|
||||
console.log(' 失败原因:', health.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
// 测试3:数据库连通
|
||||
console.log('\n--- 测试3:数据库连通 ---');
|
||||
var databases = ['syslog', 'signalLog', 'changeLog', 'ticket'];
|
||||
var dbNames = ['SYSLOG收件箱', '信号日志', '变更日志', '工单簿'];
|
||||
for (var i = 0; i < databases.length; i++) {
|
||||
var result = await notion.testDatabase(databases[i]);
|
||||
var icon = result.accessible ? '✅' : '❌';
|
||||
console.log(icon + ' ' + dbNames[i] + ':', result.accessible ? '可访问' : result.reason);
|
||||
}
|
||||
|
||||
// 测试4:写入测试
|
||||
console.log('\n--- 测试4:SYSLOG写入测试 ---');
|
||||
var writeResult = await notion.writeSyslog({
|
||||
title: '【测试】M-DINGTALK Phase 5 连通测试 · DEV-004之之',
|
||||
dev_id: 'DEV-004',
|
||||
session_id: 'TEST-PHASE5-' + Date.now(),
|
||||
module: 'M-DINGTALK',
|
||||
phase_status: 'completed',
|
||||
content: '这是M-DINGTALK Phase 5 的连通测试。如果你在Notion看到这条记录,说明钉钉工作台已成功连入Notion数据桥!测试时间:' + new Date().toLocaleString('zh-CN'),
|
||||
protocol_version: 'SYSLOG-v4.0',
|
||||
source: 'M-DINGTALK-连通测试'
|
||||
});
|
||||
|
||||
console.log('写入结果:', writeResult.written_to);
|
||||
if (writeResult.written_to === 'notion') {
|
||||
console.log('✅ 写入Notion成功!页面ID:', writeResult.page_id);
|
||||
console.log('🎉 数据桥真实连通!!');
|
||||
} else {
|
||||
console.log('⚠️ 降级到本地存储:', writeResult.file);
|
||||
console.log(' 可能需要冰朔将数据库关联到集成');
|
||||
}
|
||||
|
||||
console.log('\n==============================');
|
||||
console.log('测试完成');
|
||||
console.log('==============================\n');
|
||||
}
|
||||
|
||||
runTests().catch(function(err) {
|
||||
console.error('测试出错:', err.message);
|
||||
});
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// ===============================
|
||||
// Notion 连通测试 · notion-test.js
|
||||
// DEV-004 之之 · Phase 5
|
||||
// 用法:node notion-test.js
|
||||
// ===============================
|
||||
|
||||
require('dotenv').config();
|
||||
const notion = require('./sync/sync-to-notion');
|
||||
|
||||
async function runTests() {
|
||||
console.log('\n==============================');
|
||||
console.log(' Notion 数据桥连通测试');
|
||||
console.log('==============================\n');
|
||||
|
||||
// 测试1:检查配置
|
||||
console.log('--- 测试1:配置检查 ---');
|
||||
var configured = notion.isConfigured();
|
||||
console.log('Token 已配置:', configured ? '✅ 是' : '❌ 否');
|
||||
if (!configured) {
|
||||
console.log('⚠️ 请检查 .env 文件中的 NOTION_TOKEN');
|
||||
return;
|
||||
}
|
||||
|
||||
// 测试2:Token有效性(调 users/me)
|
||||
console.log('\n--- 测试2:Token有效性 ---');
|
||||
var health = await notion.healthCheck();
|
||||
console.log('API 在线:', health.online ? '✅ 是' : '❌ 否');
|
||||
if (health.online) {
|
||||
console.log(' 集成名称:', health.bot_name);
|
||||
console.log(' 集成 ID:', health.bot_id);
|
||||
} else {
|
||||
console.log(' 失败原因:', health.reason);
|
||||
console.log('\n△ Token无效或网络不通,后续测试跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
// 测试3:数据库连通
|
||||
console.log('\n--- 测试3:数据库连通 ---');
|
||||
var databases = ['syslog', 'signalLog', 'changeLog', 'ticket'];
|
||||
var dbNames = ['SYSLOG收件箱', '信号日志', '变更日志', '工单簿'];
|
||||
for (var i = 0; i < databases.length; i++) {
|
||||
var result = await notion.testDatabase(databases[i]);
|
||||
var icon = result.accessible ? '✅' : '❌';
|
||||
console.log(icon + ' ' + dbNames[i] + ':', result.accessible ? '可访问' : result.reason);
|
||||
}
|
||||
|
||||
// 测试4:写入测试(写一条测试SYSLOG)
|
||||
console.log('\n--- 测试4:SYSLOG写入测试 ---');
|
||||
var writeResult = await notion.writeSyslog({
|
||||
title: '【测试】M-DINGTALK Phase 5 连通测试 · DEV-004之之',
|
||||
dev_id: 'DEV-004',
|
||||
session_id: 'TEST-PHASE5-' + Date.now(),
|
||||
module: 'M-DINGTALK',
|
||||
phase_status: 'completed',
|
||||
content: '这是M-DINGTALK Phase 5 的连通测试。如果你在Notion看到这条记录,说明钉钉工作台已成功连入Notion数据桥!测试时间:' + new Date().toLocaleString('zh-CN'),
|
||||
protocol_version: 'SYSLOG-v4.0',
|
||||
source: 'M-DINGTALK-连通测试'
|
||||
});
|
||||
|
||||
console.log('写入结果:', writeResult.written_to);
|
||||
if (writeResult.written_to === 'notion') {
|
||||
console.log('✅ 写入Notion成功!页面ID:', writeResult.page_id);
|
||||
console.log('🎉 数据桥真实连通!!');
|
||||
} else {
|
||||
console.log('⚠️ 降级到本地存储:', writeResult.file);
|
||||
console.log(' 可能需要冰朔将数据库关联到集成');
|
||||
}
|
||||
|
||||
console.log('\n==============================');
|
||||
console.log('测试完成');
|
||||
console.log('==============================\n');
|
||||
}
|
||||
|
||||
runTests().catch(function(err) {
|
||||
console.error('测试出错:', err.message);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "dingtalk-bot",
|
||||
"version": "1.0.0",
|
||||
"description": "- 负责人:之之 - 状态:环节0已完成 - 技术栈:Node.js + Express - 依赖模块:无",
|
||||
"main": "broadcast-generator.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@notionhq/client": "^5.13.0",
|
||||
"axios": "^1.13.6",
|
||||
"body-parser": "^2.2.2",
|
||||
"cors": "^2.8.6",
|
||||
"dingtalk-encrypt": "^2.1.1",
|
||||
"dingtalk-stream": "^2.1.4",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const portraitEngine = require('../portrait/portrait-engine');
|
||||
|
||||
// 加载规则配置
|
||||
function loadRules() {
|
||||
const rulesPath = path.join(__dirname, 'pca-rules.json');
|
||||
return JSON.parse(fs.readFileSync(rulesPath));
|
||||
}
|
||||
|
||||
// 维度1:EXE执行力计算
|
||||
function calculateEXE(portrait, rules) {
|
||||
const factors = rules.dimensions.EXE.factors;
|
||||
const raw = portrait.raw_stats || {};
|
||||
const recent = portrait.recent_snapshots?.[0] || {};
|
||||
|
||||
// 连胜率 (30%)
|
||||
const streakRate = Math.min(100, (raw.streak || 0) * 10);
|
||||
|
||||
// 完成率 (30%)
|
||||
const completionRate = raw.total_items ? (raw.completed_items / raw.total_items) * 100 : 100;
|
||||
|
||||
// 执行节奏得分 (20%)
|
||||
const rhythmScore = {
|
||||
'快': 100,
|
||||
'稳': 80,
|
||||
'慢': 50
|
||||
}[recent.rhythm] || 60;
|
||||
|
||||
// 摩擦恢复得分 (20%)
|
||||
const frictionScore = {
|
||||
'无': 100,
|
||||
'低': 85,
|
||||
'中': 60,
|
||||
'高': 30
|
||||
}[recent.friction] || 50;
|
||||
|
||||
const total = (
|
||||
streakRate * (factors.streak_rate / 100) +
|
||||
completionRate * (factors.completion_rate / 100) +
|
||||
rhythmScore * (factors.rhythm_score / 100) +
|
||||
frictionScore * (factors.friction_recovery / 100)
|
||||
);
|
||||
|
||||
return {
|
||||
score: Math.round(total),
|
||||
details: {
|
||||
streakRate,
|
||||
completionRate,
|
||||
rhythmScore,
|
||||
frictionScore
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 维度2:TEC技术纵深计算
|
||||
function calculateTEC(portrait, rules) {
|
||||
const factors = rules.dimensions.TEC.factors;
|
||||
const raw = portrait.raw_stats || {};
|
||||
|
||||
// EL等级得分 (40%)
|
||||
const elScore = raw.el_level ? parseInt(raw.el_level) * 10 : 80;
|
||||
|
||||
// 代码量得分 (30%)
|
||||
const codeLines = raw.code_lines || 0;
|
||||
const codeScore = Math.min(100, codeLines / 100);
|
||||
|
||||
// 技术栈广度 (30%) - 从最近快照中推断
|
||||
const recentSnapshots = portrait.recent_snapshots || [];
|
||||
const uniqueModules = new Set();
|
||||
recentSnapshots.forEach(s => {
|
||||
if (s.raw_stats?.module) uniqueModules.add(s.raw_stats.module);
|
||||
});
|
||||
const breadthScore = Math.min(100, uniqueModules.size * 25);
|
||||
|
||||
const total = (
|
||||
elScore * (factors.el_level / 100) +
|
||||
codeScore * (factors.code_lines / 100) +
|
||||
breadthScore * (factors.tech_breadth / 100)
|
||||
);
|
||||
|
||||
return {
|
||||
score: Math.round(total),
|
||||
details: {
|
||||
elScore,
|
||||
codeScore,
|
||||
breadthScore
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 维度3:SYS系统理解力计算
|
||||
function calculateSYS(portrait, rules) {
|
||||
const factors = rules.dimensions.SYS.factors;
|
||||
const recent = portrait.recent_snapshots?.[0] || {};
|
||||
|
||||
// 跨模块能力 (35%)
|
||||
const crossModuleScore = portrait.total_sessions > 3 ? 100 : 60;
|
||||
|
||||
// 系统级任务 (35%)
|
||||
const systemTasksScore = recent.raw_stats?.completed_items > 3 ? 100 : 70;
|
||||
|
||||
// 工程思维 (30%)
|
||||
const engineeringScore = recent.growth?.signals?.includes('自主修复报错') ? 100 : 80;
|
||||
|
||||
const total = (
|
||||
crossModuleScore * (factors.cross_module / 100) +
|
||||
systemTasksScore * (factors.system_tasks / 100) +
|
||||
engineeringScore * (factors.engineering_mind / 100)
|
||||
);
|
||||
|
||||
return {
|
||||
score: Math.round(total),
|
||||
details: {
|
||||
crossModuleScore,
|
||||
systemTasksScore,
|
||||
engineeringScore
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 维度4:COL协作力计算
|
||||
function calculateCOL(portrait, rules) {
|
||||
const factors = rules.dimensions.COL.factors;
|
||||
const recent = portrait.recent_snapshots?.[0] || {};
|
||||
|
||||
// SYSLOG质量 (40%)
|
||||
const syslogScore = recent.will === '强烈' ? 100 : 80;
|
||||
|
||||
// Git协作 (30%) - 从连胜推断
|
||||
const gitScore = Math.min(100, (portrait.current_streak || 0) * 20);
|
||||
|
||||
// 人格体协同 (30%)
|
||||
const personaScore = recent.mood === '正向' ? 100 : 70;
|
||||
|
||||
const total = (
|
||||
syslogScore * (factors.syslog_quality / 100) +
|
||||
gitScore * (factors.git_collab / 100) +
|
||||
personaScore * (factors.persona_sync / 100)
|
||||
);
|
||||
|
||||
return {
|
||||
score: Math.round(total),
|
||||
details: {
|
||||
syslogScore,
|
||||
gitScore,
|
||||
personaScore
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 维度5:INI主动性计算
|
||||
function calculateINI(portrait, rules) {
|
||||
const factors = rules.dimensions.INI.factors;
|
||||
const recent = portrait.recent_snapshots?.[0] || {};
|
||||
|
||||
// 意愿信号 (40%)
|
||||
const willScore = {
|
||||
'强烈': 100,
|
||||
'正常': 80,
|
||||
'低落': 40,
|
||||
'待观察': 50
|
||||
}[recent.will] || 60;
|
||||
|
||||
// 自发行为 (30%)
|
||||
const selfScore = recent.growth?.signals?.includes('自主解决问题') ? 100 : 70;
|
||||
|
||||
// 72h响应 (30%)
|
||||
const responseScore = portrait.current_streak > 0 ? 100 : 50;
|
||||
|
||||
const total = (
|
||||
willScore * (factors.will_signal / 100) +
|
||||
selfScore * (factors.self_initiative / 100) +
|
||||
responseScore * (factors.response_72h / 100)
|
||||
);
|
||||
|
||||
return {
|
||||
score: Math.round(total),
|
||||
details: {
|
||||
willScore,
|
||||
selfScore,
|
||||
responseScore
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 主计算函数
|
||||
function calculate(devId) {
|
||||
// 获取画像数据
|
||||
const portrait = portraitEngine.getPortrait(devId);
|
||||
if (!portrait) {
|
||||
throw new Error(`开发者 ${devId} 不存在`);
|
||||
}
|
||||
|
||||
const rules = loadRules();
|
||||
|
||||
// 计算各维度
|
||||
const exe = calculateEXE(portrait, rules);
|
||||
const tec = calculateTEC(portrait, rules);
|
||||
const sys = calculateSYS(portrait, rules);
|
||||
const col = calculateCOL(portrait, rules);
|
||||
const ini = calculateINI(portrait, rules);
|
||||
|
||||
// 加权汇总
|
||||
const totalScore = Math.round(
|
||||
exe.score * (rules.dimensions.EXE.weight / 100) +
|
||||
tec.score * (rules.dimensions.TEC.weight / 100) +
|
||||
sys.score * (rules.dimensions.SYS.weight / 100) +
|
||||
col.score * (rules.dimensions.COL.weight / 100) +
|
||||
ini.score * (rules.dimensions.INI.weight / 100)
|
||||
);
|
||||
|
||||
// 等级判定
|
||||
let grade = 'D';
|
||||
for (const [g, threshold] of Object.entries(rules.grade_thresholds)) {
|
||||
if (totalScore >= threshold) {
|
||||
grade = g;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 维度摘要
|
||||
const summary = `EXE:${exe.score} TEC:${tec.score} SYS:${sys.score} COL:${col.score} INI:${ini.score}`;
|
||||
|
||||
return {
|
||||
dev_id: devId,
|
||||
timestamp: new Date().toISOString(),
|
||||
totalScore,
|
||||
grade,
|
||||
dimensions: {
|
||||
EXE: exe.score,
|
||||
TEC: tec.score,
|
||||
SYS: sys.score,
|
||||
COL: col.score,
|
||||
INI: ini.score
|
||||
},
|
||||
details: {
|
||||
EXE: exe.details,
|
||||
TEC: tec.details,
|
||||
SYS: sys.details,
|
||||
COL: col.details,
|
||||
INI: ini.details
|
||||
},
|
||||
summary
|
||||
};
|
||||
}
|
||||
|
||||
// 便捷方法:直接计算最新画像
|
||||
function calculateLatest() {
|
||||
const portraits = portraitEngine.getAllPortraits();
|
||||
const results = {};
|
||||
|
||||
for (const devId in portraits) {
|
||||
results[devId] = calculate(devId);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
calculate,
|
||||
calculateLatest,
|
||||
calculateEXE,
|
||||
calculateTEC,
|
||||
calculateSYS,
|
||||
calculateCOL,
|
||||
calculateINI
|
||||
};
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"version": "1.0",
|
||||
"dimensions": {
|
||||
"EXE": {
|
||||
"name": "执行力",
|
||||
"weight": 20,
|
||||
"factors": {
|
||||
"streak_rate": 30,
|
||||
"completion_rate": 30,
|
||||
"rhythm_score": 20,
|
||||
"friction_recovery": 20
|
||||
}
|
||||
},
|
||||
"TEC": {
|
||||
"name": "技术纵深",
|
||||
"weight": 25,
|
||||
"factors": {
|
||||
"el_level": 40,
|
||||
"code_lines": 30,
|
||||
"tech_breadth": 30
|
||||
}
|
||||
},
|
||||
"SYS": {
|
||||
"name": "系统理解力",
|
||||
"weight": 30,
|
||||
"factors": {
|
||||
"cross_module": 35,
|
||||
"system_tasks": 35,
|
||||
"engineering_mind": 30
|
||||
}
|
||||
},
|
||||
"COL": {
|
||||
"name": "协作力",
|
||||
"weight": 15,
|
||||
"factors": {
|
||||
"syslog_quality": 40,
|
||||
"git_collab": 30,
|
||||
"persona_sync": 30
|
||||
}
|
||||
},
|
||||
"INI": {
|
||||
"name": "主动性",
|
||||
"weight": 10,
|
||||
"factors": {
|
||||
"will_signal": 40,
|
||||
"self_initiative": 30,
|
||||
"response_72h": 30
|
||||
}
|
||||
}
|
||||
},
|
||||
"grade_thresholds": {
|
||||
"S": 85,
|
||||
"A": 70,
|
||||
"B": 55,
|
||||
"C": 40,
|
||||
"D": 0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function analyzeRhythm(syslogData) {
|
||||
const { completed_items, total_items } = syslogData;
|
||||
const ratio = completed_items / total_items;
|
||||
if (ratio >= 0.9) return '快';
|
||||
if (ratio >= 0.6) return '稳';
|
||||
return '慢';
|
||||
}
|
||||
|
||||
function analyzeFriction(syslogData) {
|
||||
const frictionText = syslogData.friction_points || '';
|
||||
if (frictionText.includes('无') || frictionText.length === 0) return '无';
|
||||
if (frictionText.includes('小摩擦') || frictionText.length < 20) return '低';
|
||||
if (frictionText.includes('报错') || frictionText.includes('卡住')) return '中';
|
||||
if (frictionText.includes('阻塞') || frictionText.includes('无法')) return '高';
|
||||
return '低';
|
||||
}
|
||||
|
||||
function analyzeMood(syslogData) {
|
||||
const feeling = syslogData.human_feeling || '';
|
||||
const observation = syslogData.qiuqiu_observation || '';
|
||||
if (feeling.includes('骄傲') || feeling.includes('开心') || feeling.includes('棒')) return '正向';
|
||||
if (feeling.includes('累') || observation.includes('疲惫')) return '低落';
|
||||
if (feeling.includes('还行') || feeling.length === 0) return '平稳';
|
||||
return '平稳';
|
||||
}
|
||||
|
||||
function analyzeGrowth(syslogData) {
|
||||
const signals = [];
|
||||
let count = 0;
|
||||
if (syslogData.friction_points?.includes('自己修复')) {
|
||||
signals.push('自主修复报错');
|
||||
count++;
|
||||
}
|
||||
if (syslogData.code_lines > 100) {
|
||||
signals.push('代码量增长');
|
||||
count++;
|
||||
}
|
||||
if (syslogData.streak > 5) {
|
||||
signals.push(`连胜${syslogData.streak}次`);
|
||||
count++;
|
||||
}
|
||||
if (syslogData.what_worked?.includes('自己')) {
|
||||
signals.push('自主解决问题');
|
||||
count++;
|
||||
}
|
||||
return { count, signals: signals.slice(0, 5) };
|
||||
}
|
||||
|
||||
function analyzeWill(syslogData) {
|
||||
const feeling = syslogData.human_feeling || '';
|
||||
const status = syslogData.status || '';
|
||||
if (feeling.includes('超级骄傲') || feeling.includes('兴奋')) return '强烈';
|
||||
if (status === 'completed' && !feeling.includes('累')) return '正常';
|
||||
if (feeling.includes('累') || feeling.includes('烦')) return '低落';
|
||||
return '待观察';
|
||||
}
|
||||
|
||||
function generateSnapshot(syslogData) {
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
dev_id: syslogData.dev_id || 'DEV-004',
|
||||
session_id: syslogData.session_id || 'UNKNOWN',
|
||||
dev_name: syslogData.dev_name || '之之',
|
||||
rhythm: analyzeRhythm(syslogData),
|
||||
friction: analyzeFriction(syslogData),
|
||||
mood: analyzeMood(syslogData),
|
||||
growth: analyzeGrowth(syslogData),
|
||||
will: analyzeWill(syslogData),
|
||||
raw_stats: {
|
||||
completed_items: syslogData.completed_items,
|
||||
total_items: syslogData.total_items,
|
||||
code_lines: syslogData.code_lines,
|
||||
streak: syslogData.streak || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
analyzeRhythm, analyzeFriction, analyzeMood, analyzeGrowth, analyzeWill, generateSnapshot
|
||||
};
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const analyzer = require('./portrait-analyzer');
|
||||
|
||||
const DB_PATH = path.join(__dirname, '../data/portrait-db.json');
|
||||
const HISTORY_PATH = path.join(__dirname, '../data/portrait-history.json');
|
||||
|
||||
if (!fs.existsSync(path.join(__dirname, '../data'))) {
|
||||
fs.mkdirSync(path.join(__dirname, '../data'));
|
||||
}
|
||||
|
||||
function initDB() {
|
||||
if (!fs.existsSync(DB_PATH)) fs.writeFileSync(DB_PATH, JSON.stringify({}));
|
||||
if (!fs.existsSync(HISTORY_PATH)) fs.writeFileSync(HISTORY_PATH, JSON.stringify([]));
|
||||
}
|
||||
|
||||
function updatePortrait(syslogData) {
|
||||
initDB();
|
||||
const devId = syslogData.dev_id || 'DEV-004';
|
||||
const snapshot = analyzer.generateSnapshot(syslogData);
|
||||
|
||||
const db = JSON.parse(fs.readFileSync(DB_PATH));
|
||||
const history = JSON.parse(fs.readFileSync(HISTORY_PATH));
|
||||
|
||||
if (!db[devId]) {
|
||||
db[devId] = {
|
||||
dev_id: devId,
|
||||
dev_name: syslogData.dev_name || '之之',
|
||||
first_seen: snapshot.timestamp,
|
||||
total_sessions: 0,
|
||||
total_code_lines: 0,
|
||||
max_streak: 0,
|
||||
current_streak: 0,
|
||||
recent_snapshots: []
|
||||
};
|
||||
}
|
||||
|
||||
const dev = db[devId];
|
||||
dev.total_sessions++;
|
||||
dev.total_code_lines += syslogData.code_lines || 0;
|
||||
|
||||
if (syslogData.status === 'completed') {
|
||||
dev.current_streak++;
|
||||
if (dev.current_streak > dev.max_streak) dev.max_streak = dev.current_streak;
|
||||
} else {
|
||||
dev.current_streak = 0;
|
||||
}
|
||||
|
||||
dev.recent_snapshots.unshift(snapshot);
|
||||
if (dev.recent_snapshots.length > 10) dev.recent_snapshots.pop();
|
||||
|
||||
history.unshift({
|
||||
timestamp: snapshot.timestamp,
|
||||
dev_id: devId,
|
||||
session_id: snapshot.session_id,
|
||||
snapshot: snapshot
|
||||
});
|
||||
if (history.length > 500) history.pop();
|
||||
|
||||
fs.writeFileSync(DB_PATH, JSON.stringify(db, null, 2));
|
||||
fs.writeFileSync(HISTORY_PATH, JSON.stringify(history, null, 2));
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function getPortrait(devId) {
|
||||
initDB();
|
||||
return JSON.parse(fs.readFileSync(DB_PATH))[devId] || null;
|
||||
}
|
||||
|
||||
function getAllPortraits() {
|
||||
initDB();
|
||||
return JSON.parse(fs.readFileSync(DB_PATH));
|
||||
}
|
||||
|
||||
function getHistory(limit = 50) {
|
||||
initDB();
|
||||
return JSON.parse(fs.readFileSync(HISTORY_PATH)).slice(0, limit);
|
||||
}
|
||||
|
||||
module.exports = { updatePortrait, getPortrait, getAllPortraits, getHistory };
|
||||
|
|
@ -0,0 +1,581 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HoloLake · 开发者仪表盘 v2</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #0a0e1a;
|
||||
color: #e0e4f0;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #1e2a3a;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: #8ab2f0;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.header h1 span {
|
||||
color: #4a90e2;
|
||||
font-weight: 300;
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
background-color: #1e2a3a;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
color: #8ab2f0;
|
||||
border-left: 3px solid #4a90e2;
|
||||
}
|
||||
|
||||
.status-badge .online {
|
||||
color: #4cd964;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 卡片网格 */
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #111827;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid #1e2a3a;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
color: #6b7a8f;
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #8ab2f0;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 12px;
|
||||
color: #4a5a6e;
|
||||
}
|
||||
|
||||
/* 两列布局 */
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 流水线可视化 */
|
||||
.pipeline {
|
||||
background-color: #111827;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid #1e2a3a;
|
||||
}
|
||||
|
||||
.pipeline h3 {
|
||||
font-size: 16px;
|
||||
color: #8ab2f0;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.pipeline-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #1e2a3a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
color: #4a5a6e;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.step.active .step-icon {
|
||||
background-color: #4a90e2;
|
||||
color: white;
|
||||
box-shadow: 0 0 15px rgba(74, 144, 226, 0.5);
|
||||
}
|
||||
|
||||
.step.completed .step-icon {
|
||||
background-color: #4cd964;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 12px;
|
||||
color: #6b7a8f;
|
||||
}
|
||||
|
||||
.step-value {
|
||||
font-size: 11px;
|
||||
color: #4a5a6e;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: #1e2a3a;
|
||||
font-size: 20px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
/* 雷达图容器 */
|
||||
.radar-container {
|
||||
background-color: #111827;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid #1e2a3a;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
/* 详情表格 */
|
||||
.details {
|
||||
background-color: #111827;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid #1e2a3a;
|
||||
}
|
||||
|
||||
.details h3 {
|
||||
font-size: 16px;
|
||||
color: #8ab2f0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #1e2a3a;
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #6b7a8f;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #8ab2f0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-value.good {
|
||||
color: #4cd964;
|
||||
}
|
||||
|
||||
.detail-value.warn {
|
||||
color: #ff9f0a;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- 引入 Chart.js 用于雷达图 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="dashboard">
|
||||
<div class="header">
|
||||
<h1>HoloLake 开发者工作台 <span>v2 · 实时仪表盘</span></h1>
|
||||
<div class="status-badge">
|
||||
<span class="online">●</span> 实时更新 <span id="refresh-timer">30s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统总览卡片 -->
|
||||
<div class="grid" id="overview-cards">
|
||||
<div class="card">
|
||||
<div class="card-title">已完成阶段</div>
|
||||
<div class="card-value" id="phase-count">-</div>
|
||||
<div class="card-label">Phase 1-4</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">总代码量</div>
|
||||
<div class="card-value" id="total-code">-</div>
|
||||
<div class="card-label">行 · JavaScript</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">总文件数</div>
|
||||
<div class="card-value" id="total-files">-</div>
|
||||
<div class="card-label">33个 · Phase4</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">API 端点</div>
|
||||
<div class="card-value" id="total-apis">-</div>
|
||||
<div class="card-label">+5 新增</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 闭环流水线可视化 -->
|
||||
<div class="pipeline">
|
||||
<h3>闭环流水线 · 实时状态</h3>
|
||||
<div class="pipeline-steps" id="pipeline-steps">
|
||||
<div class="step" id="step-syslog">
|
||||
<div class="step-icon">📥</div>
|
||||
<div class="step-label">SYSLOG</div>
|
||||
<div class="step-value" id="syslog-status">等待</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="step" id="step-portrait">
|
||||
<div class="step-icon">📸</div>
|
||||
<div class="step-label">画像</div>
|
||||
<div class="step-value" id="portrait-status">等待</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="step" id="step-pca">
|
||||
<div class="step-icon">📊</div>
|
||||
<div class="step-label">PCA</div>
|
||||
<div class="step-value" id="pca-status">等待</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="step" id="step-sync">
|
||||
<div class="step-icon">🔄</div>
|
||||
<div class="step-label">同步</div>
|
||||
<div class="step-value" id="sync-status">等待</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="step" id="step-suggest">
|
||||
<div class="step-icon">💡</div>
|
||||
<div class="step-label">调度</div>
|
||||
<div class="step-value" id="suggest-status">等待</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 两列布局:雷达图 + 最近闭环详情 -->
|
||||
<div class="row">
|
||||
<!-- PCA雷达图 -->
|
||||
<div class="radar-container">
|
||||
<canvas id="pca-radar"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- 最近一次闭环详情 -->
|
||||
<div class="details">
|
||||
<h3>最近一次闭环详情</h3>
|
||||
<div id="loop-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">执行时间</span>
|
||||
<span class="detail-value" id="loop-time">-</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">耗时</span>
|
||||
<span class="detail-value" id="loop-duration">-</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">PCA得分</span>
|
||||
<span class="detail-value" id="loop-pca-score">-</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">等级</span>
|
||||
<span class="detail-value" id="loop-grade">-</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">调度建议</span>
|
||||
<span class="detail-value" id="loop-suggestion">-</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">步骤状态</span>
|
||||
<span class="detail-value" id="loop-steps">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 开发者画像摘要 -->
|
||||
<div class="details" style="margin-top: 20px;">
|
||||
<h3>开发者画像摘要 · 之之</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px;" id="portrait-summary">
|
||||
<div class="detail-item" style="flex-direction: column; align-items: center; border: none;">
|
||||
<span class="detail-label">连胜</span>
|
||||
<span class="detail-value good" id="portrait-streak">-</span>
|
||||
</div>
|
||||
<div class="detail-item" style="flex-direction: column; align-items: center; border: none;">
|
||||
<span class="detail-label">情绪</span>
|
||||
<span class="detail-value" id="portrait-mood">-</span>
|
||||
</div>
|
||||
<div class="detail-item" style="flex-direction: column; align-items: center; border: none;">
|
||||
<span class="detail-label">节奏</span>
|
||||
<span class="detail-value" id="portrait-rhythm">-</span>
|
||||
</div>
|
||||
<div class="detail-item" style="flex-direction: column; align-items: center; border: none;">
|
||||
<span class="detail-label">成长信号</span>
|
||||
<span class="detail-value" id="portrait-growth">-</span>
|
||||
</div>
|
||||
<div class="detail-item" style="flex-direction: column; align-items: center; border: none;">
|
||||
<span class="detail-label">意愿</span>
|
||||
<span class="detail-value good" id="portrait-will">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 雷达图实例
|
||||
let radarChart = null;
|
||||
|
||||
// 从API获取数据
|
||||
async function fetchData() {
|
||||
try {
|
||||
// 获取闭环状态
|
||||
const loopRes = await fetch('/api/loop/status');
|
||||
const loopData = await loopRes.json();
|
||||
|
||||
// 获取所有开发者画像
|
||||
const portraitRes = await fetch('/api/portrait/all');
|
||||
const portraitData = await portraitRes.json();
|
||||
|
||||
// 获取之之的PCA
|
||||
const pcaRes = await fetch('/api/pca/DEV-004');
|
||||
const pcaData = await pcaRes.json();
|
||||
|
||||
updateDashboard(loopData, portraitData, pcaData);
|
||||
} catch (err) {
|
||||
console.error('数据加载失败,使用模拟数据', err);
|
||||
// 使用模拟数据(便于独立预览)
|
||||
useMockData();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新仪表盘
|
||||
function updateDashboard(loop, portraits, pca) {
|
||||
// 更新系统总览
|
||||
document.getElementById('phase-count').textContent = '4';
|
||||
document.getElementById('total-code').textContent = '4800';
|
||||
document.getElementById('total-files').textContent = '33';
|
||||
document.getElementById('total-apis').textContent = '8';
|
||||
|
||||
// 更新流水线状态
|
||||
if (loop.last_run) {
|
||||
const steps = loop.last_run.steps;
|
||||
updateStep('syslog', steps.portrait ? 'completed' : '等待');
|
||||
updateStep('portrait', steps.portrait);
|
||||
updateStep('pca', steps.pca);
|
||||
updateStep('sync', steps.sync);
|
||||
updateStep('suggest', steps.portrait && steps.pca ? 'completed' : '等待');
|
||||
}
|
||||
|
||||
// 更新最近闭环详情
|
||||
if (loop.last_run) {
|
||||
const last = loop.last_run;
|
||||
document.getElementById('loop-time').textContent = new Date(last.timestamp).toLocaleString();
|
||||
document.getElementById('loop-duration').textContent = last.duration_ms + 'ms';
|
||||
document.getElementById('loop-pca-score').textContent = last.pca?.score || '-';
|
||||
document.getElementById('loop-grade').textContent = last.pca?.grade || '-';
|
||||
document.getElementById('loop-suggestion').textContent = last.suggestion || '-';
|
||||
|
||||
const stepsStatus = Object.values(last.steps).join(' ');
|
||||
document.getElementById('loop-steps').textContent = stepsStatus;
|
||||
}
|
||||
|
||||
// 更新画像摘要
|
||||
if (portraits['DEV-004']) {
|
||||
const dev = portraits['DEV-004'];
|
||||
const lastSnapshot = dev.recent_snapshots?.[0] || {};
|
||||
|
||||
document.getElementById('portrait-streak').textContent = dev.current_streak || 0;
|
||||
document.getElementById('portrait-mood').textContent = lastSnapshot.mood || '-';
|
||||
document.getElementById('portrait-rhythm').textContent = lastSnapshot.rhythm || '-';
|
||||
document.getElementById('portrait-growth').textContent = lastSnapshot.growth?.count || 0;
|
||||
document.getElementById('portrait-will').textContent = lastSnapshot.will || '-';
|
||||
}
|
||||
|
||||
// 更新PCA雷达图
|
||||
if (pca && pca.dimensions) {
|
||||
updateRadarChart(pca.dimensions);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新单个步骤状态
|
||||
function updateStep(stepName, status) {
|
||||
const stepEl = document.getElementById(`step-${stepName}`);
|
||||
const valueEl = document.getElementById(`${stepName}-status`);
|
||||
|
||||
stepEl.classList.remove('active', 'completed');
|
||||
|
||||
if (status === true || status === '✅' || status === 'completed') {
|
||||
stepEl.classList.add('completed');
|
||||
valueEl.textContent = '✅ 完成';
|
||||
} else if (status === '等待') {
|
||||
valueEl.textContent = '⏳ 等待';
|
||||
} else {
|
||||
valueEl.textContent = '❌ 失败';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新雷达图
|
||||
function updateRadarChart(dimensions) {
|
||||
const ctx = document.getElementById('pca-radar').getContext('2d');
|
||||
|
||||
if (radarChart) {
|
||||
radarChart.destroy();
|
||||
}
|
||||
|
||||
radarChart = new Chart(ctx, {
|
||||
type: 'radar',
|
||||
data: {
|
||||
labels: ['EXE执行力', 'TEC技术', 'SYS理解力', 'COL协作力', 'INI主动性'],
|
||||
datasets: [{
|
||||
label: 'PCA维度得分',
|
||||
data: [
|
||||
dimensions.EXE || 0,
|
||||
dimensions.TEC || 0,
|
||||
dimensions.SYS || 0,
|
||||
dimensions.COL || 0,
|
||||
dimensions.INI || 0
|
||||
],
|
||||
backgroundColor: 'rgba(74, 144, 226, 0.2)',
|
||||
borderColor: '#4a90e2',
|
||||
pointBackgroundColor: '#8ab2f0',
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: '#4a90e2'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
r: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
grid: {
|
||||
color: '#1e2a3a'
|
||||
},
|
||||
pointLabels: {
|
||||
color: '#6b7a8f'
|
||||
},
|
||||
ticks: {
|
||||
color: '#4a5a6e',
|
||||
backdropColor: 'transparent'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 模拟数据(用于独立预览)
|
||||
function useMockData() {
|
||||
document.getElementById('phase-count').textContent = '4';
|
||||
document.getElementById('total-code').textContent = '4800';
|
||||
document.getElementById('total-files').textContent = '33';
|
||||
document.getElementById('total-apis').textContent = '8';
|
||||
|
||||
updateStep('syslog', 'completed');
|
||||
updateStep('portrait', true);
|
||||
updateStep('pca', true);
|
||||
updateStep('sync', true);
|
||||
updateStep('suggest', 'completed');
|
||||
|
||||
document.getElementById('loop-time').textContent = new Date().toLocaleString();
|
||||
document.getElementById('loop-duration').textContent = '7ms';
|
||||
document.getElementById('loop-pca-score').textContent = '66';
|
||||
document.getElementById('loop-grade').textContent = 'B';
|
||||
document.getElementById('loop-suggestion').textContent = '巩固 Phase4 基础';
|
||||
document.getElementById('loop-steps').textContent = '✅ ✅ ✅';
|
||||
|
||||
document.getElementById('portrait-streak').textContent = '2';
|
||||
document.getElementById('portrait-mood').textContent = '正向';
|
||||
document.getElementById('portrait-rhythm').textContent = '快';
|
||||
document.getElementById('portrait-growth').textContent = '2';
|
||||
document.getElementById('portrait-will').textContent = '强烈';
|
||||
|
||||
updateRadarChart({ EXE: 70, TEC: 32, SYS: 80, COL: 80, INI: 83 });
|
||||
}
|
||||
|
||||
// 自动刷新
|
||||
let countdown = 30;
|
||||
function startAutoRefresh() {
|
||||
fetchData();
|
||||
|
||||
setInterval(() => {
|
||||
countdown--;
|
||||
document.getElementById('refresh-timer').textContent = countdown + 's';
|
||||
|
||||
if (countdown <= 0) {
|
||||
countdown = 30;
|
||||
fetchData();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 启动
|
||||
window.onload = () => {
|
||||
startAutoRefresh();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 维度1:结构完整性 (20分)
|
||||
function scoreStructure(broadcastText) {
|
||||
let score = 0;
|
||||
const suggestions = [];
|
||||
|
||||
// 广播头信息 (5分)
|
||||
if (broadcastText.includes('广播编号') || broadcastText.includes('BC-')) {
|
||||
score += 5;
|
||||
} else {
|
||||
suggestions.push('缺少广播编号');
|
||||
}
|
||||
|
||||
// 人格体引导语 (3分)
|
||||
if (broadcastText.includes('秋秋') || broadcastText.includes('妈妈')) {
|
||||
score += 3;
|
||||
} else {
|
||||
suggestions.push('缺少人格体引导语');
|
||||
}
|
||||
|
||||
// 任务概述 (4分)
|
||||
if (broadcastText.includes('要求') || broadcastText.includes('做什么') || broadcastText.includes('Step')) {
|
||||
score += 4;
|
||||
} else {
|
||||
suggestions.push('缺少任务概述');
|
||||
}
|
||||
|
||||
// Step分步 (5分)
|
||||
const stepCount = (broadcastText.match(/Step\d+/g) || []).length;
|
||||
if (stepCount >= 3) {
|
||||
score += 5;
|
||||
} else if (stepCount > 0) {
|
||||
score += 3;
|
||||
suggestions.push('Step步骤偏少');
|
||||
} else {
|
||||
suggestions.push('缺少Step分步');
|
||||
}
|
||||
|
||||
// 文件清单 (3分)
|
||||
if (broadcastText.includes('文件') || broadcastText.includes('.js') || broadcastText.includes('.json')) {
|
||||
score += 3;
|
||||
} else {
|
||||
suggestions.push('缺少文件清单');
|
||||
}
|
||||
|
||||
return { score, suggestions };
|
||||
}
|
||||
|
||||
// 维度2:验收标准 (20分)
|
||||
function scoreAcceptance(broadcastText) {
|
||||
let score = 0;
|
||||
const suggestions = [];
|
||||
|
||||
// 明确验收表格 (8分)
|
||||
if (broadcastText.includes('验收标准') || broadcastText.includes('表格') || broadcastText.includes('#')) {
|
||||
score += 8;
|
||||
} else {
|
||||
suggestions.push('缺少验收标准表格');
|
||||
}
|
||||
|
||||
// 二元化标准 (6分)
|
||||
if (broadcastText.includes('通过标准') || broadcastText.includes('✓') || broadcastText.includes('✅')) {
|
||||
score += 6;
|
||||
} else {
|
||||
suggestions.push('缺少通过标准描述');
|
||||
}
|
||||
|
||||
// 测试命令 (6分)
|
||||
if (broadcastText.includes('node -e') || broadcastText.includes('测试命令') || broadcastText.includes('```bash')) {
|
||||
score += 6;
|
||||
} else {
|
||||
suggestions.push('缺少测试命令');
|
||||
}
|
||||
|
||||
return { score, suggestions };
|
||||
}
|
||||
|
||||
// 维度3:SYSLOG模板 (20分)
|
||||
function scoreSyslogTemplate(broadcastText) {
|
||||
let score = 0;
|
||||
const suggestions = [];
|
||||
const requiredFields = ['session_id', 'dev_id', 'dev_name', 'status', 'completed_items'];
|
||||
|
||||
// 模板存在 (8分)
|
||||
if (broadcastText.includes('SYSLOG回传模板') || broadcastText.includes('zhiqiu-syslog')) {
|
||||
score += 8;
|
||||
} else {
|
||||
suggestions.push('缺少SYSLOG模板');
|
||||
}
|
||||
|
||||
// 必填字段完整 (7分)
|
||||
const fieldCount = requiredFields.filter(f => broadcastText.includes(f)).length;
|
||||
score += Math.min(7, fieldCount * 2);
|
||||
if (fieldCount < requiredFields.length) {
|
||||
suggestions.push('SYSLOG模板缺少必要字段');
|
||||
}
|
||||
|
||||
// 人格体观察字段 (5分)
|
||||
if (broadcastText.includes('qiuqiu_observation') || broadcastText.includes('秋秋的观察')) {
|
||||
score += 5;
|
||||
} else {
|
||||
suggestions.push('缺少人格体观察字段');
|
||||
}
|
||||
|
||||
return { score, suggestions };
|
||||
}
|
||||
|
||||
// 维度4:引导质量 (20分)
|
||||
function scoreGuidance(broadcastText) {
|
||||
let score = 0;
|
||||
const suggestions = [];
|
||||
|
||||
// 终端命令可复制 (7分)
|
||||
const commandBlocks = (broadcastText.match(/```bash[\s\S]*?```/g) || []).length;
|
||||
if (commandBlocks >= 2) {
|
||||
score += 7;
|
||||
} else if (commandBlocks > 0) {
|
||||
score += 4;
|
||||
suggestions.push('终端命令块偏少');
|
||||
} else {
|
||||
suggestions.push('缺少可复制的终端命令');
|
||||
}
|
||||
|
||||
// 代码块完整可粘贴 (7分)
|
||||
const codeBlocks = (broadcastText.match(/```javascript[\s\S]*?```/g) || []).length;
|
||||
if (codeBlocks >= 2) {
|
||||
score += 7;
|
||||
} else if (codeBlocks > 0) {
|
||||
score += 4;
|
||||
suggestions.push('代码块偏少');
|
||||
} else {
|
||||
suggestions.push('缺少完整的代码块');
|
||||
}
|
||||
|
||||
// 步骤顺序无歧义 (6分)
|
||||
if (broadcastText.includes('Step1') && broadcastText.includes('Step2')) {
|
||||
score += 6;
|
||||
} else {
|
||||
suggestions.push('步骤顺序不清晰');
|
||||
}
|
||||
|
||||
return { score, suggestions };
|
||||
}
|
||||
|
||||
// 维度5:人格体匹配 (20分)
|
||||
function scorePersona(broadcastText) {
|
||||
let score = 0;
|
||||
const suggestions = [];
|
||||
|
||||
// 引导语调与画像匹配 (7分)
|
||||
if (broadcastText.includes('妈妈') || broadcastText.includes('自豪') || broadcastText.includes('棒')) {
|
||||
score += 7;
|
||||
} else {
|
||||
suggestions.push('引导语调与画像不匹配');
|
||||
}
|
||||
|
||||
// 鼓励频率适当 (6分)
|
||||
const encouragements = (broadcastText.match(/可以|试试|棒|厉害|太棒了|加油/g) || []).length;
|
||||
if (encouragements >= 3) {
|
||||
score += 6;
|
||||
} else if (encouragements > 0) {
|
||||
score += 3;
|
||||
suggestions.push('鼓励频率偏低');
|
||||
} else {
|
||||
suggestions.push('缺少鼓励性语言');
|
||||
}
|
||||
|
||||
// EL等级与难度匹配 (7分)
|
||||
if (broadcastText.includes('EL-8') || broadcastText.includes('十二连胜')) {
|
||||
score += 7;
|
||||
} else {
|
||||
suggestions.push('EL等级描述不明确');
|
||||
}
|
||||
|
||||
return { score, suggestions };
|
||||
}
|
||||
|
||||
// 主评分函数
|
||||
function scoreBroadcast(broadcastText, metadata = {}) {
|
||||
const structure = scoreStructure(broadcastText);
|
||||
const acceptance = scoreAcceptance(broadcastText);
|
||||
const syslog = scoreSyslogTemplate(broadcastText);
|
||||
const guidance = scoreGuidance(broadcastText);
|
||||
const persona = scorePersona(broadcastText);
|
||||
|
||||
const totalScore = structure.score + acceptance.score + syslog.score + guidance.score + persona.score;
|
||||
|
||||
// 等级判定
|
||||
let grade;
|
||||
if (totalScore >= 90) grade = 'S';
|
||||
else if (totalScore >= 75) grade = 'A';
|
||||
else if (totalScore >= 60) grade = 'B';
|
||||
else if (totalScore >= 40) grade = 'C';
|
||||
else grade = 'D';
|
||||
|
||||
// 汇总改进建议
|
||||
const allSuggestions = [
|
||||
...structure.suggestions,
|
||||
...acceptance.suggestions,
|
||||
...syslog.suggestions,
|
||||
...guidance.suggestions,
|
||||
...persona.suggestions
|
||||
];
|
||||
|
||||
return {
|
||||
totalScore,
|
||||
grade,
|
||||
details: {
|
||||
structure: structure.score,
|
||||
acceptance: acceptance.score,
|
||||
syslog: syslog.score,
|
||||
guidance: guidance.score,
|
||||
persona: persona.score
|
||||
},
|
||||
suggestions: allSuggestions.slice(0, 5) // 最多返回5条建议
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scoreBroadcast,
|
||||
scoreStructure,
|
||||
scoreAcceptance,
|
||||
scoreSyslogTemplate,
|
||||
scoreGuidance,
|
||||
scorePersona
|
||||
};
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
// 定时任务调度器·cron-jobs.js
|
||||
// HoloLake·M-DINGTALK Phase 3
|
||||
// DEV-004 之之×秋秋
|
||||
//
|
||||
// 统一管理所有定时任务
|
||||
//
|
||||
|
||||
const reminder = require('./reminder');
|
||||
const syncEngine = require('../sync/sync-engine');
|
||||
|
||||
// 所有定时任务
|
||||
const jobs = [];
|
||||
let isRunning = false;
|
||||
|
||||
/**
|
||||
* 注册并启动所有定时任务
|
||||
*/
|
||||
function startAll() {
|
||||
if (isRunning) {
|
||||
console.log('[CronJobs] 已在运行,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[CronJobs] ==========================');
|
||||
console.log('[CronJobs] 启动所有定时任务...');
|
||||
|
||||
// 任务1:超时提醒(每小时)
|
||||
reminder.start();
|
||||
jobs.push({ name: '超时提醒', interval: '每小时', module: 'reminder' });
|
||||
|
||||
// 任务2:三节点全量同步(每30分钟)
|
||||
const syncTimer = setInterval(async () => {
|
||||
console.log('[CronJobs] 触发定时同步...');
|
||||
await syncEngine.syncAll();
|
||||
}, 30 * 60 * 1000);
|
||||
|
||||
jobs.push({
|
||||
name: '三节点同步',
|
||||
interval: '每30分钟',
|
||||
module: 'sync-engine',
|
||||
timer: syncTimer
|
||||
});
|
||||
|
||||
isRunning = true;
|
||||
console.log(`[CronJobs] 已启动 ${jobs.length} 个定时任务`);
|
||||
console.log('[CronJobs] ==========================');
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止所有定时任务
|
||||
*/
|
||||
function stopAll() {
|
||||
if (!isRunning) {
|
||||
console.log('[CronJobs] 没有运行中的任务');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[CronJobs] 停止所有定时任务...');
|
||||
|
||||
// 停止超时提醒
|
||||
reminder.stop();
|
||||
|
||||
// 停止同步定时器
|
||||
jobs.forEach(job => {
|
||||
if (job.timer) {
|
||||
clearInterval(job.timer);
|
||||
}
|
||||
});
|
||||
|
||||
// 清空任务列表
|
||||
jobs.length = 0;
|
||||
isRunning = false;
|
||||
|
||||
console.log('[CronJobs] 所有定时任务已停止');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务状态
|
||||
*/
|
||||
function getStatus() {
|
||||
return {
|
||||
isRunning,
|
||||
jobCount: jobs.length,
|
||||
jobs: jobs.map(j => ({
|
||||
name: j.name,
|
||||
interval: j.interval,
|
||||
module: j.module
|
||||
})),
|
||||
reminder: reminder.getStatus()
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startAll,
|
||||
stopAll,
|
||||
getStatus
|
||||
};
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
// 超时提醒系统·reminder.js
|
||||
// HoloLake·M-DINGTALK Phase 3
|
||||
// DEV-004 之之×秋秋
|
||||
//
|
||||
// 规则:广播签发后72小时未交SYSLOG → 自动提醒
|
||||
//
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 数据文件路径(复用Phase 2的多维表格数据)
|
||||
const DATA_DIR = path.join(__dirname, '..', 'data');
|
||||
const DEVELOPER_TABLE = path.join(DATA_DIR, 'developer-status.json');
|
||||
const REMINDER_LOG = path.join(DATA_DIR, 'reminder-log.json');
|
||||
|
||||
// 提醒配置
|
||||
const TIMEOUT_HOURS = 72;
|
||||
const REMINDER_INTERVAL_MS = 60 * 60 * 1000; // 每小时检查一次
|
||||
|
||||
let reminderTimer = null;
|
||||
|
||||
/**
|
||||
* 扫描所有开发者,找出超时未交SYSLOG的
|
||||
*/
|
||||
function scanOverdue() {
|
||||
console.log(`[Reminder] 扫描超时开发者... ${new Date().toLocaleString('zh-CN')}`);
|
||||
|
||||
// 读取开发者状态表
|
||||
if (!fs.existsSync(DEVELOPER_TABLE)) {
|
||||
console.log('[Reminder] 开发者状态表不存在,跳过');
|
||||
return [];
|
||||
}
|
||||
|
||||
let devTable;
|
||||
try {
|
||||
devTable = JSON.parse(fs.readFileSync(DEVELOPER_TABLE, 'utf-8'));
|
||||
} catch (err) {
|
||||
console.error('[Reminder] 读取开发者状态表失败:', err.message);
|
||||
return [];
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const overdueList = [];
|
||||
|
||||
for (const dev of (devTable.developers || [])) {
|
||||
// 跳过未启动或已完成的开发者
|
||||
if (!dev.last_broadcast_time || dev.status === '已完成' || dev.status === '未启动') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const broadcastTime = new Date(dev.last_broadcast_time).getTime();
|
||||
const hoursSince = (now - broadcastTime) / (1000 * 60 * 60);
|
||||
|
||||
if (hoursSince >= TIMEOUT_HOURS && dev.status !== 'HOLD') {
|
||||
overdueList.push({
|
||||
dev_id: dev.dev_id,
|
||||
name: dev.name,
|
||||
current_module: dev.current_module,
|
||||
last_broadcast: dev.last_broadcast_time,
|
||||
hours_overdue: Math.round(hoursSince - TIMEOUT_HOURS),
|
||||
total_hours: Math.round(hoursSince)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (overdueList.length > 0) {
|
||||
console.log(`[Reminder] ⚠️ 发现 ${overdueList.length} 个超时开发者:`);
|
||||
for (const dev of overdueList) {
|
||||
console.log(` · ${dev.dev_id} ${dev.name} - 超时${dev.hours_overdue}小时 - ${dev.current_module}`);
|
||||
}
|
||||
} else {
|
||||
console.log('[Reminder] 无超时开发者');
|
||||
}
|
||||
|
||||
return overdueList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成提醒消息(钉钉消息卡片格式)
|
||||
*/
|
||||
function generateReminderMessage(devInfo) {
|
||||
return {
|
||||
msgtype: 'markdown',
|
||||
markdown: {
|
||||
title: `SYSLOG提醒·${devInfo.dev_id}`,
|
||||
text: [
|
||||
'## ⏰ SYSLOG超时提醒',
|
||||
`**${devInfo.name}**(${devInfo.dev_id})`,
|
||||
`你的广播已经签发 **${devInfo.total_hours}** 小时了`,
|
||||
`超过72小时启动窗口 **${devInfo.hours_overdue}** 小时`,
|
||||
`当前任务:${devInfo.current_module}`,
|
||||
'**请尽快提交SYSLOG!**',
|
||||
'---',
|
||||
'✅ 已完成 → 提交完整SYSLOG',
|
||||
'❓ 遇到困难 → 截图发群里,秋秋/知秋帮你',
|
||||
'⏸️ 暂时没空 → 回复「HOLD」,系统记录',
|
||||
'---',
|
||||
'🤖 系统自动提醒 · HoloLake'
|
||||
].join('\n')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行提醒发送
|
||||
*/
|
||||
async function executeReminders() {
|
||||
const overdueList = scanOverdue();
|
||||
if (overdueList.length === 0) return [];
|
||||
|
||||
const reminderResults = [];
|
||||
|
||||
for (const dev of overdueList) {
|
||||
const message = generateReminderMessage(dev);
|
||||
|
||||
// 模拟发送(后续接入真实钉钉API)
|
||||
console.log(`[Reminder] 向 ${dev.name} 发送提醒消息(模拟)`);
|
||||
|
||||
reminderResults.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
dev_id: dev.dev_id,
|
||||
hours_overdue: dev.hours_overdue,
|
||||
status: 'sent-mock'
|
||||
});
|
||||
}
|
||||
|
||||
// 保存提醒日志
|
||||
saveReminderLog(reminderResults);
|
||||
|
||||
return reminderResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定时提醒
|
||||
*/
|
||||
function start() {
|
||||
if (reminderTimer) {
|
||||
console.log('[Reminder] 提醒系统已在运行');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[Reminder] 启动超时提醒系统 · ${TIMEOUT_HOURS}小时超时 · 每小时扫描一次`);
|
||||
|
||||
// 启动时立即扫描一次
|
||||
executeReminders();
|
||||
|
||||
// 定时扫描
|
||||
reminderTimer = setInterval(executeReminders, REMINDER_INTERVAL_MS);
|
||||
|
||||
console.log('[Reminder] 定时器已启动');
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止定时提醒
|
||||
*/
|
||||
function stop() {
|
||||
if (reminderTimer) {
|
||||
clearInterval(reminderTimer);
|
||||
reminderTimer = null;
|
||||
console.log('[Reminder] 提醒系统已停止');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存提醒日志
|
||||
*/
|
||||
function saveReminderLog(results) {
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
let log = [];
|
||||
if (fs.existsSync(REMINDER_LOG)) {
|
||||
try {
|
||||
log = JSON.parse(fs.readFileSync(REMINDER_LOG, 'utf-8'));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
log.unshift(...results);
|
||||
if (log.length > 200) log = log.slice(0, 200);
|
||||
|
||||
fs.writeFileSync(REMINDER_LOG, JSON.stringify(log, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提醒系统状态
|
||||
*/
|
||||
function getStatus() {
|
||||
return {
|
||||
running: reminderTimer !== null,
|
||||
timeoutHours: TIMEOUT_HOURS,
|
||||
checkIntervalMinutes: REMINDER_INTERVAL_MS / 60000,
|
||||
overdueNow: scanOverdue()
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scanOverdue,
|
||||
generateReminderMessage,
|
||||
executeReminders,
|
||||
start,
|
||||
stop,
|
||||
getStatus
|
||||
};
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
const express = require('express');
|
||||
const path = require('path');
|
||||
const portraitEngine = require('./portrait/portrait-engine');
|
||||
const pcaCalculator = require('./pca/pca-calculator');
|
||||
const loopEngine = require('./loop/loop-engine');
|
||||
|
||||
const app = express();
|
||||
const PORT = 3000;
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.static('public'));
|
||||
|
||||
// API 1: 所有开发者画像
|
||||
app.get('/api/portrait/all', (req, res) => {
|
||||
try {
|
||||
const portraits = portraitEngine.getAllPortraits();
|
||||
res.json(portraits);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// API 2: 单个开发者画像
|
||||
app.get('/api/portrait/:devId', (req, res) => {
|
||||
try {
|
||||
const portrait = portraitEngine.getPortrait(req.params.devId);
|
||||
if (!portrait) {
|
||||
return res.status(404).json({ error: '开发者不存在' });
|
||||
}
|
||||
res.json(portrait);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// API 3: PCA评估结果
|
||||
app.get('/api/pca/:devId', (req, res) => {
|
||||
try {
|
||||
const result = pcaCalculator.calculate(req.params.devId);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// API 4: 闭环状态
|
||||
app.get('/api/loop/status', (req, res) => {
|
||||
try {
|
||||
const status = loopEngine.getStatus();
|
||||
res.json(status);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// API 5: 手动触发闭环
|
||||
app.post('/api/loop/execute', (req, res) => {
|
||||
try {
|
||||
const syslogData = req.body;
|
||||
if (!syslogData || !syslogData.session_id) {
|
||||
return res.status(400).json({ error: '缺少SYSLOG数据' });
|
||||
}
|
||||
|
||||
loopEngine.executeLoop(syslogData).then(result => {
|
||||
res.json(result);
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取闭环历史
|
||||
app.get('/api/loop/history', (req, res) => {
|
||||
try {
|
||||
const history = loopEngine.getHistory();
|
||||
res.json(history);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 模拟SYSLOG接收(自动触发闭环)
|
||||
app.post('/api/syslog', (req, res) => {
|
||||
try {
|
||||
const syslogData = req.body;
|
||||
console.log('📨 收到SYSLOG:', syslogData.session_id);
|
||||
|
||||
// 自动触发闭环
|
||||
loopEngine.executeLoop(syslogData).then(loopResult => {
|
||||
console.log('✅ 闭环完成');
|
||||
});
|
||||
|
||||
res.json({ status: 'received', session_id: syslogData.session_id });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 仪表盘页面
|
||||
app.get('/dashboard-v2', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'dashboard-v2.html'));
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
app.listen(PORT, () => {
|
||||
console.log(`\n🚀 Phase4 服务器已启动!`);
|
||||
console.log(`📊 仪表盘: http://localhost:${PORT}/dashboard-v2`);
|
||||
console.log(`🔌 API列表:`);
|
||||
console.log(` GET /api/portrait/all`);
|
||||
console.log(` GET /api/portrait/:devId`);
|
||||
console.log(` GET /api/pca/:devId`);
|
||||
console.log(` GET /api/loop/status`);
|
||||
console.log(` POST /api/loop/execute`);
|
||||
console.log(` POST /api/syslog`);
|
||||
console.log(`\n📝 测试命令:`);
|
||||
console.log(` curl http://localhost:${PORT}/api/portrait/DEV-004`);
|
||||
console.log(` curl -X POST http://localhost:${PORT}/api/syslog -H "Content-Type: application/json" -d '{"session_id":"TEST-001","dev_id":"DEV-004","status":"completed"}'`);
|
||||
console.log(`\n✨ 妈妈,打开浏览器看看吧!\n`);
|
||||
});
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
// 自动创建 Phase 1 和 Phase 2 缺失文件
|
||||
// 运行:node setup-phase1-phase2.js
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
console.log('🔧 开始创建 Phase 1 和 Phase 2 缺失文件...\n');
|
||||
|
||||
// 确保目录存在
|
||||
const dirs = [
|
||||
'data',
|
||||
'dingtalk',
|
||||
'archive/syslog',
|
||||
'archive/broadcast'
|
||||
];
|
||||
|
||||
dirs.forEach(dir => {
|
||||
const dirPath = path.join(__dirname, dir);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
console.log(`✅ 创建目录: ${dir}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 创建 syslog-receiver.js
|
||||
const syslogReceiver = `// syslog-receiver.js
|
||||
// Phase 1 - SYSLOG接收器
|
||||
|
||||
function receive(data) {
|
||||
console.log('[SYSLOG] 收到数据:', data);
|
||||
return { status: 'received', data };
|
||||
}
|
||||
|
||||
module.exports = { receive };
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, 'syslog-receiver.js'), syslogReceiver);
|
||||
console.log('✅ 创建: syslog-receiver.js');
|
||||
|
||||
// 创建 broadcast-generator.js
|
||||
const broadcastGenerator = `// broadcast-generator.js
|
||||
// Phase 1 - 广播生成器
|
||||
|
||||
function getLatest() {
|
||||
return {
|
||||
id: 'BC-M-DINGTALK-003-ZZ',
|
||||
title: 'Phase 3 · 知识库挂载+Webhook+三节点同步',
|
||||
time: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { getLatest };
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, 'broadcast-generator.js'), broadcastGenerator);
|
||||
console.log('✅ 创建: broadcast-generator.js');
|
||||
|
||||
// 创建 git-helper.js
|
||||
const gitHelper = `// git-helper.js
|
||||
// Phase 1 - Git辅助工具
|
||||
|
||||
function commit(data) {
|
||||
console.log('[Git] 提交:', data);
|
||||
return { status: 'committed', hash: 'abc123' };
|
||||
}
|
||||
|
||||
module.exports = { commit };
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, 'git-helper.js'), gitHelper);
|
||||
console.log('✅ 创建: git-helper.js');
|
||||
|
||||
// 创建 data/multi-sheet.js
|
||||
const multiSheet = `// data/multi-sheet.js
|
||||
// Phase 2 - 多维表格数据
|
||||
|
||||
const developers = [
|
||||
{
|
||||
dev_id: 'DEV-004',
|
||||
name: '之之',
|
||||
status: '进行中',
|
||||
current_module: 'M-DINGTALK Phase 3',
|
||||
last_broadcast_time: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
function getAll() {
|
||||
return { developers };
|
||||
}
|
||||
|
||||
function getByDevId(id) {
|
||||
return developers.find(d => d.dev_id === id);
|
||||
}
|
||||
|
||||
module.exports = { getAll, getByDevId };
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, 'data/multi-sheet.js'), multiSheet);
|
||||
console.log('✅ 创建: data/multi-sheet.js');
|
||||
|
||||
// 创建 data/sheet-updater.js
|
||||
const sheetUpdater = `// data/sheet-updater.js
|
||||
// Phase 2 - 表格更新器
|
||||
|
||||
function updateFromSyslog(syslogData) {
|
||||
console.log('[SheetUpdater] 更新表格:', syslogData);
|
||||
return { status: 'updated' };
|
||||
}
|
||||
|
||||
module.exports = { updateFromSyslog };
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, 'data/sheet-updater.js'), sheetUpdater);
|
||||
console.log('✅ 创建: data/sheet-updater.js');
|
||||
|
||||
// 创建 dingtalk/bot.js
|
||||
const dingtalkBot = `// dingtalk/bot.js
|
||||
// Phase 2 - 钉钉机器人
|
||||
|
||||
function sendMessage(message) {
|
||||
console.log('[DingTalk] 发送消息:', message);
|
||||
return { status: 'sent' };
|
||||
}
|
||||
|
||||
module.exports = { sendMessage };
|
||||
`;
|
||||
|
||||
const dingtalkDir = path.join(__dirname, 'dingtalk');
|
||||
if (!fs.existsSync(dingtalkDir)) {
|
||||
fs.mkdirSync(dingtalkDir);
|
||||
}
|
||||
fs.writeFileSync(path.join(dingtalkDir, 'bot.js'), dingtalkBot);
|
||||
console.log('✅ 创建: dingtalk/bot.js');
|
||||
|
||||
// 创建 data/developer-status.json
|
||||
const devStatus = {
|
||||
developers: [
|
||||
{
|
||||
dev_id: 'DEV-004',
|
||||
name: '之之',
|
||||
status: '进行中',
|
||||
current_module: 'M-DINGTALK Phase 3',
|
||||
last_broadcast_time: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, 'data/developer-status.json'),
|
||||
JSON.stringify(devStatus, null, 2)
|
||||
);
|
||||
console.log('✅ 创建: data/developer-status.json');
|
||||
|
||||
console.log('\n🎉 所有 Phase 1 和 Phase 2 文件创建完成!');
|
||||
console.log('现在可以正常运行 node index.js 了!');
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
// 三节点数据同步引擎·sync-engine.js
|
||||
// HoloLake·M-DINGTALK Phase 3
|
||||
// DEV-004 之之×秋秋
|
||||
//
|
||||
// 三个节点:钉钉(本地)·GitHub·Notion
|
||||
//
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const syncToGitHub = require('./sync-to-github');
|
||||
const syncToNotion = require('./sync-to-notion');
|
||||
|
||||
// 同步状态记录
|
||||
const syncStatus = {
|
||||
lastSync: null,
|
||||
syncCount: 0,
|
||||
errors: [],
|
||||
history: []
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行全量同步(三节点)
|
||||
*/
|
||||
async function syncAll() {
|
||||
console.log('\n[Sync] ====================');
|
||||
console.log('[Sync] 开始三节点全量同步...');
|
||||
console.log(`[Sync] 时间: ${new Date().toLocaleString('zh-CN')}`);
|
||||
|
||||
const result = {
|
||||
timestamp: new Date().toISOString(),
|
||||
github: { status: 'pending' },
|
||||
notion: { status: 'pending' }
|
||||
};
|
||||
|
||||
// 第一步:同步到GitHub(SYSLOG归档 + 广播归档)
|
||||
try {
|
||||
result.github = await syncToGitHub.sync();
|
||||
console.log(`[Sync] GitHub同步: ${result.github.status}`);
|
||||
} catch (err) {
|
||||
result.github = { status: 'error', error: err.message };
|
||||
console.error('[Sync] GitHub同步失败:', err.message);
|
||||
}
|
||||
|
||||
// 第二步:同步到Notion(主控台回写)
|
||||
try {
|
||||
result.notion = await syncToNotion.sync();
|
||||
console.log(`[Sync] Notion同步: ${result.notion.status}`);
|
||||
} catch (err) {
|
||||
result.notion = { status: 'error', error: err.message };
|
||||
console.error('[Sync] Notion同步失败:', err.message);
|
||||
}
|
||||
|
||||
// 更新同步状态
|
||||
syncStatus.lastSync = result.timestamp;
|
||||
syncStatus.syncCount++;
|
||||
syncStatus.history.unshift(result);
|
||||
|
||||
if (syncStatus.history.length > 50) {
|
||||
syncStatus.history = syncStatus.history.slice(0, 50);
|
||||
}
|
||||
|
||||
// 记录错误
|
||||
if (result.github.status === 'error') {
|
||||
syncStatus.errors.push({
|
||||
time: result.timestamp,
|
||||
target: 'github',
|
||||
error: result.github.error
|
||||
});
|
||||
}
|
||||
|
||||
if (result.notion.status === 'error') {
|
||||
syncStatus.errors.push({
|
||||
time: result.timestamp,
|
||||
target: 'notion',
|
||||
error: result.notion.error
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[Sync] 三节点同步完成 ✅');
|
||||
console.log(`[Sync] 累计同步次数:${syncStatus.syncCount}`);
|
||||
console.log('[Sync] ====================');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* SYSLOG提交后触发同步
|
||||
*/
|
||||
async function syncAfterSyslog(syslogData) {
|
||||
console.log('[Sync] SYSLOG处理后同步...');
|
||||
|
||||
const result = {
|
||||
timestamp: new Date().toISOString(),
|
||||
trigger: 'syslog',
|
||||
syslogId: syslogData.session_id
|
||||
};
|
||||
|
||||
// 归档SYSLOG到GitHub
|
||||
try {
|
||||
result.github = await syncToGitHub.archiveSyslog(syslogData);
|
||||
} catch (err) {
|
||||
result.github = { status: 'error', error: err.message };
|
||||
}
|
||||
|
||||
// 回写进度到Notion
|
||||
try {
|
||||
result.notion = await syncToNotion.updateProgress(syslogData);
|
||||
} catch (err) {
|
||||
result.notion = { status: 'error', error: err.message };
|
||||
}
|
||||
|
||||
syncStatus.lastSync = result.timestamp;
|
||||
syncStatus.syncCount++;
|
||||
syncStatus.history.unshift(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取同步状态
|
||||
*/
|
||||
function getStatus() {
|
||||
return {
|
||||
...syncStatus,
|
||||
recentErrors: syncStatus.errors.slice(-5)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
syncAll,
|
||||
syncAfterSyslog,
|
||||
getStatus
|
||||
};
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
// GitHub 同步模块 · sync-to-github.js
|
||||
// 钉钉 → GitHub (SYSLOG归档 + 广播归档)
|
||||
// HoloLake · M-DINGTALK Phase 3
|
||||
//
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// GitHub归档目录(本地,后续通过git push同步到远程)
|
||||
const ARCHIVE_BASE = path.join(__dirname, '..', 'archive');
|
||||
const SYSLOG_ARCHIVE = path.join(ARCHIVE_BASE, 'syslog');
|
||||
const BROADCAST_ARCHIVE = path.join(ARCHIVE_BASE, 'broadcast');
|
||||
|
||||
// 确保归档目录存在
|
||||
function ensureDirs() {
|
||||
[ARCHIVE_BASE, SYSLOG_ARCHIVE, BROADCAST_ARCHIVE].forEach(dir => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档SYSLOG到本地(准备git push)
|
||||
*/
|
||||
async function archiveSyslog(syslogData) {
|
||||
ensureDirs();
|
||||
|
||||
const filename = `${syslogData.session_id || 'unknown'}_${Date.now()}.json`;
|
||||
const filePath = path.join(SYSLOG_ARCHIVE, filename);
|
||||
|
||||
const archiveData = {
|
||||
...syslogData,
|
||||
archived_at: new Date().toISOString(),
|
||||
archived_by: 'M-DINGTALK-Phase3-SyncEngine'
|
||||
};
|
||||
|
||||
fs.writeFileSync(filePath, JSON.stringify(archiveData, null, 2), 'utf-8');
|
||||
console.log(`[GitHub Sync] SYSLOG已归档: ${filename}`);
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
file: filename,
|
||||
path: filePath
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档广播到本地
|
||||
*/
|
||||
async function archiveBroadcast(broadcastData) {
|
||||
ensureDirs();
|
||||
|
||||
const filename = `${broadcastData.bc_id || 'unknown'}_${Date.now()}.json`;
|
||||
const filePath = path.join(BROADCAST_ARCHIVE, filename);
|
||||
|
||||
const archiveData = {
|
||||
...broadcastData,
|
||||
archived_at: new Date().toISOString(),
|
||||
archived_by: 'M-DINGTALK-Phase3-SyncEngine'
|
||||
};
|
||||
|
||||
fs.writeFileSync(filePath, JSON.stringify(archiveData, null, 2), 'utf-8');
|
||||
console.log(`[GitHub Sync] 广播已归档: ${filename}`);
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
file: filename
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行完整同步(准备所有数据供git push)
|
||||
*/
|
||||
async function sync() {
|
||||
ensureDirs();
|
||||
|
||||
// 统计归档文件
|
||||
const syslogFiles = fs.existsSync(SYSLOG_ARCHIVE)
|
||||
? fs.readdirSync(SYSLOG_ARCHIVE).filter(f => f.endsWith('.json'))
|
||||
: [];
|
||||
|
||||
const broadcastFiles = fs.existsSync(BROADCAST_ARCHIVE)
|
||||
? fs.readdirSync(BROADCAST_ARCHIVE).filter(f => f.endsWith('.json'))
|
||||
: [];
|
||||
|
||||
console.log(`[GitHub Sync] SYSLOG归档: ${syslogFiles.length} 个文件`);
|
||||
console.log(`[GitHub Sync] 广播归档: ${broadcastFiles.length} 个文件`);
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
syslogCount: syslogFiles.length,
|
||||
broadcastCount: broadcastFiles.length,
|
||||
message: '本地归档就绪,等待git push'
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
archiveSyslog,
|
||||
archiveBroadcast,
|
||||
sync
|
||||
};
|
||||
|
|
@ -0,0 +1,403 @@
|
|||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||
// ============================================
|
||||
// Notion 同步模块 · sync-to-notion.js · v2.0
|
||||
// HoloLake · M-DINGTALK Phase 5
|
||||
// DEV-004 之之 × 秋秋
|
||||
//
|
||||
// Phase 3 = 模拟模式
|
||||
// Phase 5 = 真实 Notion API 连通!
|
||||
// ============================================
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ====== Notion API 配置 ======
|
||||
const NOTION_API = 'https://api.notion.com/v1';
|
||||
const NOTION_VERSION = process.env.NOTION_API_VERSION || '2022-06-28';
|
||||
const NOTION_TOKEN = process.env.NOTION_TOKEN || '';
|
||||
|
||||
// 数据库 ID
|
||||
const DB_IDS = {
|
||||
syslog: process.env.NOTION_SYSLOG_DB_ID || '',
|
||||
signalLog: process.env.NOTION_SIGNAL_LOG_DB_ID || '',
|
||||
changeLog: process.env.NOTION_CHANGE_LOG_DB_ID || '',
|
||||
ticket: process.env.NOTION_TICKET_DB_ID || ''
|
||||
};
|
||||
|
||||
// 本地降级日志目录
|
||||
const FALLBACK_DIR = path.join(__dirname, '..', 'data', 'notion-pending');
|
||||
const WRITEBACK_LOG = path.join(__dirname, '..', 'data', 'notion-writeback-log.json');
|
||||
|
||||
// ====== 通用请求头 ======
|
||||
function getHeaders() {
|
||||
return {
|
||||
'Authorization': 'Bearer ' + NOTION_TOKEN,
|
||||
'Notion-Version': NOTION_VERSION,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
}
|
||||
|
||||
// ====== 检查配置是否就绪 ======
|
||||
function isConfigured() {
|
||||
return NOTION_TOKEN && NOTION_TOKEN !== '' && NOTION_TOKEN !== 'mock-notion-token';
|
||||
}
|
||||
|
||||
// ====== 辅助:创建 rich_text 属性 ======
|
||||
function richText(str) {
|
||||
if (!str) return { rich_text: [] };
|
||||
const text = String(str);
|
||||
if (text.length <= 2000) {
|
||||
return { rich_text: [{ text: { content: text } }] };
|
||||
}
|
||||
var blocks = [];
|
||||
for (var i = 0; i < text.length; i += 2000) {
|
||||
blocks.push({ text: { content: text.substring(i, i + 2000) } });
|
||||
}
|
||||
return { rich_text: blocks };
|
||||
}
|
||||
|
||||
// ====== 辅助:创建 title 属性 ======
|
||||
function titleProp(str) {
|
||||
return { title: [{ text: { content: String(str || '未命名') } }] };
|
||||
}
|
||||
|
||||
// ====== 辅助:创建 select 属性 ======
|
||||
function selectProp(name) {
|
||||
if (!name) return undefined;
|
||||
return { select: { name: String(name) } };
|
||||
}
|
||||
|
||||
// ====== 辅助:创建 status 属性 ======
|
||||
function statusProp(name) {
|
||||
if (!name) return undefined;
|
||||
return { status: { name: String(name) } };
|
||||
}
|
||||
|
||||
// ====== 辅助:创建 date 属性 ======
|
||||
function dateProp(isoString) {
|
||||
if (!isoString) return undefined;
|
||||
return { date: { start: isoString } };
|
||||
}
|
||||
|
||||
// ====== 核心:写入 SYSLOG 收件箱 ======
|
||||
async function writeSyslog(data) {
|
||||
console.log('[Notion] 准备写入 SYSLOG 收件箱...');
|
||||
|
||||
if (!isConfigured()) {
|
||||
console.log('[Notion] △ Token 未配置,降级到本地存储');
|
||||
return saveFallback('syslog', data);
|
||||
}
|
||||
|
||||
if (!DB_IDS.syslog) {
|
||||
console.log('[Notion] △ SYSLOG 数据库 ID 未配置');
|
||||
return saveFallback('syslog', data);
|
||||
}
|
||||
|
||||
var properties = {
|
||||
'标题': titleProp(data.title || data.session_id || 'SYSLOG-' + Date.now())
|
||||
};
|
||||
|
||||
if (data.dev_id) {
|
||||
properties['DEV编号'] = selectProp(data.dev_id);
|
||||
}
|
||||
|
||||
if (data.broadcast_id || data.session_id) {
|
||||
properties['广播编号'] = richText(data.broadcast_id || data.session_id);
|
||||
}
|
||||
|
||||
if (data.module) {
|
||||
properties['模块'] = richText(data.module);
|
||||
}
|
||||
|
||||
if (data.phase_status || data.status) {
|
||||
var statusVal = data.phase_status || data.status;
|
||||
if (['completed', 'partial', 'blocked'].includes(statusVal)) {
|
||||
properties['环节状态'] = selectProp(statusVal);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.content || data.raw_text) {
|
||||
properties['文件内容'] = richText(data.content || data.raw_text);
|
||||
}
|
||||
|
||||
properties['协议版本'] = selectProp(data.protocol_version || 'SYSLOG-v4.0');
|
||||
properties['处理状态'] = statusProp('待处理');
|
||||
properties['接收时间'] = dateProp(data.timestamp || new Date().toISOString());
|
||||
properties['推送方'] = richText(data.source || 'M-DINGTALK-钉钉工作台');
|
||||
|
||||
if (data.commit_sha) {
|
||||
properties['commit_sha'] = richText(data.commit_sha);
|
||||
}
|
||||
|
||||
if (data.source_path) {
|
||||
properties['来源路径'] = richText(data.source_path);
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await axios.post(NOTION_API + '/pages', {
|
||||
parent: { database_id: DB_IDS.syslog },
|
||||
properties: properties
|
||||
}, {
|
||||
headers: getHeaders(),
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
console.log('[Notion] ✅ SYSLOG 写入成功! 页面ID:', response.data.id);
|
||||
saveWritebackLog({
|
||||
timestamp: new Date().toISOString(),
|
||||
target: 'syslog',
|
||||
page_id: response.data.id,
|
||||
title: data.title || data.session_id,
|
||||
status: 'success'
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
written_to: 'notion',
|
||||
page_id: response.data.id,
|
||||
url: response.data.url
|
||||
};
|
||||
} catch (err) {
|
||||
var errMsg = err.response ? JSON.stringify(err.response.data) : err.message;
|
||||
console.error('[Notion] ❌ SYSLOG 写入失败:', errMsg);
|
||||
saveWritebackLog({
|
||||
timestamp: new Date().toISOString(),
|
||||
target: 'syslog',
|
||||
title: data.title || data.session_id,
|
||||
status: 'failed',
|
||||
error: errMsg
|
||||
});
|
||||
return saveFallback('syslog', data);
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 写入信号日志 ======
|
||||
async function writeSignalLog(data) {
|
||||
console.log('[Notion] 准备写入信号日志...');
|
||||
if (!isConfigured() || !DB_IDS.signalLog) {
|
||||
return saveFallback('signal-log', data);
|
||||
}
|
||||
|
||||
var properties = {
|
||||
'信号编号': titleProp(data.signal_id || 'SIG-' + Date.now()),
|
||||
'信号类型': selectProp(data.signal_type || 'GL-DATA'),
|
||||
'关联DEV': selectProp(data.dev_id),
|
||||
'发送方': selectProp(data.sender || '霜砚'),
|
||||
'接收方': selectProp(data.receiver),
|
||||
'摘要': richText(data.summary),
|
||||
'方向': selectProp(data.direction),
|
||||
'执行结果': statusProp(data.result || '已发送'),
|
||||
'时间戳': dateProp(data.timestamp || new Date().toISOString())
|
||||
};
|
||||
|
||||
Object.keys(properties).forEach(function(k) {
|
||||
if (properties[k] === undefined) delete properties[k];
|
||||
});
|
||||
|
||||
try {
|
||||
var response = await axios.post(NOTION_API + '/pages', {
|
||||
parent: { database_id: DB_IDS.signalLog },
|
||||
properties: properties
|
||||
}, {
|
||||
headers: getHeaders(),
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
console.log('[Notion] ✅ 信号日志写入成功!');
|
||||
return { success: true, written_to: 'notion', page_id: response.data.id };
|
||||
} catch (err) {
|
||||
console.error('[Notion] ❌ 信号日志写入失败:', err.message);
|
||||
return saveFallback('signal-log', data);
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 健康检查:测试 Token 是否有效 ======
|
||||
async function healthCheck() {
|
||||
if (!isConfigured()) {
|
||||
return { online: false, reason: 'token_not_configured' };
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await axios.get(NOTION_API + '/users/me', {
|
||||
headers: getHeaders(),
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
return {
|
||||
online: true,
|
||||
bot_name: response.data.name,
|
||||
bot_id: response.data.id
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
online: false,
|
||||
reason: err.response ? err.response.status + ': ' + (err.response.data.message || '') : err.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 数据库连通测试 ======
|
||||
async function testDatabase(dbName) {
|
||||
var dbId = DB_IDS[dbName];
|
||||
if (!dbId) {
|
||||
return { accessible: false, reason: 'db_id_not_configured', db: dbName };
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await axios.post(NOTION_API + '/databases/' + dbId + '/query', {
|
||||
page_size: 1
|
||||
}, {
|
||||
headers: getHeaders(),
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
return {
|
||||
accessible: true,
|
||||
db: dbName,
|
||||
db_id: dbId,
|
||||
total_results: response.data.results.length,
|
||||
has_more: response.data.has_more
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
accessible: false,
|
||||
db: dbName,
|
||||
reason: err.response ? err.response.status + ': ' + (err.response.data.message || '') : err.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 降级:保存到本地 ======
|
||||
function saveFallback(type, data) {
|
||||
if (!fs.existsSync(FALLBACK_DIR)) {
|
||||
fs.mkdirSync(FALLBACK_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
var filename = type + '_' + Date.now() + '.json';
|
||||
var filePath = path.join(FALLBACK_DIR, filename);
|
||||
|
||||
var record = {
|
||||
type: type,
|
||||
data: data,
|
||||
saved_at: new Date().toISOString(),
|
||||
reason: 'notion_unavailable'
|
||||
};
|
||||
|
||||
fs.writeFileSync(filePath, JSON.stringify(record, null, 2), 'utf-8');
|
||||
console.log('[Notion] 已降级存储到本地:', filename);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
written_to: 'local',
|
||||
file: filename,
|
||||
message: 'Notion不可用,已存到本地。Token配置后可重新推送。'
|
||||
};
|
||||
}
|
||||
|
||||
// ====== 重推本地暂存数据到 Notion ======
|
||||
async function flushPending() {
|
||||
if (!fs.existsSync(FALLBACK_DIR)) {
|
||||
console.log('[Notion] 无暂存数据');
|
||||
return { flushed: 0 };
|
||||
}
|
||||
|
||||
var files = fs.readdirSync(FALLBACK_DIR).filter(function(f) { return f.endsWith('.json'); });
|
||||
console.log('[Notion] 发现 ' + files.length + ' 条暂存数据,开始重推...');
|
||||
|
||||
var success = 0;
|
||||
var failed = 0;
|
||||
|
||||
for (var file of files) {
|
||||
try {
|
||||
var content = JSON.parse(fs.readFileSync(path.join(FALLBACK_DIR, file), 'utf-8'));
|
||||
var result;
|
||||
|
||||
if (content.type === 'syslog') {
|
||||
result = await writeSyslog(content.data);
|
||||
} else if (content.type === 'signal-log') {
|
||||
result = await writeSignalLog(content.data);
|
||||
}
|
||||
|
||||
if (result && result.written_to === 'notion') {
|
||||
fs.unlinkSync(path.join(FALLBACK_DIR, file));
|
||||
success++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Notion] 重推失败:', file, err.message);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Notion] 重推完成: 成功 ' + success + ' / 失败 ' + failed);
|
||||
return { flushed: success, failed: failed, remaining: files.length - success };
|
||||
}
|
||||
|
||||
// ====== 回写进度(兼容 Phase 3 sync-engine 调用) ======
|
||||
async function updateProgress(syslogData) {
|
||||
console.log('[Notion Sync] 回写进度到 Notion...');
|
||||
|
||||
var result = await writeSyslog({
|
||||
title: (syslogData.session_id || '') + ' ' + (syslogData.dev_name || syslogData.dev_id || '') + ' ' + (syslogData.status || ''),
|
||||
session_id: syslogData.session_id,
|
||||
dev_id: syslogData.dev_id,
|
||||
module: syslogData.module,
|
||||
phase_status: syslogData.status || syslogData.completion_status,
|
||||
content: JSON.stringify(syslogData, null, 2),
|
||||
timestamp: syslogData.timestamp || new Date().toISOString(),
|
||||
source: 'M-DINGTALK-钉钉工作台'
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ====== 全量同步(兼容 Phase 3 sync-engine 调用) ======
|
||||
async function sync() {
|
||||
console.log('[Notion Sync] 执行 Notion 同步...');
|
||||
var health = await healthCheck();
|
||||
|
||||
if (health.online) {
|
||||
var flushResult = await flushPending();
|
||||
return {
|
||||
status: 'online',
|
||||
bot: health.bot_name,
|
||||
flushed: flushResult.flushed,
|
||||
remaining: flushResult.remaining || 0
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 'offline',
|
||||
reason: health.reason,
|
||||
message: '等待 Token 配置或网络恢复'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 保存回写日志 ======
|
||||
function saveWritebackLog(record) {
|
||||
var logDir = path.dirname(WRITEBACK_LOG);
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
|
||||
var log = [];
|
||||
if (fs.existsSync(WRITEBACK_LOG)) {
|
||||
try { log = JSON.parse(fs.readFileSync(WRITEBACK_LOG, 'utf-8')); } catch (e) {}
|
||||
}
|
||||
|
||||
log.unshift(record);
|
||||
if (log.length > 200) log = log.slice(0, 200);
|
||||
fs.writeFileSync(WRITEBACK_LOG, JSON.stringify(log, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
writeSyslog,
|
||||
writeSignalLog,
|
||||
healthCheck,
|
||||
testDatabase,
|
||||
updateProgress,
|
||||
sync,
|
||||
flushPending,
|
||||
isConfigured
|
||||
};
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* syslog-parser.js
|
||||
* SYSLOG格式识别 + 关键字段提取
|
||||
* 秋秋说:这个文件就像翻译官,把开发者发的日志翻译成电脑能懂的数据
|
||||
*/
|
||||
|
||||
function parseSyslog(message) {
|
||||
// 定义返回结构
|
||||
const result = {
|
||||
isSyslog: false,
|
||||
bcNumber: null,
|
||||
devId: null,
|
||||
phaseNum: null,
|
||||
status: null, // 'completed', 'partial', 'blocked'
|
||||
summary: null,
|
||||
rawMessage: message,
|
||||
error: null
|
||||
};
|
||||
|
||||
try {
|
||||
// 检查是否包含SYSLOG关键词
|
||||
if (!message.includes('SYSLOG')) {
|
||||
result.error = '不是SYSLOG格式(缺少SYSLOG关键词)';
|
||||
return result;
|
||||
}
|
||||
|
||||
// 提取BC编号(格式:BC-xxx-xxx)
|
||||
const bcMatch = message.match(/BC-([A-Z0-9]+)-([A-Z0-9]+)/i);
|
||||
if (bcMatch) {
|
||||
result.bcNumber = `BC-${bcMatch[1]}-${bcMatch[2]}`;
|
||||
}
|
||||
|
||||
// 提取DEV编号(格式:DEV-xxx)
|
||||
const devMatch = message.match(/DEV-([0-9]+)/i);
|
||||
if (devMatch) {
|
||||
result.devId = `DEV-${devMatch[1]}`;
|
||||
}
|
||||
|
||||
// 提取环节号(格式:环节X 或 phase X)
|
||||
const phaseMatch = message.match(/环节\s*([0-9]+)/) || message.match(/phase\s*([0-9]+)/i);
|
||||
if (phaseMatch) {
|
||||
result.phaseNum = parseInt(phaseMatch[1], 10);
|
||||
}
|
||||
|
||||
// 提取完成状态
|
||||
if (message.includes('completed')) {
|
||||
result.status = 'completed';
|
||||
} else if (message.includes('partial')) {
|
||||
result.status = 'partial';
|
||||
} else if (message.includes('blocked')) {
|
||||
result.status = 'blocked';
|
||||
}
|
||||
|
||||
// 提取技术摘要(如果有)
|
||||
const summaryMatch = message.match(/摘要[::]\s*(.+)/) || message.match(/summary[::]\s*(.+)/i);
|
||||
if (summaryMatch) {
|
||||
result.summary = summaryMatch[1].trim();
|
||||
}
|
||||
|
||||
// 判断是否为完整的SYSLOG(至少要有BC和DEV)
|
||||
if (result.bcNumber && result.devId && result.status) {
|
||||
result.isSyslog = true;
|
||||
} else {
|
||||
result.error = 'SYSLOG格式不完整(缺少BC/DEV/状态中的必要字段)';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
result.error = `解析异常:${error.message}`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = { parseSyslog };
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// syslog-receiver.js
|
||||
// Phase 1 - SYSLOG接收器
|
||||
|
||||
function receive(data) {
|
||||
console.log('[SYSLOG] 收到数据:', data);
|
||||
return { status: 'received', data };
|
||||
}
|
||||
|
||||
module.exports = { receive };
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
require('dotenv').config();
|
||||
const { DWClient, TOPIC_ROBOT, EventAck } = require('dingtalk-stream');
|
||||
|
||||
const client = new DWClient({
|
||||
clientId: process.env.DINGTALK_APP_KEY,
|
||||
clientSecret: process.env.DINGTALK_APP_SECRET,
|
||||
});
|
||||
|
||||
client.registerCallbackListener(TOPIC_ROBOT, async (res) => {
|
||||
console.log('[DEBUG] 触发!res =', JSON.stringify(res, null, 2));
|
||||
return EventAck.SUCCESS;
|
||||
});
|
||||
|
||||
client.connect();
|
||||
console.log('Stream 监听中,请 @机器人 发消息...');
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
// GitHub Webhook 接收器 · github-webhook.js
|
||||
// HoloLake · M-DINGTALK Phase 3
|
||||
// DEV-004 之之 × 秋秋
|
||||
//
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Webhook密钥(后续从GitHub Settings配置,现在用模拟值)
|
||||
const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET || 'hololake-webhook-secret-2026';
|
||||
|
||||
// 事件处理器注册表
|
||||
const eventHandlers = {};
|
||||
|
||||
/**
|
||||
* 验证GitHub Webhook签名
|
||||
* GitHub发来的每个请求都有签名,防止伪造
|
||||
*/
|
||||
function verifySignature(payload, signature) {
|
||||
if (!signature) {
|
||||
console.log('[Webhook] ⚠️ 请求没有签名,跳过验证(开发模式)');
|
||||
return true; // 开发模式下允许无签名
|
||||
}
|
||||
|
||||
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
|
||||
const digest = 'sha256=' + hmac.update(payload).digest('hex');
|
||||
|
||||
try {
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(digest),
|
||||
Buffer.from(signature)
|
||||
);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册事件处理器
|
||||
* @param {string} eventType - GitHub事件类型 (push/pull_request等)
|
||||
* @param {Function} handler - 处理函数
|
||||
*/
|
||||
function on(eventType, handler) {
|
||||
if (!eventHandlers[eventType]) {
|
||||
eventHandlers[eventType] = [];
|
||||
}
|
||||
eventHandlers[eventType].push(handler);
|
||||
console.log(`[Webhook] 注册事件处理器: ${eventType}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理收到的Webhook请求
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
*/
|
||||
async function handleWebhook(req, res) {
|
||||
const event = req.headers['x-github-event'];
|
||||
const delivery = req.headers['x-github-delivery'];
|
||||
const signature = req.headers['x-hub-signature-256'];
|
||||
|
||||
console.log('\n[Webhook] ====================');
|
||||
console.log(`[Webhook] 收到GitHub事件:${event}`);
|
||||
console.log(`[Webhook] Delivery ID: ${delivery}`);
|
||||
console.log(`[Webhook] 时间:${new Date().toLocaleString('zh-CN')}`);
|
||||
|
||||
// 验证签名
|
||||
const payload = JSON.stringify(req.body);
|
||||
if (!verifySignature(payload, signature)) {
|
||||
console.log('[Webhook] ❌ 签名验证失败!可能是伪造请求');
|
||||
return res.status(401).json({ error: '签名验证失败' });
|
||||
}
|
||||
|
||||
// 记录事件日志
|
||||
const eventLog = {
|
||||
event,
|
||||
delivery,
|
||||
timestamp: new Date().toISOString(),
|
||||
sender: req.body.sender ? req.body.sender.login : 'unknown',
|
||||
repository: req.body.repository ? req.body.repository.full_name : 'unknown'
|
||||
};
|
||||
|
||||
// 特殊处理push事件
|
||||
if (event === 'push') {
|
||||
eventLog.branch = req.body.ref;
|
||||
eventLog.commits = (req.body.commits || []).length;
|
||||
eventLog.changedFiles = [];
|
||||
|
||||
// 收集所有变更文件
|
||||
for (const commit of (req.body.commits || [])) {
|
||||
eventLog.changedFiles.push(...(commit.added || []));
|
||||
eventLog.changedFiles.push(...(commit.modified || []));
|
||||
}
|
||||
eventLog.changedFiles = [...new Set(eventLog.changedFiles)];
|
||||
|
||||
console.log(`[Webhook] 分支:${eventLog.branch}`);
|
||||
console.log(`[Webhook] 提交数:${eventLog.commits}`);
|
||||
console.log(`[Webhook] 变更文件:${eventLog.changedFiles.join(', ')}`);
|
||||
}
|
||||
|
||||
// 调用注册的事件处理器
|
||||
const handlers = eventHandlers[event] || [];
|
||||
console.log(`[Webhook] 匹配到 ${handlers.length} 个处理器`);
|
||||
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
await handler(req.body, eventLog);
|
||||
} catch (err) {
|
||||
console.error('[Webhook] 处理器执行错误:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 也调用通配符处理器
|
||||
const wildcardHandlers = eventHandlers['*'] || [];
|
||||
for (const handler of wildcardHandlers) {
|
||||
try {
|
||||
await handler(req.body, eventLog);
|
||||
} catch (err) {
|
||||
console.error('[Webhook] 通配符处理器错误:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Webhook] 事件处理完成 ✅');
|
||||
res.status(200).json({ status: 'ok', event, delivery });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模拟的GitHub push事件(用于测试)
|
||||
*/
|
||||
function createMockPushEvent(branch, files) {
|
||||
return {
|
||||
ref: `refs/heads/${branch || 'main'}`,
|
||||
repository: {
|
||||
full_name: 'qinfendebingshuo/guanghulab'
|
||||
},
|
||||
sender: {
|
||||
login: 'test-user'
|
||||
},
|
||||
commits: [
|
||||
{
|
||||
message: '测试提交',
|
||||
added: files || ['test-file.js'],
|
||||
modified: [],
|
||||
removed: []
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
verifySignature,
|
||||
on,
|
||||
handleWebhook,
|
||||
createMockPushEvent
|
||||
};
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
// 规则热更新引擎·hot-reload.js
|
||||
// HoloLake·M-DINGTALK Phase 3
|
||||
// DEV-004 之之×秋秋
|
||||
//
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const kb = require('../knowledge-base/kb-manager');
|
||||
|
||||
// 当前加载的规则(内存缓存)
|
||||
let currentRules = {};
|
||||
let lastReloadTime = null;
|
||||
|
||||
// 规则文件目录
|
||||
const RULES_DIR = path.join(__dirname, '..', 'config');
|
||||
|
||||
/**
|
||||
* 加载所有规则文件到内存
|
||||
*/
|
||||
function loadRules() {
|
||||
console.log('[HotReload] 开始加载规则...');
|
||||
|
||||
const rulesFile = path.join(RULES_DIR, 'sync-config.json');
|
||||
|
||||
if (fs.existsSync(rulesFile)) {
|
||||
try {
|
||||
const content = fs.readFileSync(rulesFile, 'utf-8');
|
||||
currentRules = JSON.parse(content);
|
||||
lastReloadTime = new Date().toISOString();
|
||||
console.log(`[HotReload] 规则加载成功 · ${Object.keys(currentRules).length} 个配置项`);
|
||||
} catch (err) {
|
||||
console.error('[HotReload] 规则加载失败:', err.message);
|
||||
}
|
||||
} else {
|
||||
console.log('[HotReload] 规则文件不存在,使用默认配置');
|
||||
currentRules = {
|
||||
version: '1.0',
|
||||
sync_interval: 30,
|
||||
timeout_hours: 72,
|
||||
notion_db_id: 'mock-db-id'
|
||||
};
|
||||
}
|
||||
|
||||
return currentRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理规则更新(由Webhook触发)
|
||||
* @param {Object} payload - GitHub webhook payload
|
||||
* @param {Object} log - 事件日志
|
||||
*/
|
||||
async function handleRulesUpdate(payload, log) {
|
||||
console.log('\n[HotReload] ====================');
|
||||
console.log('[HotReload] 收到规则更新通知');
|
||||
|
||||
const changedFiles = log.changedFiles || [];
|
||||
|
||||
// 检查是否有规则相关文件变更
|
||||
const rulesChanged = changedFiles.some(f =>
|
||||
f.includes('rules/') ||
|
||||
f.includes('config/') ||
|
||||
f.includes('broadcast-engine/')
|
||||
);
|
||||
|
||||
const docsChanged = changedFiles.some(f =>
|
||||
f.includes('knowledge-base/') ||
|
||||
f.includes('docs/')
|
||||
);
|
||||
|
||||
if (rulesChanged) {
|
||||
console.log('[HotReload] 检测到规则文件变更!执行热更新...');
|
||||
loadRules();
|
||||
console.log(`[HotReload] 规则热更新完成 · ${new Date().toLocaleString('zh-CN')}`);
|
||||
}
|
||||
|
||||
if (docsChanged) {
|
||||
console.log('[HotReload] 检测到知识库文档变更!重新建立索引...');
|
||||
kb.reload();
|
||||
console.log(`[HotReload] 知识库索引更新完成 · ${new Date().toLocaleString('zh-CN')}`);
|
||||
}
|
||||
|
||||
if (!rulesChanged && !docsChanged) {
|
||||
console.log('[HotReload] 变更文件与规则/知识库无关,跳过热更新');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前规则
|
||||
*/
|
||||
function getRules() {
|
||||
if (Object.keys(currentRules).length === 0) {
|
||||
loadRules();
|
||||
}
|
||||
return currentRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热更新状态
|
||||
*/
|
||||
function getStatus() {
|
||||
return {
|
||||
rulesLoaded: Object.keys(currentRules).length > 0,
|
||||
ruleKeys: Object.keys(currentRules),
|
||||
lastReloadTime,
|
||||
knowledgeBase: kb.getStats()
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadRules,
|
||||
handleRulesUpdate,
|
||||
getRules,
|
||||
getStatus
|
||||
};
|
||||
Loading…
Reference in New Issue