timeline-logger.js
2.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* DevLoop · timelineLogger
*
* 全流程统一事件日志(M-13 ProcessTimeline)。append-only NDJSON。
* - 每行一个事件
* - 每条 ≤ 1KB(summary 自动截 280 字符;payload > 4KB 强制 ref 形式)
* - 同步原子写(appendFileSync)
*
* 规格:docs/prd/newfunction/_spec/sdd-control.md · C-15
* docs/prd/newfunction/_spec/sdd-model.md · M-13
*/
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const pathResolver = require('./path-resolver');
const SUMMARY_MAX = 280;
const PAYLOAD_MAX_BYTES = 4096;
function _logPath(pid) { return pathResolver.resolveTimelineLog(pid); }
function _truncate(s, n) {
if (typeof s !== 'string') s = String(s);
if (s.length <= n) return s;
return s.slice(0, n - 1) + '…';
}
/**
* Append 一条事件。同步写。
* @param {string} pid
* @param {object} ev - { source, kind, ref?, summary, payload? }
*/
function append(pid, ev) {
if (!ev || !ev.source || !ev.kind || !ev.summary) {
throw new Error('timelineLogger.append: source/kind/summary required');
}
const event = {
ts: Math.floor(Date.now() / 1000),
source: ev.source,
kind: ev.kind,
ref: ev.ref || null,
summary: _truncate(ev.summary, SUMMARY_MAX),
};
// payload 超大 → 强制改 ref 形式(不写盘外部文件,仅写入 ref 占位)
if (ev.payload !== undefined) {
const json = JSON.stringify(ev.payload);
if (json.length > PAYLOAD_MAX_BYTES) {
event.payload = { oversize: true, original_bytes: json.length };
} else {
event.payload = ev.payload;
}
}
const line = JSON.stringify(event) + '\n';
const p = _logPath(pid);
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.appendFileSync(p, line);
return event;
}
/**
* 读全部 / 过滤事件。
* @param {object} opts - { limit?, since?: unix ts, source?, kind? }
*/
function read(pid, opts = {}) {
const p = _logPath(pid);
if (!fs.existsSync(p)) return [];
const limit = opts.limit || 100;
const since = opts.since;
const filterSource = opts.source;
const filterKind = opts.kind;
const lines = fs.readFileSync(p, 'utf8').split('\n').filter(Boolean);
const events = [];
// 倒序读,遇到 limit 停(避免 load 整个 file)
for (let i = lines.length - 1; i >= 0; i--) {
let ev;
try { ev = JSON.parse(lines[i]); } catch { continue; }
if (since && ev.ts < since) break; // 由于 ts 单调,可以提前 break
if (filterSource && ev.source !== filterSource) continue;
if (filterKind && ev.kind !== filterKind) continue;
events.push(ev);
if (events.length >= limit) break;
}
return events.reverse(); // 输出按时间正序
}
/** 最近 N 条(已按时间正序) */
function tail(pid, n = 10) {
return read(pid, { limit: n });
}
module.exports = { append, read, tail };