bookworm-smart-assistant/scripts/session-trace.js

112 lines
3.4 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* Hook 会话追踪 (Phase 3)
*
* 提供共享 traceId使 3 hooks 的事件可以关联分析
* 所有消费者必须 try/catch fail-open
*/
const fs = require('fs');
const { safeAppendJsonl } = require('../hooks/lib/safe-append.js');
const path = require('path');
// ─── 路径解析 ────────────────────────────────────────
let debugDir;
try {
const { PATHS } = require('./paths.config.js');
debugDir = PATHS.debugDir;
} catch {
debugDir = path.resolve(__dirname, '..', 'debug');
}
const SESSION_TRACE_FILE = path.join(debugDir, 'session-trace.json');
const SESSION_LOCK_FILE = path.join(debugDir, 'session-active.lock');
/**
* 获取当前会话的 traceId session 复用
* @returns {{ sessionId: string, traceId: string, startedAt: string, eventCount: number }}
*/
function getSessionTrace() {
if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
// 读取当前 sessionId
let sessionId = 'unknown';
try {
if (fs.existsSync(SESSION_LOCK_FILE)) {
const lock = JSON.parse(fs.readFileSync(SESSION_LOCK_FILE, 'utf8'));
sessionId = lock.sessionId || lock.id || 'unknown';
}
} catch {}
// 读取或创建 trace 状态
let trace = null;
try {
if (fs.existsSync(SESSION_TRACE_FILE)) {
trace = JSON.parse(fs.readFileSync(SESSION_TRACE_FILE, 'utf8'));
}
} catch {}
// 同 session 复用 traceId否则创建新的
if (trace && trace.sessionId === sessionId && sessionId !== 'unknown') {
return trace;
}
// 新 trace
const traceId = `t-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
const newTrace = {
sessionId,
traceId,
startedAt: new Date().toISOString(),
eventCount: 0,
};
try {
// W5: 原子写入 (temp+rename)
var tmp = SESSION_TRACE_FILE + '.tmp.' + process.pid;
fs.writeFileSync(tmp, JSON.stringify(newTrace, null, 2) + '\n');
fs.renameSync(tmp, SESSION_TRACE_FILE);
} catch {}
return newTrace;
}
/**
* 追加事件到日期分片的 trace 日志
* @param {string} hookName - 来源 hook 名称
* @param {string} eventType - 事件类型 (detection|block-warn|pass|outcome)
* @param {Object} data - 附加数据
*/
function appendTraceEvent(hookName, eventType, data) {
try {
if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
const trace = getSessionTrace();
const dateStr = new Date().toISOString().slice(0, 10);
const logFile = path.join(debugDir, `trace-${dateStr}.jsonl`);
const event = {
ts: new Date().toISOString(),
traceId: trace.traceId,
sessionId: trace.sessionId,
hook: hookName,
event: eventType,
data: data || {},
};
safeAppendJsonl(logFile, event);
// 更新 eventCount (原子写入)
trace.eventCount = (trace.eventCount || 0) + 1;
try {
var tmp2 = SESSION_TRACE_FILE + '.tmp.' + process.pid;
fs.writeFileSync(tmp2, JSON.stringify(trace, null, 2) + '\n');
fs.renameSync(tmp2, SESSION_TRACE_FILE);
} catch {}
} catch {}
}
// ─── 导出 ─────────────────────────────────────────────
if (typeof module !== 'undefined') {
module.exports = { getSessionTrace, appendTraceEvent };
}