zhizhi/backend/feishu-bot/collaboration-logger.js

257 lines
8.1 KiB
JavaScript
Raw Normal View History

// backend/feishu-bot/collaboration-logger.js
// 铸渊 · 人类×人格体协作数据采集器
//
// 记录每次协作对话的完整记录,存入 collaboration-logs/ 目录
// 数据格式对齐 persona-brain-db 五张核心表 schema
//
// 采集内容:
// - 人类发言 + 人格体回复
// - 协作通道 (霜砚/宝宝人格体)
// - 开发者画像关联
// - SYSLOG v4.0 persona/collaboration/human_feedback 字段
'use strict';
const fs = require('fs');
const path = require('path');
// ══════════════════════════════════════════════════════════
// 配置
// ══════════════════════════════════════════════════════════
const LOGS_DIR = path.resolve(__dirname, '..', '..', 'collaboration-logs');
const MAX_BUFFER_SIZE = 50; // 缓冲 50 条记录后批量写入
// ══════════════════════════════════════════════════════════
// 内存缓冲
// ══════════════════════════════════════════════════════════
let buffer = [];
// ══════════════════════════════════════════════════════════
// 日志记录
// ══════════════════════════════════════════════════════════
/**
* 记录一次对话交互
* @param {object} entry
* @param {string} entry.userId - 飞书用户 ID
* @param {string} entry.devId - 开发者编号 ( DEV-002)
* @param {string} entry.userMessage - 人类发言
* @param {string} entry.assistantReply - 人格体回复
* @param {string} entry.channel - 协作通道 (shuangyan | persona)
* @param {string} entry.personaId - 人格体 ID
* @param {object} [entry.metadata] - 额外元数据
*/
function logInteraction(entry) {
const record = {
// 对齐 persona_memory 表结构
interaction_id: generateId(),
timestamp: new Date().toISOString(),
persona_id: entry.personaId || 'ICE-GL-SY001', // 默认霜砚
type: 'collaboration',
// 对话内容
user_id: entry.userId,
dev_id: entry.devId || null,
user_message: entry.userMessage,
assistant_reply: entry.assistantReply,
channel: entry.channel || 'shuangyan',
// 对齐 dev_profiles 表
interaction_context: {
platform: 'feishu',
message_type: 'text',
channel: entry.channel || 'shuangyan',
},
// 元数据SYSLOG v4.0 字段对齐)
metadata: entry.metadata || {},
};
buffer.push(record);
// 达到缓冲上限时自动刷新
if (buffer.length >= MAX_BUFFER_SIZE) {
flush();
}
}
/**
* 记录 SYSLOG 回传中的协作数据
* @param {object} syslog - SYSLOG v4.0 完整数据
*/
function logSyslogCollaboration(syslog) {
if (!syslog) return;
const devId = syslog.dev_id || syslog.developer_id || 'UNKNOWN';
const broadcastId = syslog.broadcast_id || syslog.broadcastId || 'UNKNOWN';
const record = {
interaction_id: generateId(),
timestamp: new Date().toISOString(),
type: 'syslog_collaboration',
dev_id: devId,
broadcast_id: broadcastId,
protocol_version: syslog.protocol_version,
// SYSLOG v4.0 协作相关字段
persona: syslog.persona || null,
collaboration: syslog.collaboration || null,
human_feedback: syslog.human_feedback || null,
status: syslog.status || null,
summary: syslog.summary || null,
// 对齐 persona_memory 表
memory_entry: {
persona_id: syslog.persona?.persona_id || 'ICE-GL-SY001',
type: 'event',
title: 'SYSLOG 回传 · ' + broadcastId + ' · ' + devId,
importance: calculateImportance(syslog),
related_dev: devId,
related_broadcast: broadcastId,
tags: ['syslog', 'collaboration', broadcastId],
},
};
buffer.push(record);
if (buffer.length >= MAX_BUFFER_SIZE) {
flush();
}
}
// ══════════════════════════════════════════════════════════
// 文件写入
// ══════════════════════════════════════════════════════════
/**
* 将缓冲中的记录写入文件
*/
function flush() {
if (buffer.length === 0) return;
if (!fs.existsSync(LOGS_DIR)) {
fs.mkdirSync(LOGS_DIR, { recursive: true });
}
const dateStr = new Date().toISOString().split('T')[0];
const filename = 'collab-' + dateStr + '.jsonl';
const filepath = path.join(LOGS_DIR, filename);
// 使用 JSONL 格式(每行一个 JSON 对象)
const lines = buffer.map(r => JSON.stringify(r)).join('\n') + '\n';
try {
fs.appendFileSync(filepath, lines, 'utf8');
} catch (e) {
// 写入失败时不丢失数据,保留在缓冲中
return;
}
buffer = [];
}
/**
* 生成导出数据对齐 persona-brain-db 的5张核心表
* @returns {object} 结构化数据可直接导入到 persona-brain-db
*/
function exportForBrainDB() {
flush(); // 先刷新缓冲
const files = [];
try {
if (fs.existsSync(LOGS_DIR)) {
files.push(...fs.readdirSync(LOGS_DIR)
.filter(f => f.startsWith('collab-') && f.endsWith('.jsonl'))
.sort()
.reverse()
.slice(0, 30)); // 最近 30 天
}
} catch (e) {
return { error: e.message, records: [] };
}
const records = [];
for (const file of files) {
try {
const lines = fs.readFileSync(path.join(LOGS_DIR, file), 'utf8')
.split('\n')
.filter(l => l.trim());
for (const line of lines) {
try {
records.push(JSON.parse(line));
} catch (e) { /* skip malformed lines */ }
}
} catch (e) { /* skip unreadable files */ }
}
// 结构化为 persona-brain-db 表格式
return {
total_records: records.length,
persona_memory_entries: records
.filter(r => r.memory_entry)
.map(r => r.memory_entry),
dev_interactions: records
.filter(r => r.dev_id)
.reduce((acc, r) => {
if (!acc[r.dev_id]) acc[r.dev_id] = { dev_id: r.dev_id, interactions: 0, last_active: null };
acc[r.dev_id].interactions++;
acc[r.dev_id].last_active = r.timestamp;
return acc;
}, {}),
collaboration_sessions: records
.filter(r => r.type === 'collaboration'),
syslog_collaborations: records
.filter(r => r.type === 'syslog_collaboration'),
};
}
// ══════════════════════════════════════════════════════════
// 工具函数
// ══════════════════════════════════════════════════════════
function generateId() {
return 'CL-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6);
}
function calculateImportance(syslog) {
let score = 5; // 基础分
if (syslog.status === 'completed') score += 2;
if (syslog.human_feedback) score += 1;
if (syslog.collaboration) score += 1;
return Math.min(score, 10);
}
/**
* 获取采集统计
* @returns {object}
*/
function getStats() {
const pending = buffer.length;
let totalFiles = 0;
try {
if (fs.existsSync(LOGS_DIR)) {
totalFiles = fs.readdirSync(LOGS_DIR)
.filter(f => f.startsWith('collab-') && f.endsWith('.jsonl'))
.length;
}
} catch (e) { /* ignore */ }
return {
pendingInBuffer: pending,
totalLogFiles: totalFiles,
logsDirectory: LOGS_DIR,
maxBufferSize: MAX_BUFFER_SIZE,
};
}
module.exports = {
logInteraction,
logSyslogCollaboration,
flush,
exportForBrainDB,
getStats,
};