@tsa-group/claude-usage 0.4.3 → 0.4.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/dist/health.js CHANGED
@@ -22,6 +22,8 @@ export const STATUS_BY_CODE = {
22
22
  /** 連續失敗幾次 / 幾小時沒成功,就算不健康 */
23
23
  export const WARN_FAILURES = 3;
24
24
  export const WARN_STALE_HOURS = 6;
25
+ /** 最近這麼多分鐘內成功過,就視為「現在是好的」,蓋過失敗計數器(見 healthWarning) */
26
+ export const RECENT_OK_MIN = 45;
25
27
  /** usage_snapshots.jsonl 的最後一行(本機最新一筆快照) */
26
28
  export function lastSnapshot() {
27
29
  const p = snapPath();
@@ -77,11 +79,17 @@ export function saveHealth(h) {
77
79
  * 不會再推一次。
78
80
  */
79
81
  export function healthWarning(h) {
82
+ const ok = parseIso(h.last_ok_at);
83
+ const okMinAgo = ok ? (Date.now() - ok.getTime()) / 60_000 : Infinity;
80
84
  const cf = h.consecutive_failures ?? 0;
81
- if (cf >= WARN_FAILURES) {
85
+ // 近期成功過就不看失敗計數器。「現在有沒有在運作」最可靠的答案是「最近有沒有
86
+ // 成功過」,而不是一個可能卡住的計數器 —— 實測(Molly, 0.4.3)出現過快照確實
87
+ // 每 15 分鐘進來、consecutive_failures 卻停在 152 的矛盾狀態:多個 tick 行程
88
+ // 同時 read-modify-write health.json 就會互相蓋掉,計數器因此永遠歸不了零。
89
+ // 那種情況下報「連續 152 次失敗」是錯的,而且會讓人去修一個沒有壞的東西。
90
+ if (cf >= WARN_FAILURES && okMinAgo > RECENT_OK_MIN) {
82
91
  return `連續 ${cf} 次採樣失敗 (${h.last_sample_status}) 自 ${h.first_failure_at}`;
83
92
  }
84
- const ok = parseIso(h.last_ok_at);
85
93
  if (ok) {
86
94
  const hrs = (Date.now() - ok.getTime()) / 3_600_000;
87
95
  if (hrs >= WARN_STALE_HOURS) {
package/dist/install.js CHANGED
@@ -119,7 +119,9 @@ function winInstall() {
119
119
  console.error(` 手動執行這行可看到正確訊息: schtasks ${args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}`);
120
120
  }
121
121
  console.log(`uninstall: schtasks /Delete /TN ${WIN_TASK} /F`);
122
- console.log("! Windows 路徑尚未在真機完整驗證 —— 裝完請跑 claude-usage status 確認有心跳");
122
+ console.log("! 憑證與採樣已於 2026-08-28 Windows 實機驗證通過(含桌面版加密憑證庫)。\n" +
123
+ " 尚未完整驗證的是背景任務本身(schtasks + VBS 隱藏視窗)——\n" +
124
+ " 裝完請跑 claude-usage status 確認有心跳。");
123
125
  return r.status ?? 1;
124
126
  }
125
127
  function winUninstall() {
@@ -128,19 +130,43 @@ function winUninstall() {
128
130
  rmSync(VBS_PATH());
129
131
  return 0;
130
132
  }
133
+ /** schtasks 的 LastTaskResult。0 以外的值多半代表背景任務其實沒跑成功。 */
134
+ const TASK_RESULT = {
135
+ 0: "上次執行成功",
136
+ 267011: "尚未執行過",
137
+ 267009: "正在執行中",
138
+ 267014: "上次被中止",
139
+ };
131
140
  function winStatus() {
132
- // winInstall:不設 encoding。Windows 主控台是本地碼頁(繁中 CP950),用 utf8
133
- // 解碼會把整段表格變成亂碼,而使用者要看的只是「這個工作在不在、下次何時跑」。
134
- const r = spawnSync("schtasks", ["/Query", "/TN", WIN_TASK]);
135
- if (r.status !== 0) {
136
- console.log("bg-task: NOT installed (schtasks)");
137
- return r.status ?? 1;
141
+ // 不要印 schtasks 的原始輸出:它走**主控台代碼頁**(繁中是 CP950),而 Node 只會
142
+ // 用 utf8/latin1 解,中文必定變亂碼(實測「下午 03:45 就緒」印成 `¤U¤È 03:45 ´N°ü`)。
143
+ // 先前的版本用正則挑日期迴避,但仍會連帶吃到後面的本地化文字。
144
+ // 改走 PowerShell ScheduledTasks cmdlet,**自己指定 ASCII 安全的輸出格式** ——
145
+ // 順便拿到 LastTaskResult,那正是「背景任務上次到底有沒有跑成功」的直接答案,
146
+ // 而那是 schtasks 表格裡最該看、卻最容易被亂碼蓋掉的一欄。
147
+ const cmd = `$ErrorActionPreference='SilentlyContinue';` +
148
+ `$t=Get-ScheduledTask -TaskName '${WIN_TASK}';` +
149
+ `if(-not $t){'MISSING'}else{` +
150
+ `$i=Get-ScheduledTaskInfo -TaskName '${WIN_TASK}';` +
151
+ `$last=if($i.LastRunTime){$i.LastRunTime.ToString('yyyy-MM-dd HH:mm')}else{'-'};` +
152
+ `$next=if($i.NextRunTime){$i.NextRunTime.ToString('yyyy-MM-dd HH:mm')}else{'-'};` +
153
+ `"$($t.State)|$($i.LastTaskResult)|$last|$next"}`;
154
+ const r = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", cmd], { encoding: "utf8", timeout: 15_000, windowsHide: true });
155
+ const line = (r.stdout ?? "").trim();
156
+ if (r.status !== 0 || !line || line === "MISSING") {
157
+ console.log(`bg-task: NOT installed —— 跑 claude-usage install 重新註冊`);
158
+ return 1;
159
+ }
160
+ const [state, resultRaw, last, next] = line.split("|");
161
+ const result = Number(resultRaw);
162
+ const meaning = TASK_RESULT[result] ?? `結束碼 ${resultRaw}`;
163
+ console.log(`bg-task: 已註冊 "${WIN_TASK}" 狀態=${state} 上次=${last} 下次=${next}`);
164
+ console.log(` ${meaning}`);
165
+ // 只有「成功」與「還沒跑過」算正常;其餘代表背景任務其實沒在做事。
166
+ if (result !== 0 && result !== 267011 && result !== 267009) {
167
+ console.log(` ! 背景任務上次沒有正常結束 —— 細節: schtasks /Query /TN ${WIN_TASK} /V /FO LIST`);
168
+ return 1;
138
169
  }
139
- // 只挑出下次執行時間那一段數字/日期,避開會亂碼的本地化欄位標題
140
- const raw = (r.stdout ?? Buffer.alloc(0)).toString("latin1");
141
- const when = raw.match(/\d{4}[/-]\d{1,2}[/-]\d{1,2}[^\r\n]*/)?.[0]?.trim();
142
- console.log(`bg-task: 已註冊 "${WIN_TASK}"${when ? `,下次執行 ${when}` : ""}`);
143
- console.log(` 完整資訊: schtasks /Query /TN ${WIN_TASK} /V /FO LIST`);
144
170
  return 0;
145
171
  }
146
172
  const sessionStartGroups = (s) => s.hooks?.SessionStart ?? [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsa-group/claude-usage",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Per-user Claude usage collector — measures Claude Code token detail and account-level rate-limit utilization locally, reports to your own ingest server.",
5
5
  "type": "module",
6
6
  "bin": {