lumencode 1.3.2 → 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 +12 -0
- package/lib/attribution.js +8 -0
- package/lib/git.js +195 -192
- package/lib/report.js +5 -1
- package/lib/server.js +361 -29
- package/lib/smart-report-store.js +98 -0
- package/lib/smart-report.js +339 -0
- package/package.json +1 -1
- package/public/api.js +25 -0
- package/public/app.js +1483 -1167
- package/public/config.js +4 -0
- package/public/index.html +86 -3
- package/public/style.css +202 -0
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
|
|
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/
|
|
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
|
|
408
|
-
const
|
|
409
|
-
const
|
|
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
|
-
|
|
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
|
+
}
|