opencode-tokenwatch 0.4.0 → 0.5.0

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/dist/queries.js DELETED
@@ -1,393 +0,0 @@
1
- import { exec } from "node:child_process";
2
- function execAsync(cmd) {
3
- return new Promise((resolve, reject) => {
4
- exec(cmd, { windowsHide: true, timeout: 30000 }, (error, stdout, stderr) => {
5
- if (error)
6
- reject(Object.assign(error, { stderr }));
7
- else
8
- resolve({ stdout, stderr });
9
- });
10
- });
11
- }
12
- async function queryDb(sql) {
13
- const flatSql = sql.replace(/\s+/g, " ").trim();
14
- const { stdout, stderr } = await execAsync(`opencode db ${JSON.stringify(flatSql)} --format json`);
15
- if (stderr)
16
- throw new Error(stderr.trim());
17
- const parsed = JSON.parse(stdout.trim());
18
- return Array.isArray(parsed) ? parsed : parsed.data ?? [];
19
- }
20
- function escapeSql(value) {
21
- return value.replace(/'/g, "''");
22
- }
23
- /** 校验日期格式必须为 YYYY-MM-DD,防止格式异常字符串进入 SQL */
24
- function isValidDate(s) {
25
- return /^\d{4}-\d{2}-\d{2}$/.test(s);
26
- }
27
- function messageWhere(filters) {
28
- const where = [
29
- "json_extract(m.data, '$.role') = 'assistant'",
30
- "coalesce(json_extract(m.data, '$.tokens.total'), 0) > 0",
31
- ];
32
- if (filters.sessionId)
33
- where.push(`m.session_id = '${escapeSql(filters.sessionId)}'`);
34
- if (filters.provider)
35
- where.push(`coalesce(json_extract(m.data, '$.providerID'), '') = '${escapeSql(filters.provider)}'`);
36
- if (filters.model)
37
- where.push(`coalesce(json_extract(m.data, '$.modelID'), '') = '${escapeSql(filters.model)}'`);
38
- // Risk fix: 日期参数先验证格式(YYYY-MM-DD),格式不符则忽略该过滤条件
39
- if (filters.startDate && isValidDate(filters.startDate)) {
40
- where.push(`date(m.time_created / 1000, 'unixepoch', 'localtime') >= '${filters.startDate}'`);
41
- }
42
- if (filters.endDate && isValidDate(filters.endDate)) {
43
- where.push(`date(m.time_created / 1000, 'unixepoch', 'localtime') <= '${filters.endDate}'`);
44
- }
45
- return where.join(" AND ");
46
- }
47
- function parseList(value) {
48
- if (!value)
49
- return [];
50
- return value.split(",").map((item) => item.trim()).filter(Boolean);
51
- }
52
- function toSessionTokenData(row) {
53
- const models = parseList(row?.models_used);
54
- const providers = parseList(row?.providers_used);
55
- return {
56
- model: models.length === 1 ? models[0] : "",
57
- provider: providers.length === 1 ? providers[0] : "",
58
- modelsUsed: models,
59
- totalTokens: row?.total_tokens ?? 0,
60
- inputTokens: row?.input_tokens ?? 0,
61
- outputTokens: row?.output_tokens ?? 0,
62
- reasoningTokens: row?.reasoning_tokens ?? 0,
63
- cacheRead: row?.cache_read ?? 0,
64
- cacheWrite: row?.cache_write ?? 0,
65
- totalCost: row?.total_cost ?? 0,
66
- requestCount: row?.request_count ?? 0,
67
- };
68
- }
69
- export function getPresetRange(preset) {
70
- if (preset === "all")
71
- return {};
72
- const end = new Date();
73
- const start = new Date(end);
74
- if (preset === "7d")
75
- start.setDate(end.getDate() - 6);
76
- if (preset === "30d")
77
- start.setDate(end.getDate() - 29);
78
- if (preset === "month")
79
- start.setDate(1);
80
- const format = (date) => {
81
- const year = date.getFullYear();
82
- const month = String(date.getMonth() + 1).padStart(2, "0");
83
- const day = String(date.getDate()).padStart(2, "0");
84
- return `${year}-${month}-${day}`;
85
- };
86
- return { startDate: format(start), endDate: format(end) };
87
- }
88
- export async function getCurrentSessionStats(sessionId) {
89
- const filters = sessionId ? { sessionId } : {};
90
- return getSummary(filters);
91
- }
92
- export async function getSummary(filters = {}) {
93
- const sql = `
94
- SELECT
95
- group_concat(distinct coalesce(json_extract(m.data, '$.modelID'), 'unknown')) as models_used,
96
- group_concat(distinct coalesce(json_extract(m.data, '$.providerID'), 'unknown')) as providers_used,
97
- count(*) as request_count,
98
- sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
99
- sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
100
- sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
101
- sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
102
- sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
103
- sum(coalesce(json_extract(m.data, '$.tokens.cache.write'), 0)) as cache_write,
104
- sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
105
- FROM message m
106
- WHERE ${messageWhere(filters)}
107
- `.trim();
108
- const rows = await queryDb(sql);
109
- return toSessionTokenData(rows[0]);
110
- }
111
- export async function getModelBreakdown(filters = {}) {
112
- const sql = `
113
- SELECT
114
- coalesce(json_extract(m.data, '$.providerID'), 'unknown') as provider,
115
- coalesce(json_extract(m.data, '$.modelID'), 'unknown') as model,
116
- count(*) as requests,
117
- count(distinct m.session_id) as sessions,
118
- sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
119
- sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
120
- sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
121
- sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
122
- sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
123
- sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
124
- FROM message m
125
- WHERE ${messageWhere(filters)}
126
- GROUP BY provider, model
127
- ORDER BY total_tokens DESC
128
- `.trim();
129
- const rows = await queryDb(sql);
130
- return rows.map((row) => ({
131
- provider: row.provider ?? "unknown",
132
- model: row.model ?? "unknown",
133
- requests: row.requests ?? 0,
134
- sessions: row.sessions ?? 0,
135
- totalTokens: row.total_tokens ?? 0,
136
- inputTokens: row.input_tokens ?? 0,
137
- outputTokens: row.output_tokens ?? 0,
138
- reasoningTokens: row.reasoning_tokens ?? 0,
139
- cacheRead: row.cache_read ?? 0,
140
- totalCost: row.total_cost ?? 0,
141
- }));
142
- }
143
- export async function getProviderBreakdown(filters = {}) {
144
- const sql = `
145
- SELECT
146
- coalesce(json_extract(m.data, '$.providerID'), 'unknown') as provider,
147
- count(*) as requests,
148
- count(distinct m.session_id) as sessions,
149
- sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
150
- sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
151
- sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
152
- sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
153
- sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
154
- sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
155
- FROM message m
156
- WHERE ${messageWhere(filters)}
157
- GROUP BY provider
158
- ORDER BY total_tokens DESC
159
- `.trim();
160
- const rows = await queryDb(sql);
161
- return rows.map((row) => ({
162
- provider: row.provider ?? "unknown",
163
- requests: row.requests ?? 0,
164
- sessions: row.sessions ?? 0,
165
- totalTokens: row.total_tokens ?? 0,
166
- inputTokens: row.input_tokens ?? 0,
167
- outputTokens: row.output_tokens ?? 0,
168
- reasoningTokens: row.reasoning_tokens ?? 0,
169
- cacheRead: row.cache_read ?? 0,
170
- totalCost: row.total_cost ?? 0,
171
- }));
172
- }
173
- export async function getDailyBreakdown(filters = {}) {
174
- const limit = filters.limit ?? 30;
175
- const sql = `
176
- SELECT
177
- date(m.time_created / 1000, 'unixepoch', 'localtime') as day,
178
- count(*) as requests,
179
- count(distinct m.session_id) as sessions,
180
- sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
181
- sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
182
- sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
183
- sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
184
- sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
185
- sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
186
- FROM message m
187
- WHERE ${messageWhere(filters)}
188
- GROUP BY day
189
- ORDER BY day DESC
190
- LIMIT ${Math.max(1, limit)}
191
- `.trim();
192
- const rows = await queryDb(sql);
193
- return rows.map((row) => ({
194
- day: row.day ?? "",
195
- requests: row.requests ?? 0,
196
- sessions: row.sessions ?? 0,
197
- totalTokens: row.total_tokens ?? 0,
198
- inputTokens: row.input_tokens ?? 0,
199
- outputTokens: row.output_tokens ?? 0,
200
- reasoningTokens: row.reasoning_tokens ?? 0,
201
- cacheRead: row.cache_read ?? 0,
202
- totalCost: row.total_cost ?? 0,
203
- }));
204
- }
205
- export async function getSessionBreakdown(filters = {}) {
206
- const limit = filters.limit ?? 15;
207
- const sql = `
208
- SELECT
209
- s.id as session_id,
210
- s.title as title,
211
- coalesce(json_extract(m.data, '$.providerID'), json_extract(s.model, '$.providerID'), 'unknown') as provider,
212
- coalesce(json_extract(m.data, '$.modelID'), json_extract(s.model, '$.id'), 'unknown') as model,
213
- count(*) as requests,
214
- sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
215
- sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
216
- sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
217
- sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
218
- sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
219
- sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost,
220
- date(max(m.time_created) / 1000, 'unixepoch', 'localtime') as day
221
- FROM message m
222
- JOIN session s ON s.id = m.session_id
223
- WHERE ${messageWhere(filters)}
224
- GROUP BY s.id, s.title, provider, model
225
- ORDER BY max(m.time_created) DESC
226
- LIMIT ${Math.max(1, limit)}
227
- `.trim();
228
- const rows = await queryDb(sql);
229
- return rows.map((row) => ({
230
- sessionId: row.session_id ?? "",
231
- title: row.title ?? "(untitled)",
232
- provider: row.provider ?? "unknown",
233
- model: row.model ?? "unknown",
234
- requests: row.requests ?? 0,
235
- totalTokens: row.total_tokens ?? 0,
236
- inputTokens: row.input_tokens ?? 0,
237
- outputTokens: row.output_tokens ?? 0,
238
- reasoningTokens: row.reasoning_tokens ?? 0,
239
- cacheRead: row.cache_read ?? 0,
240
- totalCost: row.total_cost ?? 0,
241
- day: row.day ?? "",
242
- }));
243
- }
244
- export async function getAvailableModels() {
245
- const sql = `
246
- SELECT distinct coalesce(json_extract(m.data, '$.modelID'), 'unknown') as value
247
- FROM message m
248
- WHERE ${messageWhere({})}
249
- ORDER BY value ASC
250
- `.trim();
251
- const rows = await queryDb(sql);
252
- return rows.map((row) => row.value ?? "unknown");
253
- }
254
- export async function getAvailableProviders() {
255
- const sql = `
256
- SELECT distinct coalesce(json_extract(m.data, '$.providerID'), 'unknown') as value
257
- FROM message m
258
- WHERE ${messageWhere({})}
259
- ORDER BY value ASC
260
- `.trim();
261
- const rows = await queryDb(sql);
262
- return rows.map((row) => row.value ?? "unknown");
263
- }
264
- /** 失败请求计数 SQL。1次运行获取成功数+失败数+按模型细分 */
265
- export async function getErrorStats(filters = {}) {
266
- // 构建日期/Session/Provider/Model 过滤条件(不包含 tokens.total > 0 过滤)
267
- const baseConds = [
268
- "json_extract(m.data, '$.role') = 'assistant'",
269
- ];
270
- if (filters.sessionId)
271
- baseConds.push(`m.session_id = '${escapeSql(filters.sessionId)}'`);
272
- if (filters.provider)
273
- baseConds.push(`coalesce(json_extract(m.data, '$.providerID'), '') = '${escapeSql(filters.provider)}'`);
274
- if (filters.model)
275
- baseConds.push(`coalesce(json_extract(m.data, '$.modelID'), '') = '${escapeSql(filters.model)}'`);
276
- if (filters.startDate && isValidDate(filters.startDate)) {
277
- baseConds.push(`date(m.time_created / 1000, 'unixepoch', 'localtime') >= '${filters.startDate}'`);
278
- }
279
- if (filters.endDate && isValidDate(filters.endDate)) {
280
- baseConds.push(`date(m.time_created / 1000, 'unixepoch', 'localtime') <= '${filters.endDate}'`);
281
- }
282
- const baseWhere = baseConds.join(" AND ");
283
- // 按模型细化:同时统计成功和失败请求
284
- const sql = `
285
- SELECT
286
- coalesce(json_extract(m.data, '$.providerID'), 'unknown') as provider,
287
- coalesce(json_extract(m.data, '$.modelID'), 'unknown') as model,
288
- count(*) as total,
289
- sum(CASE WHEN coalesce(json_extract(m.data, '$.tokens.total'), 0) = 0 THEN 1 ELSE 0 END) as failed
290
- FROM message m
291
- WHERE ${baseWhere}
292
- GROUP BY provider, model
293
- ORDER BY failed DESC
294
- `.trim();
295
- try {
296
- const rows = await queryDb(sql);
297
- let successCount = 0, failedCount = 0;
298
- const byModel = rows.map(r => {
299
- const total = r.total ?? 0;
300
- const failed = r.failed ?? 0;
301
- const success = total - failed;
302
- successCount += success;
303
- failedCount += failed;
304
- return { provider: r.provider ?? 'unknown', model: r.model ?? 'unknown', failed, total };
305
- });
306
- const errorRate = (successCount + failedCount) > 0
307
- ? failedCount / (successCount + failedCount)
308
- : 0;
309
- return { successCount, failedCount, errorRate, byModel };
310
- }
311
- catch {
312
- return { successCount: 0, failedCount: 0, errorRate: 0, byModel: [] };
313
- }
314
- }
315
- export async function getUsageReport(filters = {}) {
316
- const [summary, models, providers, daily, sessions, errors] = await Promise.all([
317
- getSummary(filters),
318
- getModelBreakdown(filters),
319
- getProviderBreakdown(filters),
320
- getDailyBreakdown(filters),
321
- getSessionBreakdown(filters),
322
- getErrorStats(filters),
323
- ]);
324
- return { filters, summary, models, providers, daily, sessions, errors };
325
- }
326
- function csvEscape(value) {
327
- const text = String(value);
328
- return /[",\n]/.test(text) ? `"${text.replace(/"/g, "\"\"")}"` : text;
329
- }
330
- export function exportReportAsCsv(report, section) {
331
- if (section === "models") {
332
- const header = ["provider", "model", "requests", "sessions", "totalTokens", "inputTokens", "outputTokens", "reasoningTokens", "cacheRead", "totalCost"];
333
- const rows = report.models.map((item) => [
334
- item.provider,
335
- item.model,
336
- item.requests,
337
- item.sessions,
338
- item.totalTokens,
339
- item.inputTokens,
340
- item.outputTokens,
341
- item.reasoningTokens,
342
- item.cacheRead,
343
- item.totalCost,
344
- ]);
345
- return [header, ...rows].map((row) => row.map(csvEscape).join(",")).join("\n");
346
- }
347
- if (section === "providers") {
348
- const header = ["provider", "requests", "sessions", "totalTokens", "inputTokens", "outputTokens", "reasoningTokens", "cacheRead", "totalCost"];
349
- const rows = report.providers.map((item) => [
350
- item.provider,
351
- item.requests,
352
- item.sessions,
353
- item.totalTokens,
354
- item.inputTokens,
355
- item.outputTokens,
356
- item.reasoningTokens,
357
- item.cacheRead,
358
- item.totalCost,
359
- ]);
360
- return [header, ...rows].map((row) => row.map(csvEscape).join(",")).join("\n");
361
- }
362
- if (section === "daily") {
363
- const header = ["day", "requests", "sessions", "totalTokens", "inputTokens", "outputTokens", "reasoningTokens", "cacheRead", "totalCost"];
364
- const rows = report.daily.map((item) => [
365
- item.day,
366
- item.requests,
367
- item.sessions,
368
- item.totalTokens,
369
- item.inputTokens,
370
- item.outputTokens,
371
- item.reasoningTokens,
372
- item.cacheRead,
373
- item.totalCost,
374
- ]);
375
- return [header, ...rows].map((row) => row.map(csvEscape).join(",")).join("\n");
376
- }
377
- const header = ["day", "sessionId", "title", "provider", "model", "requests", "totalTokens", "inputTokens", "outputTokens", "reasoningTokens", "cacheRead", "totalCost"];
378
- const rows = report.sessions.map((item) => [
379
- item.day,
380
- item.sessionId,
381
- item.title,
382
- item.provider,
383
- item.model,
384
- item.requests,
385
- item.totalTokens,
386
- item.inputTokens,
387
- item.outputTokens,
388
- item.reasoningTokens,
389
- item.cacheRead,
390
- item.totalCost,
391
- ]);
392
- return [header, ...rows].map((row) => row.map(csvEscape).join(",")).join("\n");
393
- }