@jxgame2020/dsh-token-quota 0.1.11 → 0.1.13

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/lib/index.js CHANGED
@@ -225,6 +225,8 @@ var TokenQuotaService = class TokenQuotaService extends Service {
225
225
  checkUpdates = true;
226
226
  /** Cached upgrade availability; recomputed by {@link refreshUpgrade}. */
227
227
  upgrade = null;
228
+ /** Error message from the most recent update check; null = no error. */
229
+ upgradeError = null;
228
230
  /** Timer for the periodic update check. */
229
231
  updateTimer;
230
232
  /** In-flight update check, to coalesce the periodic and manual triggers. */
@@ -243,6 +245,8 @@ var TokenQuotaService = class TokenQuotaService extends Service {
243
245
  disposeRouteLog;
244
246
  /** Disposer for the optional manual-check route (`POST /token-quota/check-updates`). */
245
247
  disposeRouteCheck;
248
+ /** Disposer for the optional clear-log route (`POST /token-quota/clear-log`). */
249
+ disposeRouteClear;
246
250
  /** Disposer for the webServer-arrival watcher when the service mounts later. */
247
251
  disposeRouteWatcher;
248
252
  /**
@@ -353,6 +357,41 @@ var TokenQuotaService = class TokenQuotaService extends Service {
353
357
  });
354
358
  }
355
359
  });
360
+ this.disposeRouteClear = server.register({
361
+ kind: "exact",
362
+ path: "/token-quota/clear-log",
363
+ handler: (req, res) => {
364
+ if (req.method !== "POST") {
365
+ res.writeHead(405);
366
+ res.end();
367
+ return;
368
+ }
369
+ const chunks = [];
370
+ req.on("data", (chunk) => {
371
+ chunks.push(Buffer.from(chunk));
372
+ });
373
+ req.on("end", () => {
374
+ let before = "";
375
+ try {
376
+ const raw = Buffer.concat(chunks).toString("utf8");
377
+ if (raw.length > 0) {
378
+ const payload = JSON.parse(raw);
379
+ before = typeof payload.before === "string" ? payload.before : "";
380
+ }
381
+ } catch {
382
+ res.writeHead(400, { "content-type": "application/json" });
383
+ res.end(JSON.stringify({ error: "bad payload" }));
384
+ return;
385
+ }
386
+ const updated = this.clearLogBefore(before);
387
+ res.writeHead(200, {
388
+ "content-type": "application/json",
389
+ "cache-control": "no-store"
390
+ });
391
+ res.end(JSON.stringify(updated));
392
+ });
393
+ }
394
+ });
356
395
  }
357
396
  /** Query the npm registry and rebuild the cached upgrade info (coalesced). */
358
397
  refreshUpgrade() {
@@ -367,7 +406,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
367
406
  return this.upgradeCheck;
368
407
  }
369
408
  async doRefreshUpgrade() {
370
- const current = "0.1.11";
409
+ const current = "0.1.13";
371
410
  let latest;
372
411
  try {
373
412
  const response = await fetch(REGISTRY_LATEST_URL, {
@@ -376,16 +415,20 @@ var TokenQuotaService = class TokenQuotaService extends Service {
376
415
  });
377
416
  if (!response.ok) {
378
417
  this.upgrade = null;
418
+ this.upgradeError = `registry responded ${response.status} ${response.statusText}`;
379
419
  return;
380
420
  }
381
421
  const manifest = await response.json();
382
422
  latest = typeof manifest.version === "string" ? manifest.version : void 0;
383
- } catch {
423
+ } catch (err) {
384
424
  this.upgrade = null;
425
+ const msg = err instanceof Error ? err.message : String(err);
426
+ this.upgradeError = msg.includes("fetch failed") ? "无法访问 npm registry(网络超时或被墙)" : msg.includes("aborted") ? "请求超时(10s)" : msg;
385
427
  return;
386
428
  }
387
429
  if (latest === void 0 || !isNewer(latest, current)) {
388
430
  this.upgrade = null;
431
+ this.upgradeError = null;
389
432
  return;
390
433
  }
391
434
  const profile = findProfile();
@@ -395,6 +438,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
395
438
  commands,
396
439
  installKind: profile?.installKind ?? "registry"
397
440
  };
441
+ this.upgradeError = null;
398
442
  }
399
443
  /** Start or stop the periodic update check to match the current setting. */
400
444
  syncUpdateChecking() {
@@ -442,6 +486,19 @@ var TokenQuotaService = class TokenQuotaService extends Service {
442
486
  entries.sort((left, right) => right.day.localeCompare(left.day) || left.key.localeCompare(right.key));
443
487
  return { entries };
444
488
  }
489
+ /**
490
+ * Remove all historical cycles whose day portion strictly precedes `before`
491
+ * (format `YYYY-MM-DD`). The current cycle is never cleared. Returns the
492
+ * trimmed log so the client can refresh immediately.
493
+ */
494
+ clearLogBefore(before) {
495
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(before)) return this.readLog();
496
+ const kept = {};
497
+ for (const [cycle, usageByKey] of Object.entries(this.history)) if (cycle.slice(0, 10) >= before) kept[cycle] = usageByKey;
498
+ this.history = kept;
499
+ this.scheduleWrite();
500
+ return this.readLog();
501
+ }
445
502
  /** Read the current snapshot (also used by tests and inspection). */
446
503
  readSnapshot() {
447
504
  this.rollCycleIfNeeded();
@@ -462,7 +519,8 @@ var TokenQuotaService = class TokenQuotaService extends Service {
462
519
  return {
463
520
  day: this.cycle,
464
521
  entries,
465
- upgrade: this.upgrade
522
+ upgrade: this.upgrade,
523
+ upgradeError: this.upgradeError
466
524
  };
467
525
  }
468
526
  /** Today's used tokens for one model key, or `0`. */
@@ -644,6 +702,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
644
702
  if (this.disposeRoute !== void 0) this.disposeRoute();
645
703
  if (this.disposeRouteLog !== void 0) this.disposeRouteLog();
646
704
  if (this.disposeRouteCheck !== void 0) this.disposeRouteCheck();
705
+ if (this.disposeRouteClear !== void 0) this.disposeRouteClear();
647
706
  this.flush();
648
707
  }
649
708
  safeSplit(key) {
@@ -20,6 +20,8 @@ export interface TokenQuotaPanelInjected {
20
20
  setCheckUpdates: (enabled: boolean) => void;
21
21
  /** Ask the Host to check the registry right now, then refresh the snapshot. */
22
22
  checkUpdatesNow: () => void;
23
+ /** Ask the Host to clear log entries before a date, then refresh the log. */
24
+ clearLogBefore: (before: string) => void;
23
25
  }
24
26
  /** Full component props: runtime + store + locale + injected face. */
25
27
  export type TokenQuotaPanelComponentProps = PropsRuntime<'shell.overlay'> & PropsStore<ReturnType<typeof createTokenQuotaPanelStore>> & PropsLocale<'token-quota'> & TokenQuotaPanelInjected;
@@ -28,5 +30,5 @@ export type TokenQuotaPanelComponentProps = PropsRuntime<'shell.overlay'> & Prop
28
30
  * @param props - composed slot props.
29
31
  * @returns the panel element tree.
30
32
  */
31
- export declare function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, useStore, actions, useSessions, }: TokenQuotaPanelComponentProps): import("react").JSX.Element;
33
+ export declare function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, useStore, actions, useSessions, }: TokenQuotaPanelComponentProps): import("react").JSX.Element;
32
34
  //# sourceMappingURL=TokenQuotaPanel.d.ts.map
@@ -86,7 +86,7 @@ function loadPanelPos() {
86
86
  * @param props - composed slot props.
87
87
  * @returns the panel element tree.
88
88
  */
89
- export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, useStore, actions, useSessions, }) {
89
+ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, useStore, actions, useSessions, }) {
90
90
  const [collapsed, setCollapsed] = useState(false);
91
91
  const [drafts, setDrafts] = useState({});
92
92
  const [editingKey, setEditingKey] = useState(null);
@@ -117,10 +117,18 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
117
117
  const upgrade = useStore(s => s.upgrade);
118
118
  const upgradeDismissed = useStore(s => s.upgradeDismissed);
119
119
  const checkingUpdates = useStore(s => s.checkingUpdates);
120
+ const lastCheckResult = useStore(s => s.lastCheckResult);
121
+ const upgradeError = useStore(s => s.upgradeError);
120
122
  const reset = useStore(s => s.reset);
121
123
  const dialogOpen = useStore(s => s.dialogOpen);
122
124
  const logOpen = useStore(s => s.logOpen);
123
125
  const log = useStore(s => s.log);
126
+ const logSearch = useStore(s => s.logSearch);
127
+ const logPage = useStore(s => s.logPage);
128
+ const logPageSize = useStore(s => s.logPageSize);
129
+ const logClearOpen = useStore(s => s.logClearOpen);
130
+ const logClearBefore = useStore(s => s.logClearBefore);
131
+ const logClearing = useStore(s => s.logClearing);
124
132
  const fullNotice = useStore(s => s.fullNotice);
125
133
  const loading = useStore(s => s.loading);
126
134
  const error = useStore(s => s.error);
@@ -136,6 +144,23 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
136
144
  name: model.name,
137
145
  }))).sort((a, b) => a.key.localeCompare(b.key)), [groups]);
138
146
  const isMonitoredKey = (key) => monitored === null || monitored.includes(key);
147
+ // Log dialog: filter by search, then paginate.
148
+ const filteredLogEntries = useMemo(() => {
149
+ if (log === null)
150
+ return [];
151
+ const q = logSearch.trim().toLowerCase();
152
+ if (q === '')
153
+ return log.entries;
154
+ return log.entries.filter(e => e.key.toLowerCase().includes(q)
155
+ || e.provider.toLowerCase().includes(q)
156
+ || e.model.toLowerCase().includes(q));
157
+ }, [log, logSearch]);
158
+ const logTotalPages = Math.max(1, Math.ceil(filteredLogEntries.length / logPageSize));
159
+ const logCurrentPage = Math.min(logPage, logTotalPages - 1);
160
+ const logPageEntries = useMemo(() => {
161
+ const start = logCurrentPage * logPageSize;
162
+ return filteredLogEntries.slice(start, start + logPageSize);
163
+ }, [filteredLogEntries, logCurrentPage, logPageSize]);
139
164
  /** Copy text to the clipboard (clipboard API with execCommand fallback). */
140
165
  const copyToClipboard = async (text) => {
141
166
  try {
@@ -237,6 +262,15 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
237
262
  hour: reset?.hour ?? 0,
238
263
  minute: Number(event.target.value),
239
264
  });
240
- }, children: Array.from({ length: 12 }, (_, i) => i * 5).map(minute => (_jsxs("option", { value: minute, children: [String(minute).padStart(2, '0'), " \u5206"] }, minute))) })] })] }), _jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.radioRow, children: [_jsx("input", { type: "checkbox", checked: checkUpdates, onChange: (event) => { setCheckUpdates(event.target.checked); } }), _jsx("span", { children: t('checkUpdatesLabel') })] }), _jsx("div", { className: css.checkUpdatesRow, children: _jsx("button", { type: "button", className: css.checkBtn, disabled: checkingUpdates, onClick: checkUpdatesNow, children: checkingUpdates ? t('checkingUpdates') : t('checkUpdatesNow') }) })] })] })), logOpen && (_jsxs("div", { ref: logRef, className: `${css.dialog} ${css.logDialog}`, style: logPos !== null ? { left: logPos.x, top: logPos.y, transform: 'none' } : undefined, children: [_jsxs("div", { className: css.logHeader, onPointerDown: (event) => { beginDrag(event, logRef.current, setLogPos); }, children: [_jsx("div", { className: css.dialogTitle, children: t('logsTitle') }), _jsx("button", { type: "button", className: css.dialogClose, title: t('close'), onClick: () => { actions.setLogOpen(false); }, children: "\u00D7" })] }), log !== null && log.entries.length === 0 && (_jsx("div", { className: css.notice, children: t('logEmpty') })), _jsx("div", { className: css.logScroll, children: _jsxs("table", { className: css.logTable, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: t('logDay') }), _jsx("th", { children: t('logModel') }), _jsx("th", { className: css.logUsedCol, children: t('logUsed') })] }) }), _jsx("tbody", { children: log?.entries.map(entry => (_jsxs("tr", { children: [_jsx("td", { className: css.logDayCol, children: entry.day }), _jsx("td", { className: css.logModelCol, title: entry.key, children: entry.key }), _jsx("td", { className: css.logUsedCol, children: formatTokens(entry.used) })] }, `${entry.day}/${entry.key}`))) })] }) })] }))] }));
265
+ }, children: Array.from({ length: 12 }, (_, i) => i * 5).map(minute => (_jsxs("option", { value: minute, children: [String(minute).padStart(2, '0'), " \u5206"] }, minute))) })] })] }), _jsxs("div", { className: css.dialogSection, children: [_jsxs("div", { className: css.checkUpdatesRow, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: checkUpdates, onChange: (event) => { setCheckUpdates(event.target.checked); } }), _jsx("span", { children: t('checkUpdatesLabel') })] }), _jsx("button", { type: "button", className: css.checkBtn, disabled: checkingUpdates, onClick: checkUpdatesNow, children: checkingUpdates
266
+ ? t('checkingUpdates')
267
+ : lastCheckResult === 'up-to-date'
268
+ ? t('checkUpToDate')
269
+ : lastCheckResult === 'error'
270
+ ? t('checkFailed')
271
+ : t('checkUpdatesNow') })] }), lastCheckResult === 'error' && upgradeError !== null && (_jsx("div", { className: css.checkErrorHint, role: "alert", children: upgradeError }))] })] })), logOpen && (_jsxs("div", { ref: logRef, className: `${css.dialog} ${css.logDialog}`, style: logPos !== null ? { left: logPos.x, top: logPos.y, transform: 'none' } : undefined, children: [_jsxs("div", { className: css.logHeader, onPointerDown: (event) => { beginDrag(event, logRef.current, setLogPos); }, children: [_jsx("div", { className: css.dialogTitle, children: t('logsTitle') }), _jsxs("div", { className: css.logToolbar, onPointerDown: (event) => { event.stopPropagation(); }, children: [_jsx("input", { type: "search", className: css.logSearch, placeholder: t('logSearchPlaceholder'), value: logSearch, onChange: (event) => { actions.setLogSearch(event.target.value); } }), _jsx("button", { type: "button", className: css.logClearBtn, onClick: () => { actions.setLogClearOpen(true); }, children: t('logClear') })] }), _jsx("button", { type: "button", className: css.dialogClose, title: t('close'), onClick: () => { actions.setLogOpen(false); }, children: "\u00D7" })] }), log !== null && filteredLogEntries.length === 0 && (_jsx("div", { className: css.notice, children: logSearch.trim() !== '' ? t('logNoMatch') : t('logEmpty') })), _jsx("div", { className: css.logScroll, children: _jsxs("table", { className: css.logTable, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: t('logDay') }), _jsx("th", { children: t('logModel') }), _jsx("th", { className: css.logUsedCol, children: t('logUsed') })] }) }), _jsx("tbody", { children: logPageEntries.map(entry => (_jsxs("tr", { children: [_jsx("td", { className: css.logDayCol, children: entry.day }), _jsx("td", { className: css.logModelCol, title: entry.key, children: entry.key }), _jsx("td", { className: css.logUsedCol, children: formatTokens(entry.used) })] }, `${entry.day}/${entry.key}`))) })] }) }), _jsxs("div", { className: css.logPager, children: [_jsx("span", { className: css.logPagerInfo, children: t('logPagerInfo')
272
+ .replace('{from}', String(logCurrentPage * logPageSize + 1))
273
+ .replace('{to}', String(Math.min((logCurrentPage + 1) * logPageSize, filteredLogEntries.length)))
274
+ .replace('{total}', String(filteredLogEntries.length)) }), _jsxs("div", { className: css.logPagerControls, children: [_jsx("button", { type: "button", className: css.logPagerBtn, disabled: logCurrentPage <= 0, onClick: () => { actions.setLogPage(logCurrentPage - 1); }, children: "\u2039" }), _jsxs("span", { className: css.logPagerPages, children: [logCurrentPage + 1, " / ", logTotalPages] }), _jsx("button", { type: "button", className: css.logPagerBtn, disabled: logCurrentPage >= logTotalPages - 1, onClick: () => { actions.setLogPage(logCurrentPage + 1); }, children: "\u203A" }), _jsx("select", { className: css.logPageSizeSelect, value: logPageSize, onChange: (event) => { actions.setLogPageSize(Number(event.target.value)); }, children: [10, 15, 20, 50, 100].map(size => (_jsxs("option", { value: size, children: [size, t('logPerPage')] }, size))) })] })] })] })), logClearOpen && (_jsx("div", { className: css.dialogOverlay, children: _jsxs("div", { className: `${css.dialog} ${css.logClearDialog}`, children: [_jsx("div", { className: css.dialogTitle, children: t('logClearTitle') }), _jsx("div", { className: css.dialogBody, children: t('logClearHint') }), _jsx("input", { type: "date", className: css.logClearDate, value: logClearBefore, onChange: (event) => { actions.setLogClearBefore(event.target.value); } }), _jsxs("div", { className: css.dialogActions, children: [_jsx("button", { type: "button", className: css.checkBtn, disabled: logClearBefore === '' || logClearing, onClick: () => { clearLogBefore(logClearBefore); }, children: logClearing ? t('logClearing') : t('logClearConfirm') }), _jsx("button", { type: "button", className: css.checkBtn, onClick: () => { actions.setLogClearOpen(false); }, children: t('logClearCancel') })] })] }) }))] }));
241
275
  }
242
276
  //# sourceMappingURL=TokenQuotaPanel.js.map
@@ -185,6 +185,7 @@ export function apply(ctx) {
185
185
  return;
186
186
  bound?.setSnapshot(snapshot);
187
187
  bound?.setUpgrade(snapshot.upgrade ?? null);
188
+ bound?.setUpgradeError(snapshot.upgradeError ?? null);
188
189
  actOnFull(snapshot);
189
190
  });
190
191
  if (pullCount % 2 === 0)
@@ -236,21 +237,33 @@ export function apply(ctx) {
236
237
  /** Ask the Host to check the registry right now, then refresh the snapshot. */
237
238
  const checkUpdatesNow = () => {
238
239
  bound?.setCheckingUpdates(true);
240
+ bound?.setLastCheckResult('idle');
239
241
  void fetch('/token-quota/check-updates', { method: 'POST' }).then((response) => {
240
242
  if (!response.ok) {
241
243
  bound?.setError(`check-updates route: ${String(response.status)}`);
244
+ bound?.setLastCheckResult('error');
242
245
  return undefined;
243
246
  }
244
247
  return response.json();
245
248
  }, () => {
246
249
  bound?.setError('update check failed');
250
+ bound?.setLastCheckResult('error');
247
251
  }).then((snapshot) => {
248
252
  if (snapshot !== undefined) {
249
253
  bound?.setSnapshot(snapshot);
250
254
  bound?.setUpgrade(snapshot.upgrade ?? null);
255
+ bound?.setUpgradeError(snapshot.upgradeError ?? null);
251
256
  actOnFull(snapshot);
257
+ if (snapshot.upgrade === null) {
258
+ // No newer version known: distinguish "checked OK, up to date"
259
+ // from "check failed" via the host-provided error message.
260
+ bound?.setLastCheckResult(snapshot.upgradeError !== null ? 'error' : 'up-to-date');
261
+ }
252
262
  }
253
263
  bound?.setCheckingUpdates(false);
264
+ // Clear the transient result hint after a short delay so the button
265
+ // returns to its idle label. The error message stays until the next check.
266
+ setTimeout(() => { bound?.setLastCheckResult('idle'); }, 4000);
254
267
  });
255
268
  };
256
269
  const setReset = (reset) => {
@@ -276,11 +289,33 @@ export function apply(ctx) {
276
289
  bound?.setError('model switch failed');
277
290
  });
278
291
  };
292
+ /** Ask the Host to clear log entries before a given date, then refresh. */
293
+ const clearLogBefore = (before) => {
294
+ bound?.setLogClearing(true);
295
+ void fetch('/token-quota/clear-log', {
296
+ method: 'POST',
297
+ headers: { 'content-type': 'application/json' },
298
+ body: JSON.stringify({ before }),
299
+ }).then((response) => {
300
+ if (!response.ok) {
301
+ bound?.setError(`clear-log route: ${String(response.status)}`);
302
+ return undefined;
303
+ }
304
+ return response.json();
305
+ }, () => {
306
+ bound?.setError('clear log failed');
307
+ }).then((log) => {
308
+ if (log !== undefined)
309
+ bound?.setLog(log);
310
+ bound?.setLogClearing(false);
311
+ bound?.setLogClearOpen(false);
312
+ });
313
+ };
279
314
  const injected = (actions) => {
280
315
  bound = actions;
281
316
  return {
282
317
  load, setLimit, selectModel, setMonitored, setOnFull, setReset,
283
- setCheckUpdates, checkUpdatesNow,
318
+ setCheckUpdates, checkUpdatesNow, clearLogBefore,
284
319
  };
285
320
  };
286
321
  ctx.slots.inject('shell.overlay', () => ctx.slots.register({
@@ -41,6 +41,16 @@ export declare const zh: {
41
41
  logs: string;
42
42
  logsTitle: string;
43
43
  logEmpty: string;
44
+ logNoMatch: string;
45
+ logSearchPlaceholder: string;
46
+ logClear: string;
47
+ logClearTitle: string;
48
+ logClearHint: string;
49
+ logClearConfirm: string;
50
+ logClearCancel: string;
51
+ logClearing: string;
52
+ logPagerInfo: string;
53
+ logPerPage: string;
44
54
  logDay: string;
45
55
  logModel: string;
46
56
  logUsed: string;
@@ -55,6 +65,8 @@ export declare const zh: {
55
65
  checkUpdatesLabel: string;
56
66
  checkUpdatesNow: string;
57
67
  checkingUpdates: string;
68
+ checkUpToDate: string;
69
+ checkFailed: string;
58
70
  };
59
71
  /** Dictionary keys owned by the token-quota panel (a string-literal union). */
60
72
  export type TokenQuotaKey = keyof typeof zh;
@@ -95,6 +107,16 @@ export declare const en: {
95
107
  logs: string;
96
108
  logsTitle: string;
97
109
  logEmpty: string;
110
+ logNoMatch: string;
111
+ logSearchPlaceholder: string;
112
+ logClear: string;
113
+ logClearTitle: string;
114
+ logClearHint: string;
115
+ logClearConfirm: string;
116
+ logClearCancel: string;
117
+ logClearing: string;
118
+ logPagerInfo: string;
119
+ logPerPage: string;
98
120
  logDay: string;
99
121
  logModel: string;
100
122
  logUsed: string;
@@ -109,5 +131,7 @@ export declare const en: {
109
131
  checkUpdatesLabel: string;
110
132
  checkUpdatesNow: string;
111
133
  checkingUpdates: string;
134
+ checkUpToDate: string;
135
+ checkFailed: string;
112
136
  };
113
137
  //# sourceMappingURL=locales.d.ts.map
@@ -41,6 +41,16 @@ export const zh = {
41
41
  logs: '日志',
42
42
  logsTitle: '用量日志',
43
43
  logEmpty: '暂无记录',
44
+ logNoMatch: '无匹配记录',
45
+ logSearchPlaceholder: '搜索模型…',
46
+ logClear: '清除',
47
+ logClearTitle: '清除历史日志',
48
+ logClearHint: '将删除所选日期之前的所有记录(不含当日)',
49
+ logClearConfirm: '确认清除',
50
+ logClearCancel: '取消',
51
+ logClearing: '清除中…',
52
+ logPagerInfo: '第 {from}-{to} 条 / 共 {total} 条',
53
+ logPerPage: '条/页',
44
54
  logDay: '日期',
45
55
  logModel: '模型',
46
56
  logUsed: '用量',
@@ -55,6 +65,8 @@ export const zh = {
55
65
  checkUpdatesLabel: '自动检查更新',
56
66
  checkUpdatesNow: '立即检查',
57
67
  checkingUpdates: '检查中…',
68
+ checkUpToDate: '已是最新',
69
+ checkFailed: '检查失败',
58
70
  };
59
71
  /** English copy. */
60
72
  export const en = {
@@ -93,6 +105,16 @@ export const en = {
93
105
  logs: 'Logs',
94
106
  logsTitle: 'Usage Log',
95
107
  logEmpty: 'No records',
108
+ logNoMatch: 'No matching records',
109
+ logSearchPlaceholder: 'Search model…',
110
+ logClear: 'Clear',
111
+ logClearTitle: 'Clear historical log',
112
+ logClearHint: 'This deletes all records before the selected date (the date itself is kept).',
113
+ logClearConfirm: 'Confirm',
114
+ logClearCancel: 'Cancel',
115
+ logClearing: 'Clearing…',
116
+ logPagerInfo: '{from}-{to} of {total}',
117
+ logPerPage: '/page',
96
118
  logDay: 'Day',
97
119
  logModel: 'Model',
98
120
  logUsed: 'Used',
@@ -107,5 +129,7 @@ export const en = {
107
129
  checkUpdatesLabel: 'Check for updates automatically',
108
130
  checkUpdatesNow: 'Check now',
109
131
  checkingUpdates: 'Checking…',
132
+ checkUpToDate: 'Up to date',
133
+ checkFailed: 'Check failed',
110
134
  };
111
135
  //# sourceMappingURL=locales.js.map
@@ -54,6 +54,10 @@ export interface TokenQuotaPanelState {
54
54
  upgradeDismissed: boolean;
55
55
  /** Whether a manual update check is in flight. */
56
56
  checkingUpdates: boolean;
57
+ /** Result of the most recent manual check; 'idle' = nothing to show. */
58
+ lastCheckResult: 'idle' | 'up-to-date' | 'error';
59
+ /** Error message from the most recent update check (shown when lastCheckResult='error'). */
60
+ upgradeError: string | null;
57
61
  /** Whether the settings dialog is open. */
58
62
  dialogOpen: boolean;
59
63
  /** Full-quota notice shown for the `'stop'` strategy; null when none. */
@@ -64,6 +68,18 @@ export interface TokenQuotaPanelState {
64
68
  logOpen: boolean;
65
69
  /** Latest usage history; null before the first fetch. */
66
70
  log: TokenQuotaLog | null;
71
+ /** Filter string for the log table (matches provider/model key). */
72
+ logSearch: string;
73
+ /** Current page index (0-based) in the filtered log table. */
74
+ logPage: number;
75
+ /** Rows per page in the log table. */
76
+ logPageSize: number;
77
+ /** Whether the clear-log confirmation dialog is open. */
78
+ logClearOpen: boolean;
79
+ /** The `YYYY-MM-DD` date before which log entries will be cleared. */
80
+ logClearBefore: string;
81
+ /** Whether a clear-log request is in flight. */
82
+ logClearing: boolean;
67
83
  }
68
84
  /** Declared write surface. */
69
85
  export type TokenQuotaPanelActions = {
@@ -76,11 +92,19 @@ export type TokenQuotaPanelActions = {
76
92
  setUpgrade: (d: TokenQuotaPanelState, upgrade: TokenQuotaUpgrade | null) => void;
77
93
  setUpgradeDismissed: (d: TokenQuotaPanelState, dismissed: boolean) => void;
78
94
  setCheckingUpdates: (d: TokenQuotaPanelState, checking: boolean) => void;
95
+ setLastCheckResult: (d: TokenQuotaPanelState, result: 'idle' | 'up-to-date' | 'error') => void;
96
+ setUpgradeError: (d: TokenQuotaPanelState, error: string | null) => void;
79
97
  setDialogOpen: (d: TokenQuotaPanelState, open: boolean) => void;
80
98
  setFullNotice: (d: TokenQuotaPanelState, notice: string | null) => void;
81
99
  setReset: (d: TokenQuotaPanelState, reset: TokenQuotaReset | null) => void;
82
100
  setLogOpen: (d: TokenQuotaPanelState, open: boolean) => void;
83
101
  setLog: (d: TokenQuotaPanelState, log: TokenQuotaLog | null) => void;
102
+ setLogSearch: (d: TokenQuotaPanelState, search: string) => void;
103
+ setLogPage: (d: TokenQuotaPanelState, page: number) => void;
104
+ setLogPageSize: (d: TokenQuotaPanelState, size: number) => void;
105
+ setLogClearOpen: (d: TokenQuotaPanelState, open: boolean) => void;
106
+ setLogClearBefore: (d: TokenQuotaPanelState, before: string) => void;
107
+ setLogClearing: (d: TokenQuotaPanelState, clearing: boolean) => void;
84
108
  };
85
109
  /**
86
110
  * Declares the panel state and write surface.
@@ -30,11 +30,19 @@ export function createTokenQuotaPanelStore() {
30
30
  upgrade: null,
31
31
  upgradeDismissed: false,
32
32
  checkingUpdates: false,
33
+ lastCheckResult: 'idle',
34
+ upgradeError: null,
33
35
  dialogOpen: false,
34
36
  fullNotice: null,
35
37
  reset: null,
36
38
  logOpen: false,
37
39
  log: null,
40
+ logSearch: '',
41
+ logPage: 0,
42
+ logPageSize: 15,
43
+ logClearOpen: false,
44
+ logClearBefore: '',
45
+ logClearing: false,
38
46
  }),
39
47
  actions: {
40
48
  setSnapshot: (d, snapshot) => { d.snapshot = snapshot; },
@@ -53,11 +61,19 @@ export function createTokenQuotaPanelStore() {
53
61
  },
54
62
  setUpgradeDismissed: (d, dismissed) => { d.upgradeDismissed = dismissed; },
55
63
  setCheckingUpdates: (d, checking) => { d.checkingUpdates = checking; },
64
+ setLastCheckResult: (d, result) => { d.lastCheckResult = result; },
65
+ setUpgradeError: (d, error) => { d.upgradeError = error; },
56
66
  setDialogOpen: (d, open) => { d.dialogOpen = open; },
57
67
  setFullNotice: (d, notice) => { d.fullNotice = notice; },
58
68
  setReset: (d, reset) => { d.reset = reset; },
59
69
  setLogOpen: (d, open) => { d.logOpen = open; },
60
70
  setLog: (d, log) => { d.log = log; },
71
+ setLogSearch: (d, search) => { d.logSearch = search; d.logPage = 0; },
72
+ setLogPage: (d, page) => { d.logPage = page; },
73
+ setLogPageSize: (d, size) => { d.logPageSize = size; d.logPage = 0; },
74
+ setLogClearOpen: (d, open) => { d.logClearOpen = open; },
75
+ setLogClearBefore: (d, before) => { d.logClearBefore = before; },
76
+ setLogClearing: (d, clearing) => { d.logClearing = clearing; },
61
77
  },
62
78
  });
63
79
  }
@@ -59,6 +59,8 @@ export declare class TokenQuotaService extends Service {
59
59
  private checkUpdates;
60
60
  /** Cached upgrade availability; recomputed by {@link refreshUpgrade}. */
61
61
  private upgrade;
62
+ /** Error message from the most recent update check; null = no error. */
63
+ private upgradeError;
62
64
  /** Timer for the periodic update check. */
63
65
  private updateTimer;
64
66
  /** In-flight update check, to coalesce the periodic and manual triggers. */
@@ -77,6 +79,8 @@ export declare class TokenQuotaService extends Service {
77
79
  private disposeRouteLog;
78
80
  /** Disposer for the optional manual-check route (`POST /token-quota/check-updates`). */
79
81
  private disposeRouteCheck;
82
+ /** Disposer for the optional clear-log route (`POST /token-quota/clear-log`). */
83
+ private disposeRouteClear;
80
84
  /** Disposer for the webServer-arrival watcher when the service mounts later. */
81
85
  private disposeRouteWatcher;
82
86
  /**
@@ -93,6 +97,12 @@ export declare class TokenQuotaService extends Service {
93
97
  private syncUpdateChecking;
94
98
  /** Read the full per-cycle usage history (log dialog data). */
95
99
  readLog(): TokenQuotaLog;
100
+ /**
101
+ * Remove all historical cycles whose day portion strictly precedes `before`
102
+ * (format `YYYY-MM-DD`). The current cycle is never cleared. Returns the
103
+ * trimmed log so the client can refresh immediately.
104
+ */
105
+ clearLogBefore(before: string): TokenQuotaLog;
96
106
  /** Read the current snapshot (also used by tests and inspection). */
97
107
  readSnapshot(): TokenQuotaSnapshot;
98
108
  /** Today's used tokens for one model key, or `0`. */