guanghulab/backend/feishu-bot/collaboration-logger.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

257 lines
8.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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