UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

134 lines (133 loc) 4.88 kB
/** * 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}`; } }