DEV-004: M-DINGTALK Phase 9 服务上传 · 待人格体检查Stream连接问题

This commit is contained in:
zhizhi200271 2026-03-15 17:41:52 +08:00
commit 8eadc95701
56 changed files with 6950 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.env
node_modules/
*.bak
*.log

24
README.md Normal file
View File

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

12
broadcast-generator.js Normal file
View File

@ -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 };

27
config.json Normal file
View File

@ -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"]
}
}

139
conversation-manager.js Normal file
View File

@ -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
};

16
conversations/test.json Normal file
View File

@ -0,0 +1,16 @@
{
"userid": "test",
"updatedAt": "2026-03-15T03:35:58.320Z",
"messages": [
{
"role": "user",
"content": "测试",
"timestamp": 1773545752588
},
{
"role": "assistant",
"content": "你好呀,测试成功啦!🍭 有什么需要我帮忙的呢?无论是技术问题还是想聊聊,尽管问我哦!✨",
"timestamp": 1773545758320
}
]
}

View File

@ -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"
}
]
}

34
data/loop-history.json Normal file
View File

@ -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"
}
]

22
data/multi-sheet.js Normal file
View File

@ -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 };

View File

@ -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"
}
]

104
data/portrait-db.json Normal file
View File

@ -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
}
}
]
}
}

113
data/portrait-history.json Normal file
View File

@ -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
}
}
}
]

20
data/reminder-log.json Normal file
View File

@ -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"
}
]

9
data/sheet-updater.js Normal file
View File

@ -0,0 +1,9 @@
// data/sheet-updater.js
// Phase 2 - 表格更新器
function updateFromSyslog(syslogData) {
console.log('[SheetUpdater] 更新表格:', syslogData);
return { status: 'updated' };
}
module.exports = { updateFromSyslog };

199
dingtalk-api.js Normal file
View File

@ -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-ZZYYY是环节号部分
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
};

186
dingtalk-event-handler.js Normal file
View File

@ -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
};

165
dingtalk-webhook-v3.js Normal file
View File

@ -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
};

9
dingtalk/bot.js Normal file
View File

@ -0,0 +1,9 @@
// dingtalk/bot.js
// Phase 2 - 钉钉机器人
function sendMessage(message) {
console.log('[DingTalk] 发送消息:', message);
return { status: 'sent' };
}
module.exports = { sendMessage };

15
ecosystem.config.js Normal file
View File

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

9
git-helper.js Normal file
View File

@ -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 };

209
github-bridge.js Normal file
View File

@ -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
};

73
index-stream.js Normal file
View File

@ -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 模式已启动');

80
index.js Normal file
View File

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

64
index.js.bak.phase7 Normal file
View File

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

View File

@ -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 - 广播格式规范

View File

@ -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-ZZM17模块第5个广播之之
- BC-M22-007-AWM22模块第7个广播Awen
- BC-M-DINGTALK-003-ZZM-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小时内必须开始执行超时系统会自动提醒。

View File

@ -0,0 +1,26 @@
# 广播格式规范 v1.0
## BC编号格式
BC-{模块号}-{序号}-{开发者代号}
示例:
- BC-M17-005-ZZM17模块第5个广播之之
- BC-M22-007-AWM22模块第7个广播Awen
- BC-M-DINGTALK-003-ZZM-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架构级/全链路

View File

@ -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小时内必须开始执行超时系统会自动提醒。

View File

@ -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-ZZM17模块第5个广播之之\n- BC-M22-007-AWM22模块第7个广播Awen\n- BC-M-DINGTALK-003-ZZM-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": [
"广播签发后",
"小时内必须开始执行",
"超时系统会自动提醒"
]
}
]
}
]

View File

@ -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
};

178
llm-engine.js Normal file
View File

@ -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
};

201
loop/loop-engine.js Normal file
View File

@ -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
};

245
message-router.js Normal file
View File

@ -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
};

View File

@ -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
};

74
notion-test-nossl.js Normal file
View File

@ -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;
}
// 测试2Token有效性调 users/me
console.log('\n--- 测试2Token有效性 ---');
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--- 测试4SYSLOG写入测试 ---');
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);
});

76
notion-test.js Normal file
View File

@ -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;
}
// 测试2Token有效性调 users/me
console.log('\n--- 测试2Token有效性 ---');
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--- 测试4SYSLOG写入测试 ---');
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);
});

1050
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View File

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

268
pca/pca-calculator.js Normal file
View File

@ -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));
}
// 维度1EXE执行力计算
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
}
};
}
// 维度2TEC技术纵深计算
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
}
};
}
// 维度3SYS系统理解力计算
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
}
};
}
// 维度4COL协作力计算
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
}
};
}
// 维度5INI主动性计算
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
};

58
pca/pca-rules.json Normal file
View File

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

View File

@ -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
};

View File

@ -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 };

581
public/dashboard-v2.html Normal file
View File

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

228
quality/broadcast-scorer.js Normal file
View File

@ -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 };
}
// 维度3SYSLOG模板 (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
};

97
scheduler/cron-jobs.js Normal file
View File

@ -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
};

204
scheduler/reminder.js Normal file
View File

@ -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
};

119
server.js Normal file
View File

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

155
setup-phase1-phase2.js Normal file
View File

@ -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 了!');

133
sync/sync-engine.js Normal file
View File

@ -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' }
};
// 第一步同步到GitHubSYSLOG归档 + 广播归档)
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
};

102
sync/sync-to-github.js Normal file
View File

@ -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
};

403
sync/sync-to-notion.js Normal file
View File

@ -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
};

74
syslog-parser.js Normal file
View File

@ -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 };

9
syslog-receiver.js Normal file
View File

@ -0,0 +1,9 @@
// syslog-receiver.js
// Phase 1 - SYSLOG接收器
function receive(data) {
console.log('[SYSLOG] 收到数据:', data);
return { status: 'received', data };
}
module.exports = { receive };

15
test-stream.js Normal file
View File

@ -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 监听中,请 @机器人 发消息...');

153
webhook/github-webhook.js Normal file
View File

@ -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
};

114
webhook/hot-reload.js Normal file
View File

@ -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
};