lumencode 1.3.3 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,6 +66,7 @@ npx lumencode serve
66
66
  | 🎯 **行级 AI 归因** | 通过 hook 步骤追踪,精确识别每一行代码的 AI 参与度。不是"这个提交 AI 帮忙了",而是"这行代码是 AI 写的" |
67
67
  | 🌐 **三工具统一** | Claude Code / Codex / OpenCode 数据全自动汇总,左侧标签一键切换 |
68
68
  | 📝 **自然语言工作汇报** | 详报/简报一键生成,支持标准 Markdown / 飞书 / 钉钉三种格式,每个板块附诊断解读 |
69
+ | 🤖 **智能报告生成** | 连接本地 OpenCode CLI,对受限统计上下文和原始工作汇报做 AI 分析,支持默认风格与面向领导汇报的「牛马」风格 |
69
70
  | 📂 **按项目独立汇报** | 右侧面板选择项目,生成该项目的独立工作汇报(commits + AI 交互量 + 热点文件) |
70
71
  | 📅 **自定义时间范围** | 除日/周/月外,支持选择任意起止日期,方便对齐 Sprint 周期 |
71
72
  | 💰 **精确费用估算** | 600+ 模型本地定价(含 GLM/Kimi/Qwen/DeepSeek)+ Portkey API 兜底,未知模型不计费而非乱算 |
@@ -119,6 +120,9 @@ npx lumencode serve
119
120
 
120
121
  - **详报** —— 完整数据 + 洞察解读 + 板块编号,适合周报、月报
121
122
  - **简报** —— 3-5 句话核心摘要,适合日报或群消息
123
+ - **智能报告** —— 页面内调用本地 OpenCode CLI 生成 AI 分析报告,补充数据摘要、工作亮点分析、关键洞察、风险与建议
124
+ - **风格选择** —— 生成前可选择默认风格,或「牛马」风格输出更适合向领导汇报的表达倾向
125
+ - **持久化与更新提醒** —— 智能报告会按周期、项目、报告层级和风格保存;统计数据变化后提示重新生成
122
126
  - **多平台格式** —— 标准 Markdown / 飞书 / 钉钉,一键切换
123
127
  - **按项目生成** —— 右侧面板选择项目,生成该项目的独立汇报
124
128
 
@@ -250,6 +254,14 @@ node index.js hooks disable
250
254
 
251
255
  ## 更新日志
252
256
 
257
+ ### v1.3.4 (2026-06-05) — 智能报告风格与工作亮点分析
258
+
259
+ - **智能报告风格选择** — 生成智能报告前弹窗选择「默认风格」或「牛马」风格,后者面向领导汇报口径,突出投入、产出、风险兜底和下一步计划
260
+ - **工作亮点分析** — AI 智能报告新增「工作亮点分析」章节要求,让报告不止复述统计数据,而是提炼可汇报的亮点与依据
261
+ - **Boss 汇报迁移** — 移除普通工作汇报里的「汇报 Boss」层级,将其能力迁移为智能报告的一种风格,减少普通报告入口复杂度
262
+ - **报告持久化增强** — 智能报告按风格独立保存,默认风格兼容旧记录,刷新或切换页面后不会丢失
263
+ - **后台生成体验** — 智能报告改为后台任务生成,支持刷新后恢复进度,并用渐进进度条降低长时间等待的不确定感
264
+
253
265
  ### v1.3.0 (2026-05-28) — 行级 AI 归因 & 交互式 Hooks 管理
254
266
 
255
267
  - **行级 AI 归因** — 通过 hook 步骤追踪系统,将归因粒度从提交级细化到行级,精确识别每一行代码的 AI 参与度
package/lib/server.js CHANGED
@@ -1,7 +1,8 @@
1
- import { createServer } from 'http';
2
- import { readFileSync, existsSync } from 'fs';
3
- import { join, extname, resolve, sep } from 'path';
4
- import { fileURLToPath } from 'url';
1
+ import { createServer } from 'http';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { join, extname, resolve, sep } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { randomUUID } from 'crypto';
5
6
  import { saveConfig } from './config.js';
6
7
  import { parseRepoPaths } from './path-utils.js';
7
8
  import { generateWorkReport, generateFeishuCard, generateBossReport } from './report.js';
@@ -14,7 +15,9 @@ import { identifyBillingBlocks } from './blocks.js';
14
15
  import { detectAvailableTools, parseAllEnabledTools } from './parsers/index.js';
15
16
  import { isAssistantRecord, getInputTokens, getOutputTokens } from './record-utils.js';
16
17
  import { StepTracker } from './step-tracker.js';
17
- import { disableHooks, enableHooks, getHooksStatus, HOOK_TOOLS, initStepTracking } from './hooks-manager.js';
18
+ import { disableHooks, enableHooks, getHooksStatus, HOOK_TOOLS, initStepTracking } from './hooks-manager.js';
19
+ import { SMART_REPORT_PROMPT_MARKER, buildSmartReportContext, createSmartReport, detectSmartReportAgents } from './smart-report.js';
20
+ import { buildSmartReportKey, buildSourceHash, getSmartReportStoreDir, readSmartReportRecord, saveSmartReportRecord } from './smart-report-store.js';
18
21
 
19
22
  // basename 提取,兼容不同路径格式
20
23
  function getProjectBaseName(p) {
@@ -87,9 +90,11 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
87
90
  const PARSED_CACHE_TTL = 30_000; // 30s
88
91
 
89
92
  // ── 查询结果缓存(按查询条件缓存 buildReportData 结果) ──
90
- const _reportCache = new Map();
91
- const REPORT_CACHE_TTL = 30_000; // 30s
92
- const REPORT_CACHE_MAX_SIZE = 50;
93
+ const _reportCache = new Map();
94
+ const REPORT_CACHE_TTL = 30_000; // 30s
95
+ const REPORT_CACHE_MAX_SIZE = 50;
96
+ const smartReportJobs = new Map();
97
+ const SMART_REPORT_JOB_KEEP_MS = 30 * 60_000;
93
98
 
94
99
  function getReportCacheKey(period, date, tool, customStart, customEnd) {
95
100
  return `${period}|${date}|${tool || 'all'}|${customStart || ''}|${customEnd || ''}`;
@@ -115,13 +120,38 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
115
120
  _reportCache.clear();
116
121
  }
117
122
 
118
- function writeJson(res, statusCode, data) {
123
+ function writeJson(res, statusCode, data) {
119
124
  res.writeHead(statusCode, {
120
125
  'Content-Type': 'application/json',
121
126
  'Access-Control-Allow-Origin': 'http://localhost:' + PORT,
122
127
  });
123
- res.end(JSON.stringify(data));
124
- }
128
+ res.end(JSON.stringify(data));
129
+ }
130
+
131
+ function publicSmartReportJob(job) {
132
+ if (!job) return null;
133
+ return {
134
+ id: job.id,
135
+ reportKey: job.reportKey,
136
+ status: job.status,
137
+ error: job.error || '',
138
+ startedAt: job.startedAt,
139
+ updatedAt: job.updatedAt,
140
+ completedAt: job.completedAt || '',
141
+ };
142
+ }
143
+
144
+ function rememberSmartReportJob(job) {
145
+ smartReportJobs.set(job.reportKey, job);
146
+ return job;
147
+ }
148
+
149
+ function forgetSmartReportJobLater(reportKey) {
150
+ setTimeout(() => {
151
+ const job = smartReportJobs.get(reportKey);
152
+ if (job && job.status !== 'running') smartReportJobs.delete(reportKey);
153
+ }, SMART_REPORT_JOB_KEEP_MS).unref?.();
154
+ }
125
155
 
126
156
  function parseHookTools(value) {
127
157
  if (!value) return [HOOK_TOOLS.CLAUDE, HOOK_TOOLS.CODEX, HOOK_TOOLS.OPENCODE];
@@ -181,7 +211,7 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
181
211
  return result;
182
212
  }
183
213
 
184
- function deriveProjectGitStats(gitStats, project, start, end, attributionOptions) {
214
+ function deriveProjectGitStats(gitStats, project, start, end, attributionOptions) {
185
215
  if (!gitStats?.commitList?.length || !project) return null;
186
216
  const windowEnd = end + 'T23:59:59';
187
217
  const commitList = gitStats.commitList.filter(c => {
@@ -199,10 +229,178 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
199
229
  fileHotspots: computeFileHotspots(commitList, 10),
200
230
  aiContribution: computeAIContribution(commitList, null, attributionOptions),
201
231
  attributionSummary: gitStats.attributionSummary,
202
- };
203
- }
204
-
205
- function prepareJsonReportData(baseData, tool) {
232
+ };
233
+ }
234
+
235
+ function parseSmartReportParams(input = {}) {
236
+ return {
237
+ agent: input.agent || '',
238
+ period: input.period || 'daily',
239
+ date: input.date || new Date().toISOString().slice(0, 10),
240
+ start: input.start || '',
241
+ end: input.end || '',
242
+ tool: input.tool || 'all',
243
+ project: input.project || '',
244
+ level: input.level || 'detailed',
245
+ style: input.style || 'default',
246
+ platform: input.platform || 'default',
247
+ };
248
+ }
249
+
250
+ function validateSmartReportParams(params) {
251
+ const validPeriods = ['daily', 'weekly', 'monthly', 'custom'];
252
+ if (!validPeriods.includes(params.period)) {
253
+ return `无效的 period 参数,可选值:${validPeriods.join('/')}`;
254
+ }
255
+ if (params.period === 'custom') {
256
+ if (!params.start || !params.end || !/^\d{4}-\d{2}-\d{2}$/.test(params.start) || !/^\d{4}-\d{2}-\d{2}$/.test(params.end)) {
257
+ return '自定义周期需要 start 和 end 参数 (YYYY-MM-DD)';
258
+ }
259
+ if (params.start > params.end) return '起始日期不能晚于结束日期';
260
+ }
261
+ const validStyles = ['default', 'workhorse'];
262
+ if (!validStyles.includes(params.style)) {
263
+ return `无效的 style 参数,可选值:${validStyles.join('/')}`;
264
+ }
265
+ return '';
266
+ }
267
+
268
+ function buildSmartReportRecordIdentity(params, data) {
269
+ return {
270
+ agent: params.agent,
271
+ period: params.period,
272
+ date: params.period === 'daily' ? data.start : '',
273
+ start: data.start,
274
+ end: data.end,
275
+ tool: params.tool,
276
+ project: params.project,
277
+ level: params.level,
278
+ style: params.style,
279
+ platform: params.platform,
280
+ };
281
+ }
282
+
283
+ function isSmartReportInternalRecord(record) {
284
+ const text = String(record?.metadata?.text || record?.text || '');
285
+ return text.includes(SMART_REPORT_PROMPT_MARKER)
286
+ || text.includes('LumenCode 的智能报告分析器');
287
+ }
288
+
289
+ function filterInternalSmartReportParsed(parsed) {
290
+ if (!parsed?.records?.length) return parsed;
291
+ const internalSessions = new Set(
292
+ parsed.records
293
+ .filter(isSmartReportInternalRecord)
294
+ .map(r => r.sessionId)
295
+ .filter(Boolean)
296
+ );
297
+ if (internalSessions.size === 0) return parsed;
298
+
299
+ const records = parsed.records.filter(r => !internalSessions.has(r.sessionId));
300
+ const toolBreakdown = {};
301
+ for (const [tool, base] of Object.entries(parsed.toolBreakdown || {})) {
302
+ toolBreakdown[tool] = { recordCount: 0, sessionCount: 0, error: base.error };
303
+ }
304
+ const groups = {};
305
+ for (const record of records) {
306
+ const tool = record.tool || 'claude';
307
+ if (!groups[tool]) groups[tool] = { recordCount: 0, sessions: new Set() };
308
+ groups[tool].recordCount++;
309
+ if (record.sessionId) groups[tool].sessions.add(record.sessionId);
310
+ }
311
+ for (const [tool, group] of Object.entries(groups)) {
312
+ toolBreakdown[tool] = {
313
+ recordCount: group.recordCount,
314
+ sessionCount: group.sessions.size,
315
+ };
316
+ }
317
+ return { ...parsed, records, toolBreakdown };
318
+ }
319
+
320
+ function sourceReportHashesMatch(record, source) {
321
+ const saved = record?.sourceReports || {};
322
+ const current = source?.sourceHashes || {};
323
+ if (!saved.detailedHash && !saved.briefHash && !saved.bossHash) return false;
324
+ return ['detailedHash', 'briefHash', 'bossHash'].every(key => (saved[key] || '') === (current[key] || ''));
325
+ }
326
+
327
+ function smartReportNeedsUpdate(record, source) {
328
+ if (!record?.sourceHash) return false;
329
+ if (record.sourceHash === source.sourceHash) return false;
330
+ if ((record.sourceHashVersion || 1) < 2 && sourceReportHashesMatch(record, source)) return false;
331
+ return true;
332
+ }
333
+
334
+ async function buildSmartReportSource(params) {
335
+ const includeProjects = computeIncludeProjects(config);
336
+ const parsed = filterInternalSmartReportParsed(await getOrParse(config, includeProjects));
337
+ const data = await buildReportData(params.period, params.date, config, includeProjects, params.tool, parsed, {
338
+ customStart: params.start,
339
+ customEnd: params.end,
340
+ });
341
+ if (!data) return null;
342
+
343
+ let reportData = data;
344
+ let workUsageStats = data.usageStats;
345
+ let workGitStats = data.gitStats;
346
+ let workPrevStats = data.prevStats;
347
+ let projectName = '';
348
+
349
+ if (params.project) {
350
+ const { records: allRecords } = parsed;
351
+ const toolRecords = params.tool !== 'all' ? allRecords.filter(r => r.tool === params.tool) : allRecords;
352
+ const projRecords = toolRecords.filter(r => getProjectBaseName(r.project) === params.project);
353
+ const { filtered: projFiltered, start: pStart, end: pEnd } = filterRecordsByPeriod(projRecords, params.period, params.date, { customStart: params.start, customEnd: params.end });
354
+ workUsageStats = projFiltered.length > 0 ? computeUsageStats(projFiltered, config.scenarioKeywords, config.costMode) : { requestCount: 0, projects: {} };
355
+ workPrevStats = null;
356
+ workGitStats = deriveProjectGitStats(data.gitStats, params.project, pStart, pEnd, config.aiAttribution);
357
+ projectName = params.project;
358
+ reportData = {
359
+ ...data,
360
+ usageStats: workUsageStats,
361
+ gitStats: workGitStats,
362
+ prevStats: workPrevStats,
363
+ };
364
+ }
365
+
366
+ const detailedMarkdown = generateWorkReport(workUsageStats, workGitStats, params.period, data.start, data.end, workPrevStats, { level: 'detailed', platform: params.platform, tool: params.tool, projectName });
367
+ const briefMarkdown = generateWorkReport(workUsageStats, workGitStats, params.period, data.start, data.end, workPrevStats, { level: 'brief', platform: params.platform, tool: params.tool, projectName });
368
+ const bossMarkdown = generateBossReport(workUsageStats, workGitStats, params.period, data.start, data.end, workPrevStats, params.platform);
369
+ const workMarkdown = params.level === 'brief' ? briefMarkdown : detailedMarkdown;
370
+ const includeBossSource = params.style === 'workhorse';
371
+ const sourceReports = {
372
+ detailedMarkdown,
373
+ briefMarkdown,
374
+ bossMarkdown: includeBossSource ? bossMarkdown : '',
375
+ };
376
+ const sourceHashes = {
377
+ detailedHash: buildSourceHash(detailedMarkdown),
378
+ briefHash: buildSourceHash(briefMarkdown),
379
+ bossHash: includeBossSource ? buildSourceHash(bossMarkdown) : '',
380
+ };
381
+ const sourceHash = buildSourceHash(buildSmartReportContext(reportData, workMarkdown, {
382
+ period: params.period,
383
+ date: params.date,
384
+ tool: params.tool,
385
+ project: params.project,
386
+ level: params.level,
387
+ style: params.style,
388
+ platform: params.platform,
389
+ sourceReports,
390
+ }));
391
+
392
+ return {
393
+ data,
394
+ reportData,
395
+ workMarkdown,
396
+ sourceReports,
397
+ sourceHashes,
398
+ sourceHash,
399
+ identity: buildSmartReportRecordIdentity(params, data),
400
+ };
401
+ }
402
+
403
+ function prepareJsonReportData(baseData, tool) {
206
404
  const data = { ...baseData };
207
405
 
208
406
  if (tool !== 'all' && data.gitStats?.aiContributionByTool) {
@@ -251,8 +449,146 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
251
449
  res.setHeader('X-XSS-Protection', '1; mode=block');
252
450
  res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
253
451
 
254
- // API endpoint
255
- if (url.pathname === '/api/hooks') {
452
+ // API endpoint
453
+ if (url.pathname === '/api/smart-report/tools') {
454
+ try {
455
+ const tools = await detectSmartReportAgents();
456
+ writeJson(res, 200, { tools });
457
+ } catch (err) {
458
+ console.error('API error:', err.message);
459
+ writeJson(res, 500, { error: err.message || '服务端内部错误' });
460
+ }
461
+ return;
462
+ }
463
+
464
+ if (url.pathname === '/api/smart-report') {
465
+ if (req.method === 'GET') {
466
+ const params = parseSmartReportParams({
467
+ agent: url.searchParams.get('agent') || '',
468
+ period: url.searchParams.get('period') || 'daily',
469
+ date: url.searchParams.get('date') || '',
470
+ start: url.searchParams.get('start') || '',
471
+ end: url.searchParams.get('end') || '',
472
+ tool: url.searchParams.get('tool') || 'all',
473
+ project: url.searchParams.get('project') || '',
474
+ level: url.searchParams.get('level') || 'detailed',
475
+ style: url.searchParams.get('style') || 'default',
476
+ platform: url.searchParams.get('platform') || 'default',
477
+ });
478
+ const validationError = validateSmartReportParams(params);
479
+ if (validationError) {
480
+ writeJson(res, 400, { error: validationError });
481
+ return;
482
+ }
483
+
484
+ const source = await buildSmartReportSource(params);
485
+ if (!source) {
486
+ writeJson(res, 200, { record: null, needsUpdate: false });
487
+ return;
488
+ }
489
+
490
+ const storeDir = getSmartReportStoreDir(configPath);
491
+ let record = readSmartReportRecord(storeDir, buildSmartReportKey(source.identity));
492
+ if (!record) {
493
+ record = readSmartReportRecord(storeDir, buildSmartReportKey(params));
494
+ }
495
+ const reportKey = buildSmartReportKey(source.identity);
496
+ const job = smartReportJobs.get(reportKey) || null;
497
+ const needsUpdate = smartReportNeedsUpdate(record, source);
498
+ writeJson(res, 200, {
499
+ record,
500
+ job: publicSmartReportJob(job),
501
+ needsUpdate,
502
+ currentSourceHash: source.sourceHash,
503
+ range: { start: source.data.start, end: source.data.end },
504
+ });
505
+ return;
506
+ }
507
+
508
+ if (req.method !== 'POST') {
509
+ writeJson(res, 405, { error: 'Method not allowed' });
510
+ return;
511
+ }
512
+
513
+ readJsonBody(req, res, async (body) => {
514
+ const params = parseSmartReportParams(body);
515
+ const validationError = validateSmartReportParams(params);
516
+ if (validationError) {
517
+ writeJson(res, 400, { error: validationError });
518
+ return;
519
+ }
520
+
521
+ const source = await buildSmartReportSource(params);
522
+ if (!source) {
523
+ writeJson(res, 404, { error: '未找到可用于智能报告的数据' });
524
+ return;
525
+ }
526
+
527
+ const storeDir = getSmartReportStoreDir(configPath);
528
+ const reportKey = buildSmartReportKey(source.identity);
529
+ const existingJob = smartReportJobs.get(reportKey);
530
+ if (existingJob?.status === 'running') {
531
+ const record = readSmartReportRecord(storeDir, reportKey);
532
+ writeJson(res, 202, {
533
+ record,
534
+ job: publicSmartReportJob(existingJob),
535
+ needsUpdate: smartReportNeedsUpdate(record, source),
536
+ currentSourceHash: source.sourceHash,
537
+ });
538
+ return;
539
+ }
540
+
541
+ const job = rememberSmartReportJob({
542
+ id: randomUUID(),
543
+ reportKey,
544
+ status: 'running',
545
+ error: '',
546
+ startedAt: new Date().toISOString(),
547
+ updatedAt: new Date().toISOString(),
548
+ completedAt: '',
549
+ });
550
+
551
+ Promise.resolve().then(async () => {
552
+ try {
553
+ const markdown = await createSmartReport({
554
+ agent: params.agent,
555
+ reportData: source.reportData,
556
+ workMarkdown: source.workMarkdown,
557
+ options: { period: params.period, date: params.date, tool: params.tool, project: params.project, level: params.level, style: params.style, platform: params.platform, sourceReports: source.sourceReports },
558
+ requireAvailable: true,
559
+ });
560
+ const record = saveSmartReportRecord(storeDir, {
561
+ ...source.identity,
562
+ markdown,
563
+ sourceHash: source.sourceHash,
564
+ sourceHashVersion: 2,
565
+ sourceReports: source.sourceHashes,
566
+ });
567
+ job.status = 'completed';
568
+ job.record = record;
569
+ job.completedAt = new Date().toISOString();
570
+ job.updatedAt = job.completedAt;
571
+ } catch (err) {
572
+ job.status = 'failed';
573
+ job.error = err.message || '智能报告生成失败';
574
+ job.updatedAt = new Date().toISOString();
575
+ } finally {
576
+ forgetSmartReportJobLater(reportKey);
577
+ }
578
+ });
579
+
580
+ const record = readSmartReportRecord(storeDir, reportKey);
581
+ writeJson(res, 202, {
582
+ record,
583
+ job: publicSmartReportJob(job),
584
+ needsUpdate: smartReportNeedsUpdate(record, source),
585
+ currentSourceHash: source.sourceHash,
586
+ });
587
+ });
588
+ return;
589
+ }
590
+
591
+ if (url.pathname === '/api/hooks') {
256
592
  try {
257
593
  if (req.method === 'GET') {
258
594
  writeJson(res, 200, getConfiguredHooksStatus(config));
@@ -402,11 +738,12 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
402
738
  return;
403
739
  }
404
740
 
405
- if (format === 'work') {
406
- const platform = url.searchParams.get('platform') || 'default';
407
- const level = url.searchParams.get('level') || 'detailed';
408
- const feishuCard = url.searchParams.get('feishuCard') === 'true';
409
- const project = url.searchParams.get('project') || '';
741
+ if (format === 'work') {
742
+ const platform = url.searchParams.get('platform') || 'default';
743
+ const requestedLevel = url.searchParams.get('level') || 'detailed';
744
+ const level = requestedLevel === 'brief' ? 'brief' : 'detailed';
745
+ const feishuCard = url.searchParams.get('feishuCard') === 'true';
746
+ const project = url.searchParams.get('project') || '';
410
747
 
411
748
  if (feishuCard) {
412
749
  const card = generateFeishuCard(data.usageStats, data.gitStats, period, data.start, data.end, tool);
@@ -446,12 +783,7 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
446
783
  projGitStats = deriveProjectGitStats(data.gitStats, project, pStart, pEnd, config.aiAttribution);
447
784
  }
448
785
 
449
- let markdown;
450
- if (level === 'boss') {
451
- markdown = generateBossReport(projUsageStats, projGitStats, period, data.start, data.end, projPrevStats, platform);
452
- } else {
453
- markdown = generateWorkReport(projUsageStats, projGitStats, period, data.start, data.end, projPrevStats, { level, platform, tool, projectName });
454
- }
786
+ const markdown = generateWorkReport(projUsageStats, projGitStats, period, data.start, data.end, projPrevStats, { level, platform, tool, projectName });
455
787
  res.writeHead(200, {
456
788
  'Content-Type': 'text/plain; charset=utf-8',
457
789
  'Access-Control-Allow-Origin': 'http://localhost:' + PORT,
@@ -0,0 +1,98 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { createHash } from 'crypto';
4
+
5
+ const STORE_FILE = 'records.json';
6
+
7
+ function stableStringify(value) {
8
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
9
+ if (value && typeof value === 'object') {
10
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
11
+ }
12
+ return JSON.stringify(value);
13
+ }
14
+
15
+ function hashText(text) {
16
+ return createHash('sha256').update(String(text)).digest('hex');
17
+ }
18
+
19
+ export function getSmartReportStoreDir(configPath) {
20
+ return join(dirname(configPath || join(process.cwd(), 'config.json')), 'smart-reports');
21
+ }
22
+
23
+ export function buildSmartReportKey(input = {}) {
24
+ return hashText(stableStringify({
25
+ agent: input.agent || '',
26
+ period: input.period || '',
27
+ date: input.date || '',
28
+ start: input.start || '',
29
+ end: input.end || '',
30
+ tool: input.tool || 'all',
31
+ project: input.project || '',
32
+ level: input.level || 'detailed',
33
+ style: input.style && input.style !== 'default' ? input.style : '',
34
+ platform: input.platform || 'default',
35
+ }));
36
+ }
37
+
38
+ export function buildSourceHash(source = {}) {
39
+ return hashText(stableStringify(source));
40
+ }
41
+
42
+ function getStorePath(storeDir) {
43
+ return join(storeDir, STORE_FILE);
44
+ }
45
+
46
+ function readStore(storeDir) {
47
+ const file = getStorePath(storeDir);
48
+ if (!existsSync(file)) return { records: {} };
49
+ try {
50
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
51
+ if (!parsed || typeof parsed !== 'object' || !parsed.records) return { records: {} };
52
+ return parsed;
53
+ } catch {
54
+ return { records: {} };
55
+ }
56
+ }
57
+
58
+ function writeStore(storeDir, store) {
59
+ if (!existsSync(storeDir)) mkdirSync(storeDir, { recursive: true });
60
+ writeFileSync(getStorePath(storeDir), JSON.stringify(store, null, 2), 'utf8');
61
+ }
62
+
63
+ export function readSmartReportRecord(storeDir, reportKey) {
64
+ const store = readStore(storeDir);
65
+ return store.records[reportKey] || null;
66
+ }
67
+
68
+ export function saveSmartReportRecord(storeDir, input) {
69
+ const store = readStore(storeDir);
70
+ const reportKey = buildSmartReportKey(input);
71
+ const now = new Date().toISOString();
72
+ const existing = store.records[reportKey] || null;
73
+ const record = {
74
+ id: existing?.id || reportKey,
75
+ reportKey,
76
+ agent: input.agent || '',
77
+ period: input.period || '',
78
+ date: input.date || '',
79
+ start: input.start || '',
80
+ end: input.end || '',
81
+ tool: input.tool || 'all',
82
+ project: input.project || '',
83
+ level: input.level || 'detailed',
84
+ style: input.style || 'default',
85
+ platform: input.platform || 'default',
86
+ markdown: input.markdown || '',
87
+ sourceHash: input.sourceHash || '',
88
+ sourceHashVersion: input.sourceHashVersion || 1,
89
+ sourceReports: input.sourceReports || {},
90
+ createdAt: existing?.createdAt || now,
91
+ updatedAt: now,
92
+ generatedCount: (existing?.generatedCount || 0) + 1,
93
+ };
94
+
95
+ store.records[reportKey] = record;
96
+ writeStore(storeDir, store);
97
+ return record;
98
+ }