@morlay/session-rdb 0.0.21-alpha.3 → 0.0.21-alpha.5
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 +1 -1
- package/dist/artifact.d.mts +1 -1
- package/dist/artifact.mjs +1 -1
- package/dist/{backend-DpdtxYpz.d.mts → backend-a0xDrUFG.d.mts} +58 -9
- package/dist/{branch-5JzX9rUq.mjs → branch-Dlmbkfjj.mjs} +4 -12
- package/dist/deletion.d.mts +1 -1
- package/dist/deletion.mjs +1 -1
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +3 -3
- package/dist/{log-CnYct2Dv.mjs → log-C-rbdFwK.mjs} +32 -4
- package/dist/{sqlite-fpvm5Dzs.mjs → sqlite-C3LHLzYW.mjs} +282 -23
- package/dist/{src-CWTWV7vx.mjs → src-Bn0038Sp.mjs} +269 -165
- package/dist/storage.d.mts +11 -2
- package/dist/storage.mjs +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +11 -11
- package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +1 -1
- package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +1 -1
- package/package.json +2 -2
- package/src/backend.ts +26 -1
- package/src/branch.ts +5 -13
- package/src/entities/v3/event-usage.ts +2 -1
- package/src/index.ts +132 -33
- package/src/log.ts +36 -19
- package/src/postgres.ts +232 -38
- package/src/sqlite.ts +204 -25
- package/src/usage.ts +96 -32
package/src/sqlite.ts
CHANGED
|
@@ -11,12 +11,21 @@ import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistenc
|
|
|
11
11
|
import {
|
|
12
12
|
type Backend,
|
|
13
13
|
type BackendTx,
|
|
14
|
+
type EventCountBucket,
|
|
14
15
|
type EventInsert,
|
|
15
16
|
type EventRow,
|
|
16
17
|
type SessionRow,
|
|
17
18
|
} from "./backend.ts";
|
|
18
19
|
import { sessionConflictRow, sessionInsertRow, titleOfEventData, usageRowOf } from "./log.ts";
|
|
19
|
-
import
|
|
20
|
+
import {
|
|
21
|
+
COUNTED_EVENT_TYPES,
|
|
22
|
+
addActivityCount,
|
|
23
|
+
addTokenTotals,
|
|
24
|
+
addTotals,
|
|
25
|
+
emptyTotals,
|
|
26
|
+
localDayKey,
|
|
27
|
+
} from "./usage.ts";
|
|
28
|
+
import type { UsageActivityTotals, UsageAggregate, UsageTokenTotals } from "./usage.ts";
|
|
20
29
|
import {
|
|
21
30
|
DEFAULT_BUSY_TIMEOUT_MS,
|
|
22
31
|
SCHEMA_VERSION,
|
|
@@ -39,12 +48,11 @@ import type { StorageRepository } from "./storage-takeover/types.ts";
|
|
|
39
48
|
type SqliteDb = NodeSQLiteDatabase & { $client: DatabaseSync };
|
|
40
49
|
|
|
41
50
|
/** 用量聚合的原始行(列名是 SQL 别名)。 */
|
|
42
|
-
interface
|
|
51
|
+
interface RawBucketRow {
|
|
43
52
|
day: string;
|
|
44
53
|
provider: string | null;
|
|
45
54
|
model: string | null;
|
|
46
55
|
subagent: number;
|
|
47
|
-
events: number;
|
|
48
56
|
input_tokens: number | null;
|
|
49
57
|
output_tokens: number | null;
|
|
50
58
|
cache_read_tokens: number | null;
|
|
@@ -52,6 +60,55 @@ interface RawUsageRow {
|
|
|
52
60
|
total_tokens: number | null;
|
|
53
61
|
}
|
|
54
62
|
|
|
63
|
+
/** 活动计数行(派生表按事件类型存,读的时候折成四项)。 */
|
|
64
|
+
interface RawCountRow {
|
|
65
|
+
f_type: string;
|
|
66
|
+
n: number | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 「是否子代理」两组的 token 合计行。 */
|
|
70
|
+
interface RawScopeRow extends RawBucketRow {}
|
|
71
|
+
|
|
72
|
+
/** 按会话的行:token 一条查询、活动一条查询,按 session_id 合并。 */
|
|
73
|
+
interface RawSessionTokenRow extends RawBucketRow {
|
|
74
|
+
session_id: string;
|
|
75
|
+
title: string | null;
|
|
76
|
+
archived: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface RawSessionCountRow extends RawCountRow {
|
|
80
|
+
session_id: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 活动计数的派生表:可销毁重建(不在 drizzle schema / 迁移里,建表幂等)。 */
|
|
84
|
+
function ensureEventCountsTable(db: DatabaseSync): void {
|
|
85
|
+
db.exec(`CREATE TABLE IF NOT EXISTS t_event_counts (
|
|
86
|
+
f_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
87
|
+
f_session_id TEXT NOT NULL,
|
|
88
|
+
f_day TEXT NOT NULL,
|
|
89
|
+
f_type TEXT NOT NULL,
|
|
90
|
+
f_count INTEGER NOT NULL DEFAULT 0
|
|
91
|
+
);
|
|
92
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_event_counts_session_day_type
|
|
93
|
+
ON t_event_counts(f_session_id, f_day, f_type);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_event_counts_day ON t_event_counts(f_day);`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 一次性回填活动计数(表为空时从事件表重算;幂等)。 */
|
|
98
|
+
function backfillEventCounts(db: DatabaseSync): void {
|
|
99
|
+
const pending = db.prepare("SELECT count(*) AS n FROM t_event_counts").get() as { n: number };
|
|
100
|
+
if (pending.n > 0) return;
|
|
101
|
+
db.exec(`INSERT OR IGNORE INTO t_event_counts (f_session_id, f_day, f_type, f_count)
|
|
102
|
+
SELECT b.f_session_id,
|
|
103
|
+
date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
|
|
104
|
+
e.f_type,
|
|
105
|
+
count(*)
|
|
106
|
+
FROM t_session_events b
|
|
107
|
+
JOIN t_events e ON e.f_event_id = b.f_event_id
|
|
108
|
+
WHERE e.f_type IN (${COUNTED_EVENT_TYPE_SQL})
|
|
109
|
+
GROUP BY 1, 2, 3`);
|
|
110
|
+
}
|
|
111
|
+
|
|
55
112
|
/**
|
|
56
113
|
* 一次性回填:旧库的用量从未写过 `t_event_usage`,建表后按事件行补一次(幂等:表非空即跳过)。
|
|
57
114
|
*/
|
|
@@ -83,9 +140,14 @@ function backfillEventUsage(db: DatabaseSync): void {
|
|
|
83
140
|
}
|
|
84
141
|
|
|
85
142
|
/** SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。 */
|
|
86
|
-
function
|
|
143
|
+
function tokenTotalsOf(row: {
|
|
144
|
+
input_tokens: number | null;
|
|
145
|
+
output_tokens: number | null;
|
|
146
|
+
cache_read_tokens: number | null;
|
|
147
|
+
reasoning_tokens: number | null;
|
|
148
|
+
total_tokens: number | null;
|
|
149
|
+
}): UsageTokenTotals {
|
|
87
150
|
return {
|
|
88
|
-
events: row.events,
|
|
89
151
|
inputTokens: row.input_tokens ?? 0,
|
|
90
152
|
outputTokens: row.output_tokens ?? 0,
|
|
91
153
|
cacheReadTokens: row.cache_read_tokens ?? 0,
|
|
@@ -94,6 +156,25 @@ function totalsOf(row: RawUsageRow): UsageTotals {
|
|
|
94
156
|
};
|
|
95
157
|
}
|
|
96
158
|
|
|
159
|
+
/** 一行的整套指标:token 用量 + 活动计数(轮次 / 步骤 / 用户输入 / 工具调用)。 */
|
|
160
|
+
function emptyActivity(): UsageActivityTotals {
|
|
161
|
+
return { turns: 0, steps: 0, userInputs: 0, toolCalls: 0 };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 有 token 用量的会话:活动计数与列表都限定在这一批会话里,口径一致。 */
|
|
165
|
+
function hasUsageSession(column: string): string {
|
|
166
|
+
return `EXISTS (SELECT 1 FROM t_session_events ub
|
|
167
|
+
JOIN t_event_usage uu ON uu.f_event_id = ub.f_event_id
|
|
168
|
+
WHERE ub.f_session_id = ${column})`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const SUBAGENT_OF_EVENT = `EXISTS (SELECT 1 FROM t_session_events sb
|
|
172
|
+
JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
|
|
173
|
+
WHERE sb.f_event_id = %EVENT% AND ss.f_origin = 'subagent')`;
|
|
174
|
+
|
|
175
|
+
/** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
|
|
176
|
+
const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
|
|
177
|
+
|
|
97
178
|
const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
|
|
98
179
|
|
|
99
180
|
const sqliteTxQueues = new Map<string, Promise<void>>();
|
|
@@ -138,6 +219,8 @@ export async function openDatabase(
|
|
|
138
219
|
await baselineV2(db);
|
|
139
220
|
}
|
|
140
221
|
migrate(dbx, { migrationsFolder: sqliteMigrationsDir });
|
|
222
|
+
ensureEventCountsTable(db);
|
|
223
|
+
backfillEventCounts(db);
|
|
141
224
|
backfillEventUsage(db);
|
|
142
225
|
|
|
143
226
|
dbx
|
|
@@ -358,8 +441,12 @@ export class SqliteBackend implements Backend {
|
|
|
358
441
|
return scoped.orderBy(tSessionEvents.fSequence).all() as unknown as EventRow[];
|
|
359
442
|
}
|
|
360
443
|
|
|
444
|
+
// 只取类型:rewind 的边界 / 窗口探测不该把整个事件 JSON(f_data)拖回来。
|
|
361
445
|
async getEventTypeAt(id: SessionId, sequence: number): Promise<string | undefined> {
|
|
362
|
-
const row = this.
|
|
446
|
+
const row = this.db
|
|
447
|
+
.select({ fType: tEvents.fType })
|
|
448
|
+
.from(tSessionEvents)
|
|
449
|
+
.innerJoin(tEvents, eq(tSessionEvents.fEventId, tEvents.fEventId))
|
|
363
450
|
.where(and(eq(tSessionEvents.fSessionId, id), eq(tSessionEvents.fSequence, sequence)))
|
|
364
451
|
.get() as { fType?: string } | undefined;
|
|
365
452
|
return row?.fType;
|
|
@@ -370,7 +457,10 @@ export class SqliteBackend implements Backend {
|
|
|
370
457
|
beforeSequence: number,
|
|
371
458
|
limit: number,
|
|
372
459
|
): Promise<Array<Pick<EventRow, "fSequence" | "fType">>> {
|
|
373
|
-
return this.
|
|
460
|
+
return this.db
|
|
461
|
+
.select({ fSequence: tSessionEvents.fSequence, fType: tEvents.fType })
|
|
462
|
+
.from(tSessionEvents)
|
|
463
|
+
.innerJoin(tEvents, eq(tSessionEvents.fEventId, tEvents.fEventId))
|
|
374
464
|
.where(and(eq(tSessionEvents.fSessionId, id), lt(tSessionEvents.fSequence, beforeSequence)))
|
|
375
465
|
.orderBy(desc(tSessionEvents.fSequence))
|
|
376
466
|
.limit(limit)
|
|
@@ -597,36 +687,65 @@ export class SqliteBackend implements Backend {
|
|
|
597
687
|
return Number(info.changes);
|
|
598
688
|
}
|
|
599
689
|
|
|
600
|
-
/**
|
|
690
|
+
/**
|
|
691
|
+
* 用量聚合:token 用量沿 `t_event_usage` 的用量行,活动计数(轮次 / 步骤 / 用户输入 / 工具调用)
|
|
692
|
+
* 沿 `t_events` 的事件类型数——两者限定在**有 token 用量的会话**里,时间范围各自按自己的
|
|
693
|
+
* `f_created_at`(同一时刻写入,口径一致)。
|
|
694
|
+
*/
|
|
601
695
|
async usageReport(sinceMs?: number): Promise<UsageAggregate> {
|
|
602
|
-
const
|
|
603
|
-
const
|
|
696
|
+
const usageSince = sinceMs === undefined ? "" : " AND u.f_created_at >= ?";
|
|
697
|
+
const params = sinceMs === undefined ? [] : [sinceMs];
|
|
698
|
+
// 活动计数走派生表的「本地日」列(与桶的 localtime 口径一致)。
|
|
699
|
+
const daySince = sinceMs === undefined ? "" : " AND c.f_day >= ?";
|
|
700
|
+
const dayParams = sinceMs === undefined ? [] : [localDayKey(sinceMs)];
|
|
701
|
+
const subagentOfUsage = SUBAGENT_OF_EVENT.replace("%EVENT%", "u.f_event_id");
|
|
702
|
+
|
|
604
703
|
const buckets = this.db.$client
|
|
605
704
|
.prepare(
|
|
606
705
|
`SELECT date(u.f_created_at / 1000, 'unixepoch', 'localtime') AS day,
|
|
607
706
|
u.f_provider AS provider,
|
|
608
707
|
u.f_model AS model,
|
|
609
|
-
|
|
610
|
-
JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
|
|
611
|
-
WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent') AS subagent,
|
|
612
|
-
count(*) AS events,
|
|
708
|
+
${subagentOfUsage} AS subagent,
|
|
613
709
|
sum(u.f_input_tokens) AS input_tokens,
|
|
614
710
|
sum(u.f_output_tokens) AS output_tokens,
|
|
615
711
|
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
616
712
|
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
617
713
|
sum(u.f_total_tokens) AS total_tokens
|
|
618
714
|
FROM t_event_usage u
|
|
619
|
-
WHERE EXISTS (SELECT 1 FROM t_session_events rb WHERE rb.f_event_id = u.f_event_id)${
|
|
715
|
+
WHERE EXISTS (SELECT 1 FROM t_session_events rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
|
|
620
716
|
GROUP BY day, provider, model, subagent`,
|
|
621
717
|
)
|
|
622
|
-
.all(...
|
|
623
|
-
const
|
|
718
|
+
.all(...params) as unknown as RawBucketRow[];
|
|
719
|
+
const tokenScope = this.db.$client
|
|
720
|
+
.prepare(
|
|
721
|
+
`SELECT ${subagentOfUsage} AS subagent,
|
|
722
|
+
sum(u.f_input_tokens) AS input_tokens,
|
|
723
|
+
sum(u.f_output_tokens) AS output_tokens,
|
|
724
|
+
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
725
|
+
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
726
|
+
sum(u.f_total_tokens) AS total_tokens
|
|
727
|
+
FROM t_event_usage u
|
|
728
|
+
WHERE EXISTS (SELECT 1 FROM t_session_events rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
|
|
729
|
+
GROUP BY subagent`,
|
|
730
|
+
)
|
|
731
|
+
.all(...params) as unknown as RawScopeRow[];
|
|
732
|
+
const activityScope = this.db.$client
|
|
733
|
+
.prepare(
|
|
734
|
+
`SELECT (s.f_origin = 'subagent') AS subagent,
|
|
735
|
+
c.f_type AS f_type,
|
|
736
|
+
sum(c.f_count) AS n
|
|
737
|
+
FROM t_event_counts c
|
|
738
|
+
JOIN t_sessions s ON s.f_session_id = c.f_session_id
|
|
739
|
+
WHERE ${hasUsageSession("c.f_session_id")}${daySince}
|
|
740
|
+
GROUP BY subagent, c.f_type`,
|
|
741
|
+
)
|
|
742
|
+
.all(...dayParams) as unknown as Array<RawCountRow & { subagent: number }>;
|
|
743
|
+
const tokenSessions = this.db.$client
|
|
624
744
|
.prepare(
|
|
625
745
|
`SELECT b.f_session_id AS session_id,
|
|
626
746
|
s.f_title AS title,
|
|
627
747
|
(s.f_origin = 'subagent') AS subagent,
|
|
628
748
|
(s.f_archived_at IS NOT NULL) AS archived,
|
|
629
|
-
count(*) AS events,
|
|
630
749
|
sum(u.f_input_tokens) AS input_tokens,
|
|
631
750
|
sum(u.f_output_tokens) AS output_tokens,
|
|
632
751
|
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
@@ -635,30 +754,90 @@ export class SqliteBackend implements Backend {
|
|
|
635
754
|
FROM t_session_events b
|
|
636
755
|
JOIN t_event_usage u ON u.f_event_id = b.f_event_id
|
|
637
756
|
JOIN t_sessions s ON s.f_session_id = b.f_session_id
|
|
638
|
-
WHERE 1 = 1${
|
|
757
|
+
WHERE 1 = 1${usageSince}
|
|
639
758
|
GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at`,
|
|
640
759
|
)
|
|
641
|
-
.all(...
|
|
642
|
-
|
|
643
|
-
|
|
760
|
+
.all(...params) as unknown as RawSessionTokenRow[];
|
|
761
|
+
const activitySessions = this.db.$client
|
|
762
|
+
.prepare(
|
|
763
|
+
`SELECT c.f_session_id AS session_id,
|
|
764
|
+
c.f_type AS f_type,
|
|
765
|
+
sum(c.f_count) AS n
|
|
766
|
+
FROM t_event_counts c
|
|
767
|
+
WHERE ${hasUsageSession("c.f_session_id")}${daySince}
|
|
768
|
+
GROUP BY c.f_session_id, c.f_type`,
|
|
769
|
+
)
|
|
770
|
+
.all(...dayParams) as unknown as RawSessionCountRow[];
|
|
771
|
+
const human = emptyTotals();
|
|
772
|
+
const subagent = emptyTotals();
|
|
773
|
+
for (const row of tokenScope) {
|
|
774
|
+
addTokenTotals(row.subagent === 1 ? subagent : human, tokenTotalsOf(row));
|
|
775
|
+
}
|
|
776
|
+
for (const row of activityScope) {
|
|
777
|
+
addActivityCount(row.subagent === 1 ? subagent : human, row.f_type, row.n ?? 0);
|
|
778
|
+
}
|
|
779
|
+
const totals = addTotals(addTotals(emptyTotals(), human), subagent);
|
|
780
|
+
|
|
781
|
+
const activityBySession = new Map<string, UsageActivityTotals>();
|
|
782
|
+
for (const row of activitySessions) {
|
|
783
|
+
const target = activityBySession.get(row.session_id) ?? emptyActivity();
|
|
784
|
+
addActivityCount(target, row.f_type, row.n ?? 0);
|
|
785
|
+
activityBySession.set(row.session_id, target);
|
|
786
|
+
}
|
|
644
787
|
return {
|
|
788
|
+
totals,
|
|
789
|
+
subagent,
|
|
790
|
+
human,
|
|
645
791
|
buckets: buckets.map((row) => ({
|
|
646
792
|
day: row.day,
|
|
647
793
|
provider: row.provider,
|
|
648
794
|
model: row.model,
|
|
649
795
|
subagent: row.subagent === 1,
|
|
650
|
-
...
|
|
796
|
+
...tokenTotalsOf(row),
|
|
651
797
|
})),
|
|
652
|
-
sessions:
|
|
798
|
+
sessions: tokenSessions.map((row) => ({
|
|
653
799
|
sessionId: row.session_id,
|
|
654
800
|
title: row.title,
|
|
655
801
|
subagent: row.subagent === 1,
|
|
656
802
|
archived: row.archived === 1,
|
|
657
|
-
...
|
|
803
|
+
...tokenTotalsOf(row),
|
|
804
|
+
...(activityBySession.get(row.session_id) ?? emptyActivity()),
|
|
658
805
|
})),
|
|
659
806
|
};
|
|
660
807
|
}
|
|
661
808
|
|
|
809
|
+
/** 活动计数旁路累加:派生表 `t_event_counts`,不在写事务里(失败可丢,表可销毁重建)。 */
|
|
810
|
+
async incrementEventCounts(id: SessionId, buckets: readonly EventCountBucket[]): Promise<void> {
|
|
811
|
+
if (buckets.length === 0) return;
|
|
812
|
+
const statement = this.db.$client.prepare(
|
|
813
|
+
`INSERT INTO t_event_counts (f_session_id, f_day, f_type, f_count) VALUES (?, ?, ?, ?)
|
|
814
|
+
ON CONFLICT(f_session_id, f_day, f_type) DO UPDATE SET f_count = f_count + excluded.f_count`,
|
|
815
|
+
);
|
|
816
|
+
for (const bucket of buckets) statement.run(id, bucket.day, bucket.type, bucket.count);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** 按会话重算活动计数(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
|
|
820
|
+
async rebuildEventCounts(id: SessionId): Promise<void> {
|
|
821
|
+
this.db.$client.prepare("DELETE FROM t_event_counts WHERE f_session_id = ?").run(id);
|
|
822
|
+
this.db.$client
|
|
823
|
+
.prepare(
|
|
824
|
+
`INSERT INTO t_event_counts (f_session_id, f_day, f_type, f_count)
|
|
825
|
+
SELECT b.f_session_id,
|
|
826
|
+
date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
|
|
827
|
+
e.f_type,
|
|
828
|
+
count(*)
|
|
829
|
+
FROM t_session_events b
|
|
830
|
+
JOIN t_events e ON e.f_event_id = b.f_event_id
|
|
831
|
+
WHERE b.f_session_id = ? AND e.f_type IN (${COUNTED_EVENT_TYPE_SQL})
|
|
832
|
+
GROUP BY 1, 2, 3`,
|
|
833
|
+
)
|
|
834
|
+
.run(id);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async deleteEventCounts(id: SessionId): Promise<void> {
|
|
838
|
+
this.db.$client.prepare("DELETE FROM t_event_counts WHERE f_session_id = ?").run(id);
|
|
839
|
+
}
|
|
840
|
+
|
|
662
841
|
async vacuum(): Promise<void> {
|
|
663
842
|
await enqueueSqliteTx(this.dbPath, async () => {
|
|
664
843
|
this.db.$client.exec("VACUUM");
|
package/src/usage.ts
CHANGED
|
@@ -48,9 +48,8 @@ function parseUsageRange(value: unknown): UsageRangeKey {
|
|
|
48
48
|
: "all";
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
export interface
|
|
53
|
-
events: number;
|
|
51
|
+
/** 一段 token 用量(字段直接取事件里模型报的 usage)。 */
|
|
52
|
+
export interface UsageTokenTotals {
|
|
54
53
|
inputTokens: number;
|
|
55
54
|
outputTokens: number;
|
|
56
55
|
cacheReadTokens: number;
|
|
@@ -58,8 +57,25 @@ export interface UsageTotals {
|
|
|
58
57
|
totalTokens: number;
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
/**
|
|
62
|
-
|
|
60
|
+
/**
|
|
61
|
+
* 活动计数:与 token 用量同一时间范围、同一去重口径(只算被会话引用的事件行),
|
|
62
|
+
* 按事件类型数出来——「轮次 / 步骤 / 用户输入 / 工具调用」比「事件行数」有信息量。
|
|
63
|
+
*/
|
|
64
|
+
export interface UsageActivityTotals {
|
|
65
|
+
turns: number;
|
|
66
|
+
steps: number;
|
|
67
|
+
userInputs: number;
|
|
68
|
+
toolCalls: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 总量与按会话行用的一整套指标(token + 活动)。 */
|
|
72
|
+
export interface UsageTotals extends UsageTokenTotals, UsageActivityTotals {}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 一天 × 一个模型 × 是否子代理 的用量桶:总览、按天、按模型都由它折叠。
|
|
76
|
+
* 只有 token 用量——活动计数没有模型归属(一个轮次可能跨模型),挂在总量与按会话行上。
|
|
77
|
+
*/
|
|
78
|
+
export interface UsageBucket extends UsageTokenTotals {
|
|
63
79
|
day: string;
|
|
64
80
|
provider: string | null;
|
|
65
81
|
model: string | null;
|
|
@@ -74,25 +90,63 @@ export interface UsageSessionRow extends UsageTotals {
|
|
|
74
90
|
archived: boolean;
|
|
75
91
|
}
|
|
76
92
|
|
|
77
|
-
/**
|
|
93
|
+
/** 后端的原始聚合结果(总量 / 其中子代理 / 人类、按天×模型的桶、按会话的行)。 */
|
|
78
94
|
export interface UsageAggregate {
|
|
79
|
-
buckets: UsageBucket[];
|
|
80
|
-
sessions: UsageSessionRow[];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** 一次统计请求的完整回报。 */
|
|
84
|
-
export interface SessionUsageReport {
|
|
85
95
|
totals: UsageTotals;
|
|
86
96
|
subagent: UsageTotals;
|
|
87
|
-
/** 只被人类会话引用的部分(`totals − subagent`)。 */
|
|
88
97
|
human: UsageTotals;
|
|
89
98
|
buckets: UsageBucket[];
|
|
90
99
|
sessions: UsageSessionRow[];
|
|
91
100
|
}
|
|
92
101
|
|
|
93
|
-
|
|
102
|
+
/** 一次统计请求的完整回报。 */
|
|
103
|
+
export type SessionUsageReport = UsageAggregate;
|
|
104
|
+
|
|
105
|
+
/** 计入活动计数的事件类型(轮次 / 步骤 / 用户输入 / 工具调用)。 */
|
|
106
|
+
export const COUNTED_EVENT_TYPES = [
|
|
107
|
+
"turn/start",
|
|
108
|
+
"step/start",
|
|
109
|
+
"user/message",
|
|
110
|
+
"tool/call",
|
|
111
|
+
] as const;
|
|
112
|
+
|
|
113
|
+
/** 按事件类型把计数累加到活动指标上(表里按类型存,读的时候折成四项)。 */
|
|
114
|
+
export function addActivityCount(
|
|
115
|
+
target: UsageActivityTotals,
|
|
116
|
+
type: string,
|
|
117
|
+
count: number,
|
|
118
|
+
): UsageActivityTotals {
|
|
119
|
+
switch (type) {
|
|
120
|
+
case "turn/start":
|
|
121
|
+
target.turns += count;
|
|
122
|
+
return target;
|
|
123
|
+
case "step/start":
|
|
124
|
+
target.steps += count;
|
|
125
|
+
return target;
|
|
126
|
+
case "user/message":
|
|
127
|
+
target.userInputs += count;
|
|
128
|
+
return target;
|
|
129
|
+
case "tool/call":
|
|
130
|
+
target.toolCalls += count;
|
|
131
|
+
return target;
|
|
132
|
+
default:
|
|
133
|
+
return target;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 毫秒时间戳 → host 本地日(`YYYY-MM-DD`,与 SQL 的 localtime 口径一致)。 */
|
|
138
|
+
export function localDayKey(ms: number): string {
|
|
139
|
+
const date = new Date(ms);
|
|
140
|
+
const pad = (value: number): string => String(value).padStart(2, "0");
|
|
141
|
+
return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function emptyTotals(): UsageTotals {
|
|
94
145
|
return {
|
|
95
|
-
|
|
146
|
+
turns: 0,
|
|
147
|
+
steps: 0,
|
|
148
|
+
userInputs: 0,
|
|
149
|
+
toolCalls: 0,
|
|
96
150
|
inputTokens: 0,
|
|
97
151
|
outputTokens: 0,
|
|
98
152
|
cacheReadTokens: 0,
|
|
@@ -101,18 +155,31 @@ function emptyTotals(): UsageTotals {
|
|
|
101
155
|
};
|
|
102
156
|
}
|
|
103
157
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
158
|
+
export function addTokenTotals(target: UsageTokenTotals, row: UsageTokenTotals): UsageTokenTotals {
|
|
159
|
+
target.inputTokens += row.inputTokens;
|
|
160
|
+
target.outputTokens += row.outputTokens;
|
|
161
|
+
target.cacheReadTokens += row.cacheReadTokens;
|
|
162
|
+
target.reasoningTokens += row.reasoningTokens;
|
|
163
|
+
target.totalTokens += row.totalTokens;
|
|
164
|
+
return target;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function addActivityTotals(
|
|
168
|
+
target: UsageActivityTotals,
|
|
169
|
+
row: UsageActivityTotals,
|
|
170
|
+
): UsageActivityTotals {
|
|
171
|
+
target.turns += row.turns;
|
|
172
|
+
target.steps += row.steps;
|
|
173
|
+
target.userInputs += row.userInputs;
|
|
174
|
+
target.toolCalls += row.toolCalls;
|
|
175
|
+
return target;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 把一行的整套指标累加进目标(后端合并「是否子代理」两组时用)。 */
|
|
179
|
+
export function addTotals(target: UsageTotals, row: UsageTotals): UsageTotals {
|
|
180
|
+
addActivityTotals(target, row);
|
|
181
|
+
addTokenTotals(target, row);
|
|
182
|
+
return target;
|
|
116
183
|
}
|
|
117
184
|
|
|
118
185
|
/**
|
|
@@ -167,14 +234,11 @@ export function registerSessionUsage(ctx: Context, persistence: SessionPersisten
|
|
|
167
234
|
}
|
|
168
235
|
}
|
|
169
236
|
try {
|
|
170
|
-
const
|
|
237
|
+
const report = await persistence.usageReport(
|
|
171
238
|
resolveUsageSince(parseUsageRange(envelope.range), Date.now()),
|
|
172
239
|
);
|
|
173
|
-
const totals = sumUsage(aggregate.buckets);
|
|
174
|
-
const subagent = sumUsage(aggregate.buckets.filter((bucket) => bucket.subagent));
|
|
175
|
-
const human = sumUsage(aggregate.buckets.filter((bucket) => !bucket.subagent));
|
|
176
240
|
res.writeHead(200, { "content-type": "application/json" });
|
|
177
|
-
res.end(JSON.stringify(
|
|
241
|
+
res.end(JSON.stringify(report));
|
|
178
242
|
} catch (error: unknown) {
|
|
179
243
|
res.writeHead(500, { "content-type": "application/json" });
|
|
180
244
|
res.end(
|