@chrysb/alphaclaw 0.3.3 → 0.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/bin/alphaclaw.js +18 -0
- package/lib/plugin/usage-tracker/index.js +308 -0
- package/lib/plugin/usage-tracker/openclaw.plugin.json +8 -0
- package/lib/public/css/explorer.css +51 -1
- package/lib/public/css/shell.css +3 -1
- package/lib/public/css/theme.css +35 -0
- package/lib/public/js/app.js +73 -24
- package/lib/public/js/components/file-tree.js +231 -28
- package/lib/public/js/components/file-viewer.js +193 -20
- package/lib/public/js/components/segmented-control.js +33 -0
- package/lib/public/js/components/sidebar.js +14 -32
- package/lib/public/js/components/telegram-workspace/index.js +353 -0
- package/lib/public/js/components/telegram-workspace/manage.js +397 -0
- package/lib/public/js/components/telegram-workspace/onboarding.js +616 -0
- package/lib/public/js/components/usage-tab.js +528 -0
- package/lib/public/js/components/watchdog-tab.js +1 -1
- package/lib/public/js/lib/api.js +25 -1
- package/lib/public/js/lib/telegram-api.js +78 -0
- package/lib/public/js/lib/ui-settings.js +38 -0
- package/lib/public/setup.html +34 -30
- package/lib/server/alphaclaw-version.js +3 -3
- package/lib/server/constants.js +1 -0
- package/lib/server/onboarding/openclaw.js +15 -0
- package/lib/server/routes/auth.js +5 -1
- package/lib/server/routes/telegram.js +185 -60
- package/lib/server/routes/usage.js +133 -0
- package/lib/server/usage-db.js +570 -0
- package/lib/server.js +21 -1
- package/lib/setup/core-prompts/AGENTS.md +0 -101
- package/package.json +1 -1
- package/lib/public/js/components/telegram-workspace.js +0 -1365
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
4
|
+
|
|
5
|
+
const kDefaultSessionLimit = 50;
|
|
6
|
+
const kMaxSessionLimit = 200;
|
|
7
|
+
const kDefaultDays = 30;
|
|
8
|
+
const kDefaultMaxPoints = 100;
|
|
9
|
+
const kMaxMaxPoints = 1000;
|
|
10
|
+
const kTokensPerMillion = 1_000_000;
|
|
11
|
+
const kGlobalModelPricing = {
|
|
12
|
+
"claude-opus-4-6": { input: 15.0, output: 75.0 },
|
|
13
|
+
"claude-sonnet-4-6": { input: 3.0, output: 15.0 },
|
|
14
|
+
"claude-haiku-4-6": { input: 0.8, output: 4.0 },
|
|
15
|
+
"gpt-5.1-codex": { input: 2.5, output: 10.0 },
|
|
16
|
+
"gpt-5.3-codex": { input: 2.5, output: 10.0 },
|
|
17
|
+
"gpt-4o": { input: 2.5, output: 10.0 },
|
|
18
|
+
"gemini-3-pro-preview": { input: 1.25, output: 5.0 },
|
|
19
|
+
"gemini-3-flash-preview": { input: 0.1, output: 0.4 },
|
|
20
|
+
"gemini-2.0-flash": { input: 0.1, output: 0.4 },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let db = null;
|
|
24
|
+
let usageDbPath = "";
|
|
25
|
+
|
|
26
|
+
const coerceInt = (value, fallbackValue = 0) => {
|
|
27
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
28
|
+
return Number.isFinite(parsed) ? parsed : fallbackValue;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const clampInt = (value, minValue, maxValue, fallbackValue) =>
|
|
32
|
+
Math.min(maxValue, Math.max(minValue, coerceInt(value, fallbackValue)));
|
|
33
|
+
|
|
34
|
+
const resolvePricing = (model) => {
|
|
35
|
+
const normalized = String(model || "").toLowerCase();
|
|
36
|
+
if (!normalized) return null;
|
|
37
|
+
const exact = kGlobalModelPricing[normalized];
|
|
38
|
+
if (exact) return exact;
|
|
39
|
+
const matchKey = Object.keys(kGlobalModelPricing).find((key) =>
|
|
40
|
+
normalized.includes(key),
|
|
41
|
+
);
|
|
42
|
+
return matchKey ? kGlobalModelPricing[matchKey] : null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const deriveCostBreakdown = ({
|
|
46
|
+
inputTokens = 0,
|
|
47
|
+
outputTokens = 0,
|
|
48
|
+
cacheReadTokens = 0,
|
|
49
|
+
cacheWriteTokens = 0,
|
|
50
|
+
model = "",
|
|
51
|
+
}) => {
|
|
52
|
+
const pricing = resolvePricing(model);
|
|
53
|
+
if (!pricing) {
|
|
54
|
+
return {
|
|
55
|
+
inputCost: 0,
|
|
56
|
+
outputCost: 0,
|
|
57
|
+
cacheReadCost: 0,
|
|
58
|
+
cacheWriteCost: 0,
|
|
59
|
+
totalCost: 0,
|
|
60
|
+
pricingFound: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const inputCost = (inputTokens / kTokensPerMillion) * pricing.input;
|
|
64
|
+
const outputCost = (outputTokens / kTokensPerMillion) * pricing.output;
|
|
65
|
+
const cacheReadCost = 0;
|
|
66
|
+
const cacheWriteCost = (cacheWriteTokens / kTokensPerMillion) * pricing.input;
|
|
67
|
+
return {
|
|
68
|
+
inputCost,
|
|
69
|
+
outputCost,
|
|
70
|
+
cacheReadCost,
|
|
71
|
+
cacheWriteCost,
|
|
72
|
+
totalCost: inputCost + outputCost + cacheReadCost + cacheWriteCost,
|
|
73
|
+
pricingFound: true,
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const ensureDb = () => {
|
|
78
|
+
if (!db) throw new Error("Usage DB not initialized");
|
|
79
|
+
return db;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const safeAlterTable = (database, sql) => {
|
|
83
|
+
try {
|
|
84
|
+
database.exec(sql);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const message = String(err?.message || "").toLowerCase();
|
|
87
|
+
if (!message.includes("duplicate column name")) throw err;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const ensureSchema = (database) => {
|
|
92
|
+
database.exec("PRAGMA journal_mode=WAL;");
|
|
93
|
+
database.exec("PRAGMA synchronous=NORMAL;");
|
|
94
|
+
database.exec("PRAGMA busy_timeout=5000;");
|
|
95
|
+
database.exec(`
|
|
96
|
+
CREATE TABLE IF NOT EXISTS usage_events (
|
|
97
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
98
|
+
timestamp INTEGER NOT NULL,
|
|
99
|
+
session_id TEXT,
|
|
100
|
+
session_key TEXT,
|
|
101
|
+
run_id TEXT,
|
|
102
|
+
provider TEXT NOT NULL,
|
|
103
|
+
model TEXT NOT NULL,
|
|
104
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
105
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
106
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
107
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
108
|
+
total_tokens INTEGER NOT NULL DEFAULT 0
|
|
109
|
+
);
|
|
110
|
+
`);
|
|
111
|
+
database.exec(`
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_ts
|
|
113
|
+
ON usage_events(timestamp DESC);
|
|
114
|
+
`);
|
|
115
|
+
database.exec(`
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_session
|
|
117
|
+
ON usage_events(session_id);
|
|
118
|
+
`);
|
|
119
|
+
safeAlterTable(
|
|
120
|
+
database,
|
|
121
|
+
"ALTER TABLE usage_events ADD COLUMN session_key TEXT;",
|
|
122
|
+
);
|
|
123
|
+
database.exec(`
|
|
124
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_session_key
|
|
125
|
+
ON usage_events(session_key);
|
|
126
|
+
`);
|
|
127
|
+
database.exec(`
|
|
128
|
+
CREATE TABLE IF NOT EXISTS usage_daily (
|
|
129
|
+
date TEXT NOT NULL,
|
|
130
|
+
model TEXT NOT NULL,
|
|
131
|
+
provider TEXT,
|
|
132
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
133
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
134
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
135
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
136
|
+
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
137
|
+
turn_count INTEGER NOT NULL DEFAULT 0,
|
|
138
|
+
PRIMARY KEY (date, model)
|
|
139
|
+
);
|
|
140
|
+
`);
|
|
141
|
+
database.exec(`
|
|
142
|
+
CREATE TABLE IF NOT EXISTS tool_events (
|
|
143
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
144
|
+
timestamp INTEGER NOT NULL,
|
|
145
|
+
session_id TEXT,
|
|
146
|
+
session_key TEXT,
|
|
147
|
+
tool_name TEXT NOT NULL,
|
|
148
|
+
success INTEGER NOT NULL DEFAULT 1,
|
|
149
|
+
duration_ms INTEGER
|
|
150
|
+
);
|
|
151
|
+
`);
|
|
152
|
+
database.exec(`
|
|
153
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_session
|
|
154
|
+
ON tool_events(session_id);
|
|
155
|
+
`);
|
|
156
|
+
safeAlterTable(
|
|
157
|
+
database,
|
|
158
|
+
"ALTER TABLE tool_events ADD COLUMN session_key TEXT;",
|
|
159
|
+
);
|
|
160
|
+
database.exec(`
|
|
161
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_session_key
|
|
162
|
+
ON tool_events(session_key);
|
|
163
|
+
`);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const initUsageDb = ({ rootDir }) => {
|
|
167
|
+
const dbDir = path.join(rootDir, "db");
|
|
168
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
169
|
+
usageDbPath = path.join(dbDir, "usage.db");
|
|
170
|
+
db = new DatabaseSync(usageDbPath);
|
|
171
|
+
ensureSchema(db);
|
|
172
|
+
return { path: usageDbPath };
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const toDayKey = (timestampMs) => new Date(timestampMs).toISOString().slice(0, 10);
|
|
176
|
+
|
|
177
|
+
const getPeriodRange = (days) => {
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
const safeDays = clampInt(days, 1, 3650, kDefaultDays);
|
|
180
|
+
const startMs = now - safeDays * 24 * 60 * 60 * 1000;
|
|
181
|
+
return { now, safeDays, startDay: toDayKey(startMs) };
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const appendCostToRows = (rows) =>
|
|
185
|
+
rows.map((row) => {
|
|
186
|
+
const inputTokens = coerceInt(row.input_tokens);
|
|
187
|
+
const outputTokens = coerceInt(row.output_tokens);
|
|
188
|
+
const cacheReadTokens = coerceInt(row.cache_read_tokens);
|
|
189
|
+
const cacheWriteTokens = coerceInt(row.cache_write_tokens);
|
|
190
|
+
const totalTokens =
|
|
191
|
+
coerceInt(row.total_tokens) ||
|
|
192
|
+
inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
193
|
+
const cost = deriveCostBreakdown({
|
|
194
|
+
inputTokens,
|
|
195
|
+
outputTokens,
|
|
196
|
+
cacheReadTokens,
|
|
197
|
+
cacheWriteTokens,
|
|
198
|
+
model: row.model,
|
|
199
|
+
});
|
|
200
|
+
return {
|
|
201
|
+
...row,
|
|
202
|
+
inputTokens,
|
|
203
|
+
outputTokens,
|
|
204
|
+
cacheReadTokens,
|
|
205
|
+
cacheWriteTokens,
|
|
206
|
+
totalTokens,
|
|
207
|
+
...cost,
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const getDailySummary = ({ days = kDefaultDays } = {}) => {
|
|
212
|
+
const database = ensureDb();
|
|
213
|
+
const { safeDays, startDay } = getPeriodRange(days);
|
|
214
|
+
const rows = database
|
|
215
|
+
.prepare(`
|
|
216
|
+
SELECT
|
|
217
|
+
date,
|
|
218
|
+
model,
|
|
219
|
+
provider,
|
|
220
|
+
input_tokens,
|
|
221
|
+
output_tokens,
|
|
222
|
+
cache_read_tokens,
|
|
223
|
+
cache_write_tokens,
|
|
224
|
+
total_tokens,
|
|
225
|
+
turn_count
|
|
226
|
+
FROM usage_daily
|
|
227
|
+
WHERE date >= $startDay
|
|
228
|
+
ORDER BY date ASC, total_tokens DESC
|
|
229
|
+
`)
|
|
230
|
+
.all({ $startDay: startDay });
|
|
231
|
+
const enriched = appendCostToRows(rows);
|
|
232
|
+
const byDate = new Map();
|
|
233
|
+
for (const row of enriched) {
|
|
234
|
+
if (!byDate.has(row.date)) byDate.set(row.date, []);
|
|
235
|
+
byDate.get(row.date).push({
|
|
236
|
+
model: row.model,
|
|
237
|
+
provider: row.provider,
|
|
238
|
+
inputTokens: row.inputTokens,
|
|
239
|
+
outputTokens: row.outputTokens,
|
|
240
|
+
cacheReadTokens: row.cacheReadTokens,
|
|
241
|
+
cacheWriteTokens: row.cacheWriteTokens,
|
|
242
|
+
totalTokens: row.totalTokens,
|
|
243
|
+
turnCount: coerceInt(row.turn_count),
|
|
244
|
+
totalCost: row.totalCost,
|
|
245
|
+
inputCost: row.inputCost,
|
|
246
|
+
outputCost: row.outputCost,
|
|
247
|
+
cacheReadCost: row.cacheReadCost,
|
|
248
|
+
cacheWriteCost: row.cacheWriteCost,
|
|
249
|
+
pricingFound: row.pricingFound,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const daily = [];
|
|
253
|
+
const totals = {
|
|
254
|
+
inputTokens: 0,
|
|
255
|
+
outputTokens: 0,
|
|
256
|
+
cacheReadTokens: 0,
|
|
257
|
+
cacheWriteTokens: 0,
|
|
258
|
+
totalTokens: 0,
|
|
259
|
+
totalCost: 0,
|
|
260
|
+
turnCount: 0,
|
|
261
|
+
modelCount: 0,
|
|
262
|
+
};
|
|
263
|
+
for (const [date, modelRows] of byDate.entries()) {
|
|
264
|
+
const aggregate = modelRows.reduce(
|
|
265
|
+
(acc, row) => ({
|
|
266
|
+
inputTokens: acc.inputTokens + row.inputTokens,
|
|
267
|
+
outputTokens: acc.outputTokens + row.outputTokens,
|
|
268
|
+
cacheReadTokens: acc.cacheReadTokens + row.cacheReadTokens,
|
|
269
|
+
cacheWriteTokens: acc.cacheWriteTokens + row.cacheWriteTokens,
|
|
270
|
+
totalTokens: acc.totalTokens + row.totalTokens,
|
|
271
|
+
totalCost: acc.totalCost + row.totalCost,
|
|
272
|
+
turnCount: acc.turnCount + row.turnCount,
|
|
273
|
+
}),
|
|
274
|
+
{
|
|
275
|
+
inputTokens: 0,
|
|
276
|
+
outputTokens: 0,
|
|
277
|
+
cacheReadTokens: 0,
|
|
278
|
+
cacheWriteTokens: 0,
|
|
279
|
+
totalTokens: 0,
|
|
280
|
+
totalCost: 0,
|
|
281
|
+
turnCount: 0,
|
|
282
|
+
},
|
|
283
|
+
);
|
|
284
|
+
daily.push({ date, ...aggregate, models: modelRows });
|
|
285
|
+
totals.inputTokens += aggregate.inputTokens;
|
|
286
|
+
totals.outputTokens += aggregate.outputTokens;
|
|
287
|
+
totals.cacheReadTokens += aggregate.cacheReadTokens;
|
|
288
|
+
totals.cacheWriteTokens += aggregate.cacheWriteTokens;
|
|
289
|
+
totals.totalTokens += aggregate.totalTokens;
|
|
290
|
+
totals.totalCost += aggregate.totalCost;
|
|
291
|
+
totals.turnCount += aggregate.turnCount;
|
|
292
|
+
totals.modelCount += modelRows.length;
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
updatedAt: Date.now(),
|
|
296
|
+
days: safeDays,
|
|
297
|
+
daily,
|
|
298
|
+
totals,
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const getSessionsList = ({ limit = kDefaultSessionLimit } = {}) => {
|
|
303
|
+
const database = ensureDb();
|
|
304
|
+
const safeLimit = clampInt(limit, 1, kMaxSessionLimit, kDefaultSessionLimit);
|
|
305
|
+
const rows = database
|
|
306
|
+
.prepare(`
|
|
307
|
+
SELECT
|
|
308
|
+
COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) AS session_ref,
|
|
309
|
+
MAX(session_key) AS session_key,
|
|
310
|
+
MAX(session_id) AS session_id,
|
|
311
|
+
MIN(timestamp) AS first_activity_ms,
|
|
312
|
+
MAX(timestamp) AS last_activity_ms,
|
|
313
|
+
COUNT(*) AS turn_count,
|
|
314
|
+
SUM(input_tokens) AS input_tokens,
|
|
315
|
+
SUM(output_tokens) AS output_tokens,
|
|
316
|
+
SUM(cache_read_tokens) AS cache_read_tokens,
|
|
317
|
+
SUM(cache_write_tokens) AS cache_write_tokens,
|
|
318
|
+
SUM(total_tokens) AS total_tokens
|
|
319
|
+
FROM usage_events
|
|
320
|
+
WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) IS NOT NULL
|
|
321
|
+
GROUP BY session_ref
|
|
322
|
+
ORDER BY last_activity_ms DESC
|
|
323
|
+
LIMIT $limit
|
|
324
|
+
`)
|
|
325
|
+
.all({ $limit: safeLimit });
|
|
326
|
+
return rows.map((row) => {
|
|
327
|
+
const modelRows = appendCostToRows(
|
|
328
|
+
database
|
|
329
|
+
.prepare(`
|
|
330
|
+
SELECT
|
|
331
|
+
model,
|
|
332
|
+
SUM(input_tokens) AS input_tokens,
|
|
333
|
+
SUM(output_tokens) AS output_tokens,
|
|
334
|
+
SUM(cache_read_tokens) AS cache_read_tokens,
|
|
335
|
+
SUM(cache_write_tokens) AS cache_write_tokens,
|
|
336
|
+
SUM(total_tokens) AS total_tokens
|
|
337
|
+
FROM usage_events
|
|
338
|
+
WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
|
|
339
|
+
GROUP BY model
|
|
340
|
+
ORDER BY total_tokens DESC
|
|
341
|
+
`)
|
|
342
|
+
.all({ $sessionRef: row.session_ref }),
|
|
343
|
+
);
|
|
344
|
+
const dominantModel = String(modelRows[0]?.model || "");
|
|
345
|
+
const totalCost = modelRows.reduce(
|
|
346
|
+
(sum, modelRow) => sum + Number(modelRow.totalCost || 0),
|
|
347
|
+
0,
|
|
348
|
+
);
|
|
349
|
+
return {
|
|
350
|
+
sessionId: row.session_ref,
|
|
351
|
+
sessionKey: String(row.session_key || ""),
|
|
352
|
+
rawSessionId: String(row.session_id || ""),
|
|
353
|
+
firstActivityMs: coerceInt(row.first_activity_ms),
|
|
354
|
+
lastActivityMs: coerceInt(row.last_activity_ms),
|
|
355
|
+
durationMs: Math.max(
|
|
356
|
+
0,
|
|
357
|
+
coerceInt(row.last_activity_ms) - coerceInt(row.first_activity_ms),
|
|
358
|
+
),
|
|
359
|
+
turnCount: coerceInt(row.turn_count),
|
|
360
|
+
inputTokens: coerceInt(row.input_tokens),
|
|
361
|
+
outputTokens: coerceInt(row.output_tokens),
|
|
362
|
+
cacheReadTokens: coerceInt(row.cache_read_tokens),
|
|
363
|
+
cacheWriteTokens: coerceInt(row.cache_write_tokens),
|
|
364
|
+
totalTokens: coerceInt(row.total_tokens),
|
|
365
|
+
totalCost,
|
|
366
|
+
dominantModel,
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const getSessionDetail = ({ sessionId }) => {
|
|
372
|
+
const safeSessionRef = String(sessionId || "").trim();
|
|
373
|
+
if (!safeSessionRef) return null;
|
|
374
|
+
const database = ensureDb();
|
|
375
|
+
const summaryRow = database
|
|
376
|
+
.prepare(`
|
|
377
|
+
SELECT
|
|
378
|
+
MAX(session_key) AS session_key,
|
|
379
|
+
MAX(session_id) AS session_id,
|
|
380
|
+
MIN(timestamp) AS first_activity_ms,
|
|
381
|
+
MAX(timestamp) AS last_activity_ms,
|
|
382
|
+
COUNT(*) AS turn_count,
|
|
383
|
+
SUM(input_tokens) AS input_tokens,
|
|
384
|
+
SUM(output_tokens) AS output_tokens,
|
|
385
|
+
SUM(cache_read_tokens) AS cache_read_tokens,
|
|
386
|
+
SUM(cache_write_tokens) AS cache_write_tokens,
|
|
387
|
+
SUM(total_tokens) AS total_tokens
|
|
388
|
+
FROM usage_events
|
|
389
|
+
WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
|
|
390
|
+
`)
|
|
391
|
+
.get({ $sessionRef: safeSessionRef });
|
|
392
|
+
if (!summaryRow || !coerceInt(summaryRow.turn_count)) return null;
|
|
393
|
+
|
|
394
|
+
const modelRows = appendCostToRows(
|
|
395
|
+
database
|
|
396
|
+
.prepare(`
|
|
397
|
+
SELECT
|
|
398
|
+
provider,
|
|
399
|
+
model,
|
|
400
|
+
COUNT(*) AS turn_count,
|
|
401
|
+
SUM(input_tokens) AS input_tokens,
|
|
402
|
+
SUM(output_tokens) AS output_tokens,
|
|
403
|
+
SUM(cache_read_tokens) AS cache_read_tokens,
|
|
404
|
+
SUM(cache_write_tokens) AS cache_write_tokens,
|
|
405
|
+
SUM(total_tokens) AS total_tokens
|
|
406
|
+
FROM usage_events
|
|
407
|
+
WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
|
|
408
|
+
GROUP BY provider, model
|
|
409
|
+
ORDER BY total_tokens DESC
|
|
410
|
+
`)
|
|
411
|
+
.all({ $sessionRef: safeSessionRef }),
|
|
412
|
+
).map((row) => ({
|
|
413
|
+
provider: row.provider,
|
|
414
|
+
model: row.model,
|
|
415
|
+
turnCount: coerceInt(row.turn_count),
|
|
416
|
+
inputTokens: row.inputTokens,
|
|
417
|
+
outputTokens: row.outputTokens,
|
|
418
|
+
cacheReadTokens: row.cacheReadTokens,
|
|
419
|
+
cacheWriteTokens: row.cacheWriteTokens,
|
|
420
|
+
totalTokens: row.totalTokens,
|
|
421
|
+
totalCost: row.totalCost,
|
|
422
|
+
inputCost: row.inputCost,
|
|
423
|
+
outputCost: row.outputCost,
|
|
424
|
+
cacheReadCost: row.cacheReadCost,
|
|
425
|
+
cacheWriteCost: row.cacheWriteCost,
|
|
426
|
+
pricingFound: row.pricingFound,
|
|
427
|
+
}));
|
|
428
|
+
|
|
429
|
+
const toolRows = database
|
|
430
|
+
.prepare(`
|
|
431
|
+
SELECT
|
|
432
|
+
tool_name,
|
|
433
|
+
COUNT(*) AS call_count,
|
|
434
|
+
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) AS success_count,
|
|
435
|
+
SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) AS error_count,
|
|
436
|
+
AVG(duration_ms) AS avg_duration_ms,
|
|
437
|
+
MIN(duration_ms) AS min_duration_ms,
|
|
438
|
+
MAX(duration_ms) AS max_duration_ms
|
|
439
|
+
FROM tool_events
|
|
440
|
+
WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
|
|
441
|
+
GROUP BY tool_name
|
|
442
|
+
ORDER BY call_count DESC
|
|
443
|
+
`)
|
|
444
|
+
.all({ $sessionRef: safeSessionRef })
|
|
445
|
+
.map((row) => {
|
|
446
|
+
const callCount = coerceInt(row.call_count);
|
|
447
|
+
const successCount = coerceInt(row.success_count);
|
|
448
|
+
const errorCount = coerceInt(row.error_count);
|
|
449
|
+
return {
|
|
450
|
+
toolName: row.tool_name,
|
|
451
|
+
callCount,
|
|
452
|
+
successCount,
|
|
453
|
+
errorCount,
|
|
454
|
+
errorRate: callCount > 0 ? errorCount / callCount : 0,
|
|
455
|
+
avgDurationMs: Number(row.avg_duration_ms || 0),
|
|
456
|
+
minDurationMs: coerceInt(row.min_duration_ms),
|
|
457
|
+
maxDurationMs: coerceInt(row.max_duration_ms),
|
|
458
|
+
};
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const firstActivityMs = coerceInt(summaryRow.first_activity_ms);
|
|
462
|
+
const lastActivityMs = coerceInt(summaryRow.last_activity_ms);
|
|
463
|
+
const totalCost = modelRows.reduce(
|
|
464
|
+
(sum, modelRow) => sum + Number(modelRow.totalCost || 0),
|
|
465
|
+
0,
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
sessionId: safeSessionRef,
|
|
470
|
+
sessionKey: String(summaryRow.session_key || ""),
|
|
471
|
+
rawSessionId: String(summaryRow.session_id || ""),
|
|
472
|
+
firstActivityMs,
|
|
473
|
+
lastActivityMs,
|
|
474
|
+
durationMs: Math.max(0, lastActivityMs - firstActivityMs),
|
|
475
|
+
turnCount: coerceInt(summaryRow.turn_count),
|
|
476
|
+
inputTokens: coerceInt(summaryRow.input_tokens),
|
|
477
|
+
outputTokens: coerceInt(summaryRow.output_tokens),
|
|
478
|
+
cacheReadTokens: coerceInt(summaryRow.cache_read_tokens),
|
|
479
|
+
cacheWriteTokens: coerceInt(summaryRow.cache_write_tokens),
|
|
480
|
+
totalTokens: coerceInt(summaryRow.total_tokens),
|
|
481
|
+
totalCost,
|
|
482
|
+
modelBreakdown: modelRows,
|
|
483
|
+
toolUsage: toolRows,
|
|
484
|
+
};
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const downsamplePoints = (points, maxPoints) => {
|
|
488
|
+
if (points.length <= maxPoints) return points;
|
|
489
|
+
const stride = Math.ceil(points.length / maxPoints);
|
|
490
|
+
const sampled = [];
|
|
491
|
+
for (let index = 0; index < points.length; index += stride) {
|
|
492
|
+
sampled.push(points[index]);
|
|
493
|
+
}
|
|
494
|
+
const lastPoint = points[points.length - 1];
|
|
495
|
+
if (sampled[sampled.length - 1]?.timestamp !== lastPoint.timestamp) {
|
|
496
|
+
sampled.push(lastPoint);
|
|
497
|
+
}
|
|
498
|
+
return sampled;
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const getSessionTimeSeries = ({ sessionId, maxPoints = kDefaultMaxPoints }) => {
|
|
502
|
+
const safeSessionRef = String(sessionId || "").trim();
|
|
503
|
+
if (!safeSessionRef) return { sessionId: safeSessionRef, points: [] };
|
|
504
|
+
const database = ensureDb();
|
|
505
|
+
const rows = database
|
|
506
|
+
.prepare(`
|
|
507
|
+
SELECT
|
|
508
|
+
timestamp,
|
|
509
|
+
session_key,
|
|
510
|
+
session_id,
|
|
511
|
+
model,
|
|
512
|
+
input_tokens,
|
|
513
|
+
output_tokens,
|
|
514
|
+
cache_read_tokens,
|
|
515
|
+
cache_write_tokens,
|
|
516
|
+
total_tokens
|
|
517
|
+
FROM usage_events
|
|
518
|
+
WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
|
|
519
|
+
ORDER BY timestamp ASC
|
|
520
|
+
`)
|
|
521
|
+
.all({ $sessionRef: safeSessionRef });
|
|
522
|
+
let cumulativeTokens = 0;
|
|
523
|
+
let cumulativeCost = 0;
|
|
524
|
+
const points = rows.map((row) => {
|
|
525
|
+
const inputTokens = coerceInt(row.input_tokens);
|
|
526
|
+
const outputTokens = coerceInt(row.output_tokens);
|
|
527
|
+
const cacheReadTokens = coerceInt(row.cache_read_tokens);
|
|
528
|
+
const cacheWriteTokens = coerceInt(row.cache_write_tokens);
|
|
529
|
+
const totalTokens =
|
|
530
|
+
coerceInt(row.total_tokens) ||
|
|
531
|
+
inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
532
|
+
const cost = deriveCostBreakdown({
|
|
533
|
+
inputTokens,
|
|
534
|
+
outputTokens,
|
|
535
|
+
cacheReadTokens,
|
|
536
|
+
cacheWriteTokens,
|
|
537
|
+
model: row.model,
|
|
538
|
+
});
|
|
539
|
+
cumulativeTokens += totalTokens;
|
|
540
|
+
cumulativeCost += cost.totalCost;
|
|
541
|
+
return {
|
|
542
|
+
timestamp: coerceInt(row.timestamp),
|
|
543
|
+
sessionKey: String(row.session_key || ""),
|
|
544
|
+
rawSessionId: String(row.session_id || ""),
|
|
545
|
+
model: String(row.model || ""),
|
|
546
|
+
inputTokens,
|
|
547
|
+
outputTokens,
|
|
548
|
+
cacheReadTokens,
|
|
549
|
+
cacheWriteTokens,
|
|
550
|
+
totalTokens,
|
|
551
|
+
cost: cost.totalCost,
|
|
552
|
+
cumulativeTokens,
|
|
553
|
+
cumulativeCost,
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
const safeMaxPoints = clampInt(maxPoints, 10, kMaxMaxPoints, kDefaultMaxPoints);
|
|
557
|
+
return {
|
|
558
|
+
sessionId: safeSessionRef,
|
|
559
|
+
points: downsamplePoints(points, safeMaxPoints),
|
|
560
|
+
};
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
module.exports = {
|
|
564
|
+
initUsageDb,
|
|
565
|
+
getDailySummary,
|
|
566
|
+
getSessionsList,
|
|
567
|
+
getSessionDetail,
|
|
568
|
+
getSessionTimeSeries,
|
|
569
|
+
kGlobalModelPricing,
|
|
570
|
+
};
|
package/lib/server.js
CHANGED
|
@@ -36,6 +36,13 @@ const {
|
|
|
36
36
|
insertWatchdogEvent,
|
|
37
37
|
getRecentEvents,
|
|
38
38
|
} = require("./server/watchdog-db");
|
|
39
|
+
const {
|
|
40
|
+
initUsageDb,
|
|
41
|
+
getDailySummary,
|
|
42
|
+
getSessionsList,
|
|
43
|
+
getSessionDetail,
|
|
44
|
+
getSessionTimeSeries,
|
|
45
|
+
} = require("./server/usage-db");
|
|
39
46
|
const { createWebhookMiddleware } = require("./server/webhook-middleware");
|
|
40
47
|
const {
|
|
41
48
|
readEnvFile,
|
|
@@ -87,6 +94,7 @@ const { registerProxyRoutes } = require("./server/routes/proxy");
|
|
|
87
94
|
const { registerTelegramRoutes } = require("./server/routes/telegram");
|
|
88
95
|
const { registerWebhookRoutes } = require("./server/routes/webhooks");
|
|
89
96
|
const { registerWatchdogRoutes } = require("./server/routes/watchdog");
|
|
97
|
+
const { registerUsageRoutes } = require("./server/routes/usage");
|
|
90
98
|
|
|
91
99
|
const { PORT, GATEWAY_URL, kTrustProxyHops, SETUP_API_PREFIXES } = constants;
|
|
92
100
|
|
|
@@ -149,6 +157,9 @@ initWatchdogDb({
|
|
|
149
157
|
rootDir: constants.kRootDir,
|
|
150
158
|
pruneDays: constants.kWatchdogLogRetentionDays,
|
|
151
159
|
});
|
|
160
|
+
initUsageDb({
|
|
161
|
+
rootDir: constants.kRootDir,
|
|
162
|
+
});
|
|
152
163
|
const webhookMiddleware = createWebhookMiddleware({
|
|
153
164
|
gatewayUrl: constants.GATEWAY_URL,
|
|
154
165
|
insertRequest,
|
|
@@ -256,6 +267,7 @@ registerTelegramRoutes({
|
|
|
256
267
|
app,
|
|
257
268
|
telegramApi,
|
|
258
269
|
syncPromptFiles: doSyncPromptFiles,
|
|
270
|
+
shellCmd,
|
|
259
271
|
});
|
|
260
272
|
registerWebhookRoutes({
|
|
261
273
|
app,
|
|
@@ -278,6 +290,14 @@ registerWatchdogRoutes({
|
|
|
278
290
|
getRecentEvents,
|
|
279
291
|
readLogTail,
|
|
280
292
|
});
|
|
293
|
+
registerUsageRoutes({
|
|
294
|
+
app,
|
|
295
|
+
requireAuth,
|
|
296
|
+
getDailySummary,
|
|
297
|
+
getSessionsList,
|
|
298
|
+
getSessionDetail,
|
|
299
|
+
getSessionTimeSeries,
|
|
300
|
+
});
|
|
281
301
|
registerProxyRoutes({
|
|
282
302
|
app,
|
|
283
303
|
proxy,
|
|
@@ -294,7 +314,7 @@ server.on("upgrade", (req, socket, head) => {
|
|
|
294
314
|
);
|
|
295
315
|
if (requestUrl.pathname.startsWith("/openclaw")) {
|
|
296
316
|
const upgradeReq = {
|
|
297
|
-
|
|
317
|
+
headers: req.headers,
|
|
298
318
|
path: requestUrl.pathname,
|
|
299
319
|
query: Object.fromEntries(requestUrl.searchParams.entries()),
|
|
300
320
|
};
|