257 lines
8.1 KiB
JavaScript
257 lines
8.1 KiB
JavaScript
// 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,
|
||
};
|