autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
134 lines (133 loc) • 4.88 kB
JavaScript
/**
* ReportStore — 报告持久化服务
*
* 管道产物(governance / compliance / metrics / analysis)写入 JSONL,
* 供 API 查询历史报告。
*
* @module infrastructure/report/ReportStore
*/
import { randomBytes } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
// ── Constants ───────────────────────────────────────
const VALID_CATEGORIES = new Set([
'governance',
'compliance',
'metrics',
'analysis',
]);
// ── ReportStore ─────────────────────────────────────
export class ReportStore {
#baseDir;
constructor(baseDir) {
this.#baseDir = baseDir;
}
/** 写入一条报告(追加 JSONL) */
async write(entry) {
const id = ReportStore.#generateId(entry.timestamp);
const full = { id, ...entry };
const filePath = this.#resolveFile(entry.category, entry.timestamp);
const dir = path.dirname(filePath);
fs.mkdirSync(dir, { recursive: true });
fs.appendFileSync(filePath, `${JSON.stringify(full)}\n`, 'utf8');
return full;
}
/** 查询报告列表 */
async query(opts = {}) {
const categories = opts.category?.length
? opts.category.filter((c) => VALID_CATEGORIES.has(c))
: [...VALID_CATEGORIES];
const all = [];
for (const cat of categories) {
const catDir = path.join(this.#baseDir, cat);
if (!fs.existsSync(catDir)) {
continue;
}
const files = fs
.readdirSync(catDir)
.filter((f) => f.endsWith('.jsonl'))
.sort()
.reverse();
for (const file of files) {
const entries = this.#readJsonl(path.join(catDir, file));
all.push(...entries);
}
}
// 过滤
let filtered = all;
if (opts.type) {
filtered = filtered.filter((e) => e.type === opts.type);
}
if (opts.from) {
filtered = filtered.filter((e) => e.timestamp >= opts.from);
}
if (opts.to) {
filtered = filtered.filter((e) => e.timestamp <= opts.to);
}
// 按时间倒序
filtered.sort((a, b) => b.timestamp - a.timestamp);
const total = filtered.length;
const offset = opts.offset ?? 0;
const limit = opts.limit ?? 20;
const reports = filtered.slice(offset, offset + limit);
return { reports, total };
}
/** 分类统计 */
async stats(opts = {}) {
const result = {};
for (const cat of VALID_CATEGORIES) {
const catDir = path.join(this.#baseDir, cat);
if (!fs.existsSync(catDir)) {
result[cat] = 0;
continue;
}
const files = fs.readdirSync(catDir).filter((f) => f.endsWith('.jsonl'));
let count = 0;
for (const file of files) {
const entries = this.#readJsonl(path.join(catDir, file));
for (const e of entries) {
if (opts.from && e.timestamp < opts.from) {
continue;
}
if (opts.to && e.timestamp > opts.to) {
continue;
}
count++;
}
}
result[cat] = count;
}
return result;
}
// ── Private ───────────────────────────────────────
#resolveFile(category, timestamp) {
const d = new Date(timestamp);
const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
return path.join(this.#baseDir, category, `${dateStr}.jsonl`);
}
#readJsonl(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n').filter((l) => l.trim());
const entries = [];
for (const line of lines) {
try {
entries.push(JSON.parse(line));
}
catch {
// 跳过损坏行
}
}
return entries;
}
catch {
return [];
}
}
static #generateId(timestamp) {
const d = new Date(timestamp);
const dateStr = `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
const rand = randomBytes(4).toString('hex');
return `rpt-${dateStr}-${rand}`;
}
}