


























@@ -38,6 +38,7 @@ import type {
3838SessionLogEntry,
3939SessionMessageCounts,
4040SessionModelUsage,
41+SessionUtcQuarterHourMessageCounts,
4142SessionToolUsage,
4243SessionUsageTimePoint,
4344SessionUsageTimeSeries,
@@ -57,6 +58,7 @@ export type {
5758SessionLogEntry,
5859SessionMessageCounts,
5960SessionModelUsage,
61+SessionUtcQuarterHourMessageCounts,
6062SessionToolUsage,
6163SessionUsageTimePoint,
6264SessionUsageTimeSeries,
@@ -165,6 +167,39 @@ const parseTranscriptEntry = (entry: Record<string, unknown>): ParsedTranscriptE
165167const formatDayKey = (date: Date): string =>
166168date.toLocaleDateString("en-CA", { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone });
167169170+const formatUtcDayKey = (date: Date): string =>
171+`${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
172+173+/**
174+ * Accumulate message-level counts into a bucket (daily or UTC quarter-hour).
175+ * Avoids duplicating the same logic for both daily and quarter-hour message counts.
176+ */
177+const accumulateMessageCounts = (
178+bucket: {
179+total: number;
180+user: number;
181+assistant: number;
182+toolCalls: number;
183+toolResults: number;
184+errors: number;
185+},
186+entry: ParsedTranscriptEntry,
187+errorStopReasons: Set<string>,
188+) => {
189+bucket.total += entry.role === "user" || entry.role === "assistant" ? 1 : 0;
190+if (entry.role === "user") {
191+bucket.user += 1;
192+} else if (entry.role === "assistant") {
193+bucket.assistant += 1;
194+}
195+bucket.toolCalls += entry.toolNames.length;
196+bucket.toolResults += entry.toolResultCounts.total;
197+bucket.errors += entry.toolResultCounts.errors;
198+if (entry.stopReason && errorStopReasons.has(entry.stopReason)) {
199+bucket.errors += 1;
200+}
201+};
202+168203const computeLatencyStats = (values: number[]): SessionLatencyStats | undefined => {
169204if (!values.length) {
170205return undefined;
@@ -572,6 +607,7 @@ export async function loadSessionCostSummary(params: {
572607const activityDatesSet = new Set<string>();
573608const dailyMap = new Map<string, { tokens: number; cost: number }>();
574609const dailyMessageMap = new Map<string, SessionDailyMessageCounts>();
610+const utcQuarterHourMessageMap = new Map<string, SessionUtcQuarterHourMessageCounts>();
575611const dailyLatencyMap = new Map<string, number[]>();
576612const dailyModelUsageMap = new Map<string, SessionDailyModelUsage>();
577613const messageCounts: SessionMessageCounts = {
@@ -669,19 +705,27 @@ export async function loadSessionCostSummary(params: {
669705toolResults: 0,
670706errors: 0,
671707};
672-daily.total += entry.role === "user" || entry.role === "assistant" ? 1 : 0;
673-if (entry.role === "user") {
674-daily.user += 1;
675-} else if (entry.role === "assistant") {
676-daily.assistant += 1;
677-}
678-daily.toolCalls += entry.toolNames.length;
679-daily.toolResults += entry.toolResultCounts.total;
680-daily.errors += entry.toolResultCounts.errors;
681-if (entry.stopReason && errorStopReasons.has(entry.stopReason)) {
682-daily.errors += 1;
683-}
708+accumulateMessageCounts(daily, entry, errorStopReasons);
684709dailyMessageMap.set(dayKey, daily);
710+711+// Per-quarter-hour message counts for precise hourly stats (UTC-based)
712+const quarterIndex = Math.floor(
713+(entry.timestamp.getUTCHours() * 60 + entry.timestamp.getUTCMinutes()) / 15,
714+);
715+const utcDayKey = formatUtcDayKey(entry.timestamp);
716+const quarterKey = `${utcDayKey}::${quarterIndex}`;
717+const utcQuarterHour = utcQuarterHourMessageMap.get(quarterKey) ?? {
718+date: utcDayKey,
719+ quarterIndex,
720+total: 0,
721+user: 0,
722+assistant: 0,
723+toolCalls: 0,
724+toolResults: 0,
725+errors: 0,
726+};
727+accumulateMessageCounts(utcQuarterHour, entry, errorStopReasons);
728+utcQuarterHourMessageMap.set(quarterKey, utcQuarterHour);
685729}
686730687731if (!entry.usage) {
@@ -767,6 +811,10 @@ export async function loadSessionCostSummary(params: {
767811dailyMessageMap.values(),
768812).toSorted((a, b) => a.date.localeCompare(b.date));
769813814+const utcQuarterHourMessageCounts: SessionUtcQuarterHourMessageCounts[] = Array.from(
815+utcQuarterHourMessageMap.values(),
816+).toSorted((a, b) => a.date.localeCompare(b.date) || a.quarterIndex - b.quarterIndex);
817+770818const dailyLatency: SessionDailyLatency[] = Array.from(dailyLatencyMap.entries())
771819.map(([date, values]) => {
772820const stats = computeLatencyStats(values);
@@ -814,6 +862,9 @@ export async function loadSessionCostSummary(params: {
814862activityDates: Array.from(activityDatesSet).toSorted(),
815863 dailyBreakdown,
816864 dailyMessageCounts,
865+utcQuarterHourMessageCounts: utcQuarterHourMessageCounts.length
866+ ? utcQuarterHourMessageCounts
867+ : undefined,
817868dailyLatency: dailyLatency.length ? dailyLatency : undefined,
818869dailyModelUsage: dailyModelUsage.length ? dailyModelUsage : undefined,
819870 messageCounts,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。