feat: add project usage analytics with ECharts

This commit is contained in:
shiyue
2026-08-04 00:49:52 +08:00
parent 58d5f816c2
commit c61b7434d2
39 changed files with 5040 additions and 283 deletions

View File

@@ -10,6 +10,7 @@ const DEFAULT_MAX_RANGE_DAYS = 370;
const DEFAULT_RECENT_LIMIT = 50;
const DEFAULT_MCP_TOOL_LIMIT = 200;
const DEFAULT_SKILL_LIMIT = 100;
const DEFAULT_PROJECT_LIMIT = 100;
const DEFAULT_MCP_DETAIL_LIMIT = 250;
const BUILD_BATCH_SIZE = 1;
const UPSERT_DEBOUNCE_MS = 300;
@@ -292,6 +293,7 @@ function createTrendBucket(date) {
mcpCalls: 0,
mcpFailures: 0,
skillMentions: 0,
projects: 0,
};
}
@@ -316,9 +318,11 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
const recentLimit = clampInteger(params.recentLimit, DEFAULT_RECENT_LIMIT, 1, 100);
const mcpToolLimit = clampInteger(params.mcpToolLimit, DEFAULT_MCP_TOOL_LIMIT, 1, 500);
const skillLimit = clampInteger(params.skillLimit, DEFAULT_SKILL_LIMIT, 1, 500);
const projectLimit = clampInteger(params.projectLimit, DEFAULT_PROJECT_LIMIT, 1, 500);
const mcpDetailLimit = clampInteger(params.mcpDetailLimit, DEFAULT_MCP_DETAIL_LIMIT, 1, 500);
const dayKey = createDayKeyFormatter(range.timeZone);
const trendMap = createTrendBuckets(range, dayKey);
const trendProjectKeys = new Map(Array.from(trendMap.keys(), (key) => [key, new Set()]));
const overview = {
newSessions: 0,
messages: 0,
@@ -327,10 +331,14 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
mcpCalls: 0,
mcpFailures: 0,
skillMentions: 0,
projects: 0,
projectSessions: 0,
unassignedProjectSessions: 0,
};
const mcpStatus = { completed: 0, failed: 0, other: 0 };
const mcpTools = new Map();
const skills = new Map();
const projects = new Map();
const mcpRecentCalls = [];
const recentSessions = [];
let retainedMessages = 0;
@@ -338,12 +346,15 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
for (const doc of documents || []) {
if (!doc || !doc.sessionId) continue;
retainedMessages += Math.max(0, Number(doc.sourceMessageCount || 0));
const projectCwd = cleanDisplayText(String(doc.cwd || ''), 1200).replace(/\\/g, '/').replace(/\/+$/, '');
const projectName = cleanDisplayText(String(doc.projectName || basenameFromCwd(projectCwd)), 240);
const projectKey = projectName ? (projectCwd ? `cwd:${projectCwd}` : `name:${projectName}`) : '';
const sessionSummary = {
sessionId: doc.sessionId,
title: doc.title || '未命名会话',
agent: doc.agent || 'codex',
cwd: doc.cwd || '',
projectName: doc.projectName || '',
projectName,
created: doc.created || null,
lastActivity: null,
newSession: false,
@@ -361,7 +372,10 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
sessionSummary.newSession = true;
sessionSummary.lastActivity = new Date(createdMs).toISOString();
const bucket = trendMap.get(dayKey(createdMs));
if (bucket) bucket.newSessions += 1;
if (bucket) {
bucket.newSessions += 1;
if (projectKey) trendProjectKeys.get(bucket.date)?.add(projectKey);
}
}
for (const event of Array.isArray(doc.events) ? doc.events : []) {
@@ -373,6 +387,7 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
}
const bucket = trendMap.get(dayKey(timestampMs));
if (!bucket) continue;
if (projectKey) trendProjectKeys.get(bucket.date)?.add(projectKey);
if (event.type === 'message') {
overview.messages += 1;
@@ -448,7 +463,34 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
}
}
if (sessionSummary.lastActivity) recentSessions.push(sessionSummary);
if (sessionSummary.lastActivity) {
if (projectKey) {
const current = projects.get(projectKey) || {
key: projectKey,
name: projectName,
cwd: projectCwd,
sessions: 0,
messages: 0,
mcpCalls: 0,
lastActiveAt: null,
};
current.sessions += 1;
current.messages += sessionSummary.messages;
current.mcpCalls += sessionSummary.mcpCalls;
if (!current.lastActiveAt || sessionSummary.lastActivity > current.lastActiveAt) {
current.lastActiveAt = sessionSummary.lastActivity;
}
projects.set(projectKey, current);
overview.projectSessions += 1;
} else {
overview.unassignedProjectSessions += 1;
}
recentSessions.push(sessionSummary);
}
}
for (const bucket of trendMap.values()) {
bucket.projects = trendProjectKeys.get(bucket.date)?.size || 0;
}
const mcpToolRows = Array.from(mcpTools.values());
@@ -460,6 +502,12 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
|| a.server.localeCompare(b.server)
|| a.tool.localeCompare(b.tool));
const skillRows = Array.from(skills.values()).sort((a, b) => b.uses - a.uses || a.name.localeCompare(b.name));
const projectRows = Array.from(projects.values()).sort((a, b) => b.messages - a.messages
|| b.mcpCalls - a.mcpCalls
|| b.sessions - a.sessions
|| a.name.localeCompare(b.name)
|| a.key.localeCompare(b.key));
overview.projects = projectRows.length;
recentSessions.sort((a, b) => String(b.lastActivity).localeCompare(String(a.lastActivity)) || a.sessionId.localeCompare(b.sessionId));
mcpRecentCalls.sort((a, b) => b.timestamp.localeCompare(a.timestamp)
|| a.server.localeCompare(b.server)
@@ -479,6 +527,7 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
row.detailCallsReturned = returnedDetailCounts.get(`${row.server}\u0000${row.tool}`) || 0;
}
const returnedSkills = skillRows.slice(0, skillLimit);
const returnedProjects = projectRows.slice(0, projectLimit);
return {
schemaVersion: SCHEMA_VERSION,
@@ -501,6 +550,8 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
returnedMcpTools: returnedMcpTools.length,
distinctSkills: skillRows.length,
returnedSkills: returnedSkills.length,
distinctProjects: projectRows.length,
returnedProjects: returnedProjects.length,
lastBuiltAt: meta.lastBuiltAt || null,
mcpTimestampBasis: 'assistant_message',
skillBasis: 'explicit_composer_mention',
@@ -511,12 +562,14 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
limits: {
mcpTools: mcpToolLimit,
skills: skillLimit,
projects: projectLimit,
mcpRecentCalls: mcpDetailLimit,
recentSessions: recentLimit,
},
mcpTools: returnedMcpTools,
mcpRecentCalls: returnedMcpRecentCalls,
skills: returnedSkills,
projects: returnedProjects,
recentSessions: recentSessions.slice(0, recentLimit),
};
}