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