opencode-tokenwatch 0.1.0 → 0.3.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 CHANGED
@@ -20,6 +20,10 @@ async function queryDb(sql) {
20
20
  function escapeSql(value) {
21
21
  return value.replace(/'/g, "''");
22
22
  }
23
+ /** 校验日期格式必须为 YYYY-MM-DD,防止格式异常字符串进入 SQL */
24
+ function isValidDate(s) {
25
+ return /^\d{4}-\d{2}-\d{2}$/.test(s);
26
+ }
23
27
  function messageWhere(filters) {
24
28
  const where = [
25
29
  "json_extract(m.data, '$.role') = 'assistant'",
@@ -31,10 +35,13 @@ function messageWhere(filters) {
31
35
  where.push(`coalesce(json_extract(m.data, '$.providerID'), '') = '${escapeSql(filters.provider)}'`);
32
36
  if (filters.model)
33
37
  where.push(`coalesce(json_extract(m.data, '$.modelID'), '') = '${escapeSql(filters.model)}'`);
34
- if (filters.startDate)
35
- where.push(`date(m.time_created / 1000, 'unixepoch', 'localtime') >= '${escapeSql(filters.startDate)}'`);
36
- if (filters.endDate)
37
- where.push(`date(m.time_created / 1000, 'unixepoch', 'localtime') <= '${escapeSql(filters.endDate)}'`);
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
+ }
38
45
  return where.join(" AND ");
39
46
  }
40
47
  function parseList(value) {
@@ -83,41 +90,41 @@ export async function getCurrentSessionStats(sessionId) {
83
90
  return getSummary(filters);
84
91
  }
85
92
  export async function getSummary(filters = {}) {
86
- const sql = `
87
- SELECT
88
- group_concat(distinct coalesce(json_extract(m.data, '$.modelID'), 'unknown')) as models_used,
89
- group_concat(distinct coalesce(json_extract(m.data, '$.providerID'), 'unknown')) as providers_used,
90
- count(*) as request_count,
91
- sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
92
- sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
93
- sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
94
- sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
95
- sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
96
- sum(coalesce(json_extract(m.data, '$.tokens.cache.write'), 0)) as cache_write,
97
- sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
98
- FROM message m
99
- WHERE ${messageWhere(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)}
100
107
  `.trim();
101
108
  const rows = await queryDb(sql);
102
109
  return toSessionTokenData(rows[0]);
103
110
  }
104
111
  export async function getModelBreakdown(filters = {}) {
105
- const sql = `
106
- SELECT
107
- coalesce(json_extract(m.data, '$.providerID'), 'unknown') as provider,
108
- coalesce(json_extract(m.data, '$.modelID'), 'unknown') as model,
109
- count(*) as requests,
110
- count(distinct m.session_id) as sessions,
111
- sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
112
- sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
113
- sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
114
- sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
115
- sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
116
- sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
117
- FROM message m
118
- WHERE ${messageWhere(filters)}
119
- GROUP BY provider, model
120
- ORDER BY total_tokens DESC
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
121
128
  `.trim();
122
129
  const rows = await queryDb(sql);
123
130
  return rows.map((row) => ({
@@ -134,21 +141,21 @@ ORDER BY total_tokens DESC
134
141
  }));
135
142
  }
136
143
  export async function getProviderBreakdown(filters = {}) {
137
- const sql = `
138
- SELECT
139
- coalesce(json_extract(m.data, '$.providerID'), 'unknown') as provider,
140
- count(*) as requests,
141
- count(distinct m.session_id) as sessions,
142
- sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
143
- sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
144
- sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
145
- sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
146
- sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
147
- sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
148
- FROM message m
149
- WHERE ${messageWhere(filters)}
150
- GROUP BY provider
151
- ORDER BY total_tokens DESC
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
152
159
  `.trim();
153
160
  const rows = await queryDb(sql);
154
161
  return rows.map((row) => ({
@@ -165,22 +172,22 @@ ORDER BY total_tokens DESC
165
172
  }
166
173
  export async function getDailyBreakdown(filters = {}) {
167
174
  const limit = filters.limit ?? 30;
168
- const sql = `
169
- SELECT
170
- date(m.time_created / 1000, 'unixepoch', 'localtime') as day,
171
- count(*) as requests,
172
- count(distinct m.session_id) as sessions,
173
- sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
174
- sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
175
- sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
176
- sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
177
- sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
178
- sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost
179
- FROM message m
180
- WHERE ${messageWhere(filters)}
181
- GROUP BY day
182
- ORDER BY day DESC
183
- LIMIT ${Math.max(1, limit)}
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)}
184
191
  `.trim();
185
192
  const rows = await queryDb(sql);
186
193
  return rows.map((row) => ({
@@ -197,26 +204,26 @@ LIMIT ${Math.max(1, limit)}
197
204
  }
198
205
  export async function getSessionBreakdown(filters = {}) {
199
206
  const limit = filters.limit ?? 15;
200
- const sql = `
201
- SELECT
202
- s.id as session_id,
203
- s.title as title,
204
- coalesce(json_extract(m.data, '$.providerID'), json_extract(s.model, '$.providerID'), 'unknown') as provider,
205
- coalesce(json_extract(m.data, '$.modelID'), json_extract(s.model, '$.id'), 'unknown') as model,
206
- count(*) as requests,
207
- sum(coalesce(json_extract(m.data, '$.tokens.total'), 0)) as total_tokens,
208
- sum(coalesce(json_extract(m.data, '$.tokens.input'), 0)) as input_tokens,
209
- sum(coalesce(json_extract(m.data, '$.tokens.output'), 0)) as output_tokens,
210
- sum(coalesce(json_extract(m.data, '$.tokens.reasoning'), 0)) as reasoning_tokens,
211
- sum(coalesce(json_extract(m.data, '$.tokens.cache.read'), 0)) as cache_read,
212
- sum(coalesce(json_extract(m.data, '$.cost'), 0)) as total_cost,
213
- date(max(m.time_created) / 1000, 'unixepoch', 'localtime') as day
214
- FROM message m
215
- JOIN session s ON s.id = m.session_id
216
- WHERE ${messageWhere(filters)}
217
- GROUP BY s.id, s.title, provider, model
218
- ORDER BY max(m.time_created) DESC
219
- LIMIT ${Math.max(1, limit)}
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)}
220
227
  `.trim();
221
228
  const rows = await queryDb(sql);
222
229
  return rows.map((row) => ({
@@ -235,34 +242,86 @@ LIMIT ${Math.max(1, limit)}
235
242
  }));
236
243
  }
237
244
  export async function getAvailableModels() {
238
- const sql = `
239
- SELECT distinct coalesce(json_extract(m.data, '$.modelID'), 'unknown') as value
240
- FROM message m
241
- WHERE ${messageWhere({})}
242
- ORDER BY value ASC
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
243
250
  `.trim();
244
251
  const rows = await queryDb(sql);
245
252
  return rows.map((row) => row.value ?? "unknown");
246
253
  }
247
254
  export async function getAvailableProviders() {
248
- const sql = `
249
- SELECT distinct coalesce(json_extract(m.data, '$.providerID'), 'unknown') as value
250
- FROM message m
251
- WHERE ${messageWhere({})}
252
- ORDER BY value ASC
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
253
260
  `.trim();
254
261
  const rows = await queryDb(sql);
255
262
  return rows.map((row) => row.value ?? "unknown");
256
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
+ }
257
315
  export async function getUsageReport(filters = {}) {
258
- const [summary, models, providers, daily, sessions] = await Promise.all([
316
+ const [summary, models, providers, daily, sessions, errors] = await Promise.all([
259
317
  getSummary(filters),
260
318
  getModelBreakdown(filters),
261
319
  getProviderBreakdown(filters),
262
320
  getDailyBreakdown(filters),
263
321
  getSessionBreakdown(filters),
322
+ getErrorStats(filters),
264
323
  ]);
265
- return { filters, summary, models, providers, daily, sessions };
324
+ return { filters, summary, models, providers, daily, sessions, errors };
266
325
  }
267
326
  function csvEscape(value) {
268
327
  const text = String(value);
@@ -0,0 +1,22 @@
1
+ import type { TuiPluginApi, TuiTheme } from "@opencode-ai/plugin/tui";
2
+ import type { PerfTracker } from "./perf-tracker.js";
3
+ import type { TokenMessage } from "./tui.jsx";
4
+ export interface SidebarConfig {
5
+ sidebar: {
6
+ showPerformance: boolean;
7
+ showPricing: boolean;
8
+ showTokenDistribution: boolean;
9
+ showTrend: boolean;
10
+ };
11
+ language: "zh" | "en" | "auto";
12
+ }
13
+ export declare function loadConfig(api: TuiPluginApi): SidebarConfig;
14
+ interface TokenWatchPanelProps {
15
+ api: TuiPluginApi;
16
+ theme: TuiTheme;
17
+ perfTracker: PerfTracker;
18
+ messages: readonly any[];
19
+ allTokenMessages: TokenMessage[];
20
+ }
21
+ export declare function TokenWatchPanel(props: TokenWatchPanelProps): any;
22
+ export {};