@raingor/pi-web-switch 0.4.2 → 0.4.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/dist/index.html +18 -0
- package/index.html +2 -2
- package/package.json +7 -38
- package/public/apple-touch-icon.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.webmanifest +2 -2
- package/public/pi.svg +41 -6
- package/public/sw.js +51 -51
- package/server/pi-reader.ts +353 -237
- package/src/App.tsx +2 -0
- package/src/components/dashboard/DashboardPage.tsx +341 -56
- package/src/components/layout/AppShell.tsx +45 -13
- package/src/components/layout/Sidebar.tsx +78 -85
- package/src/components/providers/ProvidersModelsPage.tsx +125 -131
- package/src/components/sessions/MemoryPage.tsx +34 -13
- package/src/components/sessions/SessionsPage.tsx +20 -40
- package/src/components/settings/SettingsPage.tsx +6 -1
- package/src/components/speedtest/ModelSpeedTestPage.tsx +429 -0
- package/src/components/ui/EmptyState.tsx +7 -6
- package/src/components/ui/Modal.tsx +33 -25
- package/src/components/ui/StatCard.tsx +10 -13
- package/src/data/builtin-providers.test.ts +109 -0
- package/src/data/builtin-providers.ts +67 -44
- package/src/data/model-catalog.test.ts +122 -0
- package/src/data/model-catalog.ts +697 -478
- package/src/index.css +624 -210
- package/src/lib/translations/en.ts +88 -14
- package/src/lib/translations/ja.ts +88 -14
- package/src/lib/translations/zh-CN.ts +88 -14
- package/src/lib/translations/zh-TW.ts +87 -14
- package/src/main.tsx +97 -44
- package/src/types/index.ts +0 -1
- package/vite.config.ts +65 -164
- package/dist-electron/main/main.cjs +0 -2453
- package/src/data/mock-config.ts +0 -247
- package/src/data/mock-usage.ts +0 -151
package/src/main.tsx
CHANGED
|
@@ -39,10 +39,10 @@ function useThemeSync(initialized: boolean) {
|
|
|
39
39
|
function LoadingScreen() {
|
|
40
40
|
const { t } = useTranslation();
|
|
41
41
|
return (
|
|
42
|
-
<div className="
|
|
43
|
-
<div className="
|
|
44
|
-
<div className="
|
|
45
|
-
<p
|
|
42
|
+
<div className="loading-console">
|
|
43
|
+
<div className="loading-core">
|
|
44
|
+
<div className="loading-ring" />
|
|
45
|
+
<p>{t("loading.config")}</p>
|
|
46
46
|
</div>
|
|
47
47
|
</div>
|
|
48
48
|
);
|
|
@@ -51,8 +51,8 @@ function LoadingScreen() {
|
|
|
51
51
|
function ErrorScreen({ error, onRetry }: { error: string; onRetry: () => void }) {
|
|
52
52
|
const { t } = useTranslation();
|
|
53
53
|
return (
|
|
54
|
-
<div className="
|
|
55
|
-
<div className="flex flex-col items-center gap-4
|
|
54
|
+
<div className="loading-console">
|
|
55
|
+
<div className="tech-panel relative z-10 flex max-w-md flex-col items-center gap-4 rounded-xl border p-8 text-center">
|
|
56
56
|
<p className="text-lg font-semibold text-red-400">{t("loading.error_title")}</p>
|
|
57
57
|
<p className="text-sm text-gray-400">{error}</p>
|
|
58
58
|
<button
|
|
@@ -112,48 +112,101 @@ function applySavedFontSize() {
|
|
|
112
112
|
applySavedZoom();
|
|
113
113
|
function applySavedZoom() {
|
|
114
114
|
const saved = Number(localStorage.getItem("pi-ui-zoom"));
|
|
115
|
+
document.documentElement.style.zoom = "";
|
|
115
116
|
if (saved >= 50 && saved <= 200) {
|
|
116
|
-
document.documentElement.style.zoom
|
|
117
|
+
document.documentElement.style.setProperty("--ui-zoom", String(saved / 100));
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
// ─── Service worker lifecycle ───────────────────────────
|
|
122
|
+
// A production SW can remain registered when the same localhost origin is
|
|
123
|
+
// later opened with Vite dev. Actively remove it in development; otherwise a
|
|
124
|
+
// normal Cmd+R may be served an old HTML shell while Cmd+Shift+R bypasses it.
|
|
125
|
+
const APP_CACHE_PREFIX = "pi-web-switch-";
|
|
126
|
+
const DEV_SW_CLEANUP_KEY = "pi-web-switch-dev-sw-cleaned";
|
|
127
|
+
|
|
128
|
+
async function clearAppCaches() {
|
|
129
|
+
if (!("caches" in window)) return;
|
|
130
|
+
const keys = await caches.keys();
|
|
131
|
+
await Promise.all(
|
|
132
|
+
keys.filter((key) => key.startsWith(APP_CACHE_PREFIX)).map((key) => caches.delete(key))
|
|
133
|
+
);
|
|
134
|
+
}
|
|
125
135
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
136
|
+
function getAppBaseUrl() {
|
|
137
|
+
const moduleScript = Array.from(document.scripts).find(
|
|
138
|
+
(script) => script.type === "module" && script.src
|
|
139
|
+
);
|
|
140
|
+
return new URL("../", moduleScript?.src ?? document.baseURI);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function configureServiceWorker() {
|
|
144
|
+
if (!("serviceWorker" in navigator)) return;
|
|
145
|
+
|
|
146
|
+
const appBaseUrl = getAppBaseUrl();
|
|
147
|
+
|
|
148
|
+
if (import.meta.env.DEV) {
|
|
149
|
+
try {
|
|
150
|
+
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
151
|
+
const appRegistrations = registrations.filter(
|
|
152
|
+
(registration) => registration.scope === appBaseUrl.href
|
|
153
|
+
);
|
|
154
|
+
const hadController = Boolean(navigator.serviceWorker.controller);
|
|
155
|
+
const removed = await Promise.all(
|
|
156
|
+
appRegistrations.map((registration) => registration.unregister())
|
|
157
|
+
);
|
|
158
|
+
await clearAppCaches();
|
|
159
|
+
|
|
160
|
+
// If this document was already controlled by the old production worker,
|
|
161
|
+
// reload once after unregistering it so the first API/static requests are
|
|
162
|
+
// also guaranteed to bypass the old worker. sessionStorage prevents a
|
|
163
|
+
// reload loop if the browser reports the controller for one extra tick.
|
|
164
|
+
if (
|
|
165
|
+
hadController &&
|
|
166
|
+
removed.some(Boolean) &&
|
|
167
|
+
sessionStorage.getItem(DEV_SW_CLEANUP_KEY) !== "1"
|
|
168
|
+
) {
|
|
169
|
+
sessionStorage.setItem(DEV_SW_CLEANUP_KEY, "1");
|
|
170
|
+
window.location.reload();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
sessionStorage.removeItem(DEV_SW_CLEANUP_KEY);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
// Cache cleanup must never prevent React from mounting.
|
|
176
|
+
console.warn("SW cleanup failed:", err);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
// Resolve from the built JS asset directory so this also works when the
|
|
183
|
+
// app is hosted under a subdirectory instead of the domain root.
|
|
184
|
+
const swUrl = new URL("sw.js", appBaseUrl);
|
|
185
|
+
const registration = await navigator.serviceWorker.register(swUrl, {
|
|
186
|
+
updateViaCache: "none",
|
|
157
187
|
});
|
|
188
|
+
// Check on every application start instead of waiting for the browser's
|
|
189
|
+
// periodic service-worker update window.
|
|
190
|
+
await registration.update();
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.warn("SW registration failed:", err);
|
|
158
193
|
}
|
|
159
|
-
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function bootstrap() {
|
|
197
|
+
if (import.meta.env.DEV) {
|
|
198
|
+
await configureServiceWorker();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
createRoot(document.getElementById("root")!).render(
|
|
202
|
+
<StrictMode>
|
|
203
|
+
<Root />
|
|
204
|
+
</StrictMode>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (import.meta.env.PROD) {
|
|
208
|
+
void configureServiceWorker();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
void bootstrap();
|
package/src/types/index.ts
CHANGED
package/vite.config.ts
CHANGED
|
@@ -7,16 +7,12 @@ import type { Connect } from "vite";
|
|
|
7
7
|
// ─── Pi Config API Plugin ───────────────────────────────
|
|
8
8
|
|
|
9
9
|
function piApiPlugin(): Plugin {
|
|
10
|
-
// Lazy-load the server-side module (Node.js only)
|
|
11
|
-
let pi: typeof import("./server/pi-reader");
|
|
12
|
-
let builtins: typeof import("./src/data/builtin-providers");
|
|
13
|
-
|
|
14
10
|
return {
|
|
15
11
|
name: "pi-api",
|
|
16
12
|
configureServer(server) {
|
|
17
|
-
//
|
|
18
|
-
pi = require("./server/pi-reader");
|
|
19
|
-
builtins = require("./src/data/builtin-providers");
|
|
13
|
+
// Lazy-load the server-side module (Node.js only)
|
|
14
|
+
const pi = require("./server/pi-reader");
|
|
15
|
+
const builtins = require("./src/data/builtin-providers");
|
|
20
16
|
|
|
21
17
|
// Warm the usage cache in the background so the dashboard's first
|
|
22
18
|
// request doesn't block on scanning ~150MB of session JSONL.
|
|
@@ -27,15 +23,12 @@ function piApiPlugin(): Plugin {
|
|
|
27
23
|
/* ignore warm-up failure */
|
|
28
24
|
}
|
|
29
25
|
try {
|
|
30
|
-
pi.
|
|
26
|
+
pi.readChatgptUsage();
|
|
31
27
|
} catch {
|
|
32
28
|
/* ignore warm-up failure */
|
|
33
29
|
}
|
|
34
30
|
}, 0);
|
|
35
31
|
|
|
36
|
-
// Start the auto-expiry timer: scan once at startup, then every 24h.
|
|
37
|
-
try { pi.startAutoExpiryTimer(); } catch { /* ignore */ }
|
|
38
|
-
|
|
39
32
|
const routes: Record<string, (req: Connect.IncomingMessage, res: any) => void> = {
|
|
40
33
|
"GET /api/pi/settings"(_, res) {
|
|
41
34
|
const data = pi.readSettings();
|
|
@@ -56,6 +49,48 @@ function piApiPlugin(): Plugin {
|
|
|
56
49
|
res.setHeader("Content-Type", "application/json");
|
|
57
50
|
res.end(JSON.stringify(data ?? {}));
|
|
58
51
|
},
|
|
52
|
+
"GET /api/pi/official-usage-config"(_, res) {
|
|
53
|
+
const config = pi.readOfficialUsageConfig();
|
|
54
|
+
res.setHeader("Content-Type", "application/json");
|
|
55
|
+
res.end(JSON.stringify({
|
|
56
|
+
endpoint: config.endpoint,
|
|
57
|
+
authMode: config.authMode,
|
|
58
|
+
keyCount: config.apiKeys.length,
|
|
59
|
+
maskedKeys: config.apiKeys.map((key: string) => key.length > 8 ? `${key.slice(0, 4)}••••${key.slice(-4)}` : "••••••••"),
|
|
60
|
+
}));
|
|
61
|
+
},
|
|
62
|
+
"POST /api/pi/official-usage-refresh"(_, res) {
|
|
63
|
+
pi.queryOfficialUsage(pi.readOfficialUsageConfig()).then((usage) => {
|
|
64
|
+
res.setHeader("Content-Type", "application/json");
|
|
65
|
+
res.end(JSON.stringify({ success: true, usage }));
|
|
66
|
+
}).catch((error) => {
|
|
67
|
+
res.statusCode = 400;
|
|
68
|
+
res.setHeader("Content-Type", "application/json");
|
|
69
|
+
res.end(JSON.stringify({ success: false, error: error instanceof Error ? error.message : "Official usage query failed" }));
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
"POST /api/pi/official-usage-query"(req, res) {
|
|
73
|
+
let body = "";
|
|
74
|
+
req.on("data", (chunk: string) => (body += chunk));
|
|
75
|
+
req.on("end", async () => {
|
|
76
|
+
try {
|
|
77
|
+
const input = JSON.parse(body);
|
|
78
|
+
const config = {
|
|
79
|
+
endpoint: typeof input.endpoint === "string" ? input.endpoint : "",
|
|
80
|
+
apiKeys: Array.isArray(input.apiKeys) ? input.apiKeys : [],
|
|
81
|
+
authMode: input.authMode,
|
|
82
|
+
};
|
|
83
|
+
const usage = await pi.queryOfficialUsage(config);
|
|
84
|
+
const saved = pi.writeOfficialUsageConfig(config);
|
|
85
|
+
res.setHeader("Content-Type", "application/json");
|
|
86
|
+
res.end(JSON.stringify({ success: saved, usage, error: saved ? undefined : "Failed to save configuration" }));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
res.statusCode = 400;
|
|
89
|
+
res.setHeader("Content-Type", "application/json");
|
|
90
|
+
res.end(JSON.stringify({ success: false, error: error instanceof Error ? error.message : "Official usage query failed" }));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
},
|
|
59
94
|
"POST /api/pi/auth"(req, res) {
|
|
60
95
|
let body = "";
|
|
61
96
|
req.on("data", (chunk: string) => (body += chunk));
|
|
@@ -187,21 +222,6 @@ function piApiPlugin(): Plugin {
|
|
|
187
222
|
}
|
|
188
223
|
});
|
|
189
224
|
},
|
|
190
|
-
"POST /api/pi/session/auto-expire"(req, res) {
|
|
191
|
-
let body = "";
|
|
192
|
-
req.on("data", (chunk: string) => (body += chunk));
|
|
193
|
-
req.on("end", () => {
|
|
194
|
-
try {
|
|
195
|
-
const result = pi.autoExpireSessions();
|
|
196
|
-
res.setHeader("Content-Type", "application/json");
|
|
197
|
-
res.end(JSON.stringify({ success: true, ...result }));
|
|
198
|
-
} catch {
|
|
199
|
-
res.statusCode = 500;
|
|
200
|
-
res.setHeader("Content-Type", "application/json");
|
|
201
|
-
res.end(JSON.stringify({ success: false, error: "Auto-expire failed" }));
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
},
|
|
205
225
|
"GET /api/pi/session-preview"(req, res) {
|
|
206
226
|
const parsedUrl = new URL(req.url!, "http://localhost");
|
|
207
227
|
const p = parsedUrl.searchParams.get("path") || "";
|
|
@@ -312,6 +332,14 @@ function piApiPlugin(): Plugin {
|
|
|
312
332
|
// Strip query string
|
|
313
333
|
const pathOnly = url.split("?")[0];
|
|
314
334
|
|
|
335
|
+
// Automatically archive sessions that have been inactive for more
|
|
336
|
+
// than two weeks. This is recoverable through the existing trash tab.
|
|
337
|
+
if (method === "POST" && pathOnly === "/api/pi/sessions/auto-trash") {
|
|
338
|
+
const result = pi.autoTrashStaleSessions(14);
|
|
339
|
+
res.setHeader("Content-Type", "application/json");
|
|
340
|
+
return res.end(JSON.stringify(result));
|
|
341
|
+
}
|
|
342
|
+
|
|
315
343
|
// Handle DELETE /api/pi/session?path=... (move to trash) and /api/pi/trash?path=... (permanent)
|
|
316
344
|
if (method === "DELETE" && (pathOnly === "/api/pi/session" || pathOnly === "/api/pi/trash")) {
|
|
317
345
|
const parsedUrl = new URL(url, "http://localhost");
|
|
@@ -335,45 +363,11 @@ function piApiPlugin(): Plugin {
|
|
|
335
363
|
const range = parsedUrl.searchParams.get("range") || "today";
|
|
336
364
|
const fromParam = parsedUrl.searchParams.get("from") || "";
|
|
337
365
|
const toParam = parsedUrl.searchParams.get("to") || "";
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const localDateStr = (dt: Date) =>
|
|
342
|
-
new Intl.DateTimeFormat("en-CA", {
|
|
343
|
-
timeZone: "Asia/Shanghai",
|
|
344
|
-
year: "numeric",
|
|
345
|
-
month: "2-digit",
|
|
346
|
-
day: "2-digit",
|
|
347
|
-
}).format(dt);
|
|
348
|
-
let fromDate: string;
|
|
349
|
-
let toDate = localDateStr(now);
|
|
350
|
-
|
|
351
|
-
if (range === "today") {
|
|
352
|
-
fromDate = toDate;
|
|
353
|
-
} else if (range === "7d") {
|
|
354
|
-
const d = new Date(now); d.setDate(d.getDate() - 6); fromDate = localDateStr(d);
|
|
355
|
-
} else if (range === "30d") {
|
|
356
|
-
const d = new Date(now); d.setDate(d.getDate() - 29); fromDate = localDateStr(d);
|
|
357
|
-
} else if (range === "custom" && fromParam) {
|
|
358
|
-
fromDate = fromParam;
|
|
359
|
-
if (toParam) toDate = toParam;
|
|
360
|
-
} else {
|
|
361
|
-
fromDate = toDate;
|
|
366
|
+
if (parsedUrl.searchParams.get("refresh") === "1") {
|
|
367
|
+
pi.clearUsageCache();
|
|
368
|
+
pi.clearChatgptUsageCache();
|
|
362
369
|
}
|
|
363
370
|
|
|
364
|
-
const allRecords = pi.readAllUsage();
|
|
365
|
-
const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
|
|
366
|
-
res.setHeader("Content-Type", "application/json");
|
|
367
|
-
return res.end(JSON.stringify(usage));
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Handle GET /api/pi/cindy-usage-range?range=today|7d|30d|custom&from=...&to=...
|
|
371
|
-
if (method === "GET" && pathOnly === "/api/pi/cindy-usage-range") {
|
|
372
|
-
const parsedUrl = new URL(url, "http://localhost");
|
|
373
|
-
const range = parsedUrl.searchParams.get("range") || "today";
|
|
374
|
-
const fromParam = parsedUrl.searchParams.get("from") || "";
|
|
375
|
-
const toParam = parsedUrl.searchParams.get("to") || "";
|
|
376
|
-
|
|
377
371
|
const now = new Date();
|
|
378
372
|
// Date buckets follow China time (UTC+8) regardless of system timezone
|
|
379
373
|
const localDateStr = (dt: Date) =>
|
|
@@ -399,83 +393,7 @@ function piApiPlugin(): Plugin {
|
|
|
399
393
|
fromDate = toDate;
|
|
400
394
|
}
|
|
401
395
|
|
|
402
|
-
const allRecords = pi.
|
|
403
|
-
const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
|
|
404
|
-
res.setHeader("Content-Type", "application/json");
|
|
405
|
-
return res.end(JSON.stringify(usage));
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Handle GET /api/pi/claude-usage-range?range=today|7d|30d|custom&from=...&to=...
|
|
409
|
-
if (method === "GET" && pathOnly === "/api/pi/claude-usage-range") {
|
|
410
|
-
const parsedUrl = new URL(url, "http://localhost");
|
|
411
|
-
const range = parsedUrl.searchParams.get("range") || "today";
|
|
412
|
-
const fromParam = parsedUrl.searchParams.get("from") || "";
|
|
413
|
-
const toParam = parsedUrl.searchParams.get("to") || "";
|
|
414
|
-
|
|
415
|
-
const now = new Date();
|
|
416
|
-
// Date buckets follow China time (UTC+8) regardless of system timezone
|
|
417
|
-
const localDateStr = (dt: Date) =>
|
|
418
|
-
new Intl.DateTimeFormat("en-CA", {
|
|
419
|
-
timeZone: "Asia/Shanghai",
|
|
420
|
-
year: "numeric",
|
|
421
|
-
month: "2-digit",
|
|
422
|
-
day: "2-digit",
|
|
423
|
-
}).format(dt);
|
|
424
|
-
let fromDate: string;
|
|
425
|
-
let toDate = localDateStr(now);
|
|
426
|
-
|
|
427
|
-
if (range === "today") {
|
|
428
|
-
fromDate = toDate;
|
|
429
|
-
} else if (range === "7d") {
|
|
430
|
-
const d = new Date(now); d.setDate(d.getDate() - 6); fromDate = localDateStr(d);
|
|
431
|
-
} else if (range === "30d") {
|
|
432
|
-
const d = new Date(now); d.setDate(d.getDate() - 29); fromDate = localDateStr(d);
|
|
433
|
-
} else if (range === "custom" && fromParam) {
|
|
434
|
-
fromDate = fromParam;
|
|
435
|
-
if (toParam) toDate = toParam;
|
|
436
|
-
} else {
|
|
437
|
-
fromDate = toDate;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const allRecords = pi.readClaudeUsage();
|
|
441
|
-
const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
|
|
442
|
-
res.setHeader("Content-Type", "application/json");
|
|
443
|
-
return res.end(JSON.stringify(usage));
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Handle GET /api/pi/codex-usage-range?range=today|7d|30d|custom&from=...&to=...
|
|
447
|
-
if (method === "GET" && pathOnly === "/api/pi/codex-usage-range") {
|
|
448
|
-
const parsedUrl = new URL(url, "http://localhost");
|
|
449
|
-
const range = parsedUrl.searchParams.get("range") || "today";
|
|
450
|
-
const fromParam = parsedUrl.searchParams.get("from") || "";
|
|
451
|
-
const toParam = parsedUrl.searchParams.get("to") || "";
|
|
452
|
-
|
|
453
|
-
const now = new Date();
|
|
454
|
-
// Date buckets follow China time (UTC+8) regardless of system timezone
|
|
455
|
-
const localDateStr = (dt: Date) =>
|
|
456
|
-
new Intl.DateTimeFormat("en-CA", {
|
|
457
|
-
timeZone: "Asia/Shanghai",
|
|
458
|
-
year: "numeric",
|
|
459
|
-
month: "2-digit",
|
|
460
|
-
day: "2-digit",
|
|
461
|
-
}).format(dt);
|
|
462
|
-
let fromDate: string;
|
|
463
|
-
let toDate = localDateStr(now);
|
|
464
|
-
|
|
465
|
-
if (range === "today") {
|
|
466
|
-
fromDate = toDate;
|
|
467
|
-
} else if (range === "7d") {
|
|
468
|
-
const d = new Date(now); d.setDate(d.getDate() - 6); fromDate = localDateStr(d);
|
|
469
|
-
} else if (range === "30d") {
|
|
470
|
-
const d = new Date(now); d.setDate(d.getDate() - 29); fromDate = localDateStr(d);
|
|
471
|
-
} else if (range === "custom" && fromParam) {
|
|
472
|
-
fromDate = fromParam;
|
|
473
|
-
if (toParam) toDate = toParam;
|
|
474
|
-
} else {
|
|
475
|
-
fromDate = toDate;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const allRecords = pi.readCodexUsage();
|
|
396
|
+
const allRecords = pi.readAllUsage();
|
|
479
397
|
const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
|
|
480
398
|
res.setHeader("Content-Type", "application/json");
|
|
481
399
|
return res.end(JSON.stringify(usage));
|
|
@@ -502,33 +420,18 @@ function piApiPlugin(): Plugin {
|
|
|
502
420
|
return { fromDate, toDate };
|
|
503
421
|
};
|
|
504
422
|
|
|
505
|
-
// Handle GET /api/pi/
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const range = parsedUrl.searchParams.get("range") || "today";
|
|
509
|
-
const fromParam = parsedUrl.searchParams.get("from") || "";
|
|
510
|
-
const toParam = parsedUrl.searchParams.get("to") || "";
|
|
511
|
-
const { fromDate, toDate } = resolveDateRange(range, fromParam, toParam);
|
|
512
|
-
const allRecords = pi.readAllCombinedUsage();
|
|
513
|
-
const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
|
|
514
|
-
res.setHeader("Content-Type", "application/json");
|
|
515
|
-
return res.end(JSON.stringify(usage));
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Handle provider-filtered endpoints: /api/pi/{provider}-usage-range
|
|
519
|
-
// Copilot is read from the local ~/.copilot/session-store.db (no
|
|
520
|
-
// GitHub REST API, no PAT), so it shares the same sync pipeline as
|
|
521
|
-
// the other local sources.
|
|
522
|
-
const providerMatch = pathOnly.match(/^\/api\/pi\/(atomcode|copilot|opencode|gemini|grok)-usage-range$/);
|
|
523
|
-
if (method === "GET" && providerMatch) {
|
|
524
|
-
const providerId = providerMatch[1]!;
|
|
423
|
+
// Handle GET /api/pi/chatgpt-usage-range using local Codex Desktop
|
|
424
|
+
// rollout JSONL files under ~/.codex/sessions and archived_sessions.
|
|
425
|
+
if (method === "GET" && pathOnly === "/api/pi/chatgpt-usage-range") {
|
|
525
426
|
const parsedUrl = new URL(url, "http://localhost");
|
|
526
427
|
const range = parsedUrl.searchParams.get("range") || "today";
|
|
527
428
|
const fromParam = parsedUrl.searchParams.get("from") || "";
|
|
528
429
|
const toParam = parsedUrl.searchParams.get("to") || "";
|
|
430
|
+
if (parsedUrl.searchParams.get("refresh") === "1") {
|
|
431
|
+
pi.clearChatgptUsageCache();
|
|
432
|
+
}
|
|
529
433
|
const { fromDate, toDate } = resolveDateRange(range, fromParam, toParam);
|
|
530
|
-
const
|
|
531
|
-
const usage = pi.getUsageByRange(allRecords, fromDate, toDate);
|
|
434
|
+
const usage = pi.getUsageByRange(pi.readChatgptUsage(), fromDate, toDate);
|
|
532
435
|
res.setHeader("Content-Type", "application/json");
|
|
533
436
|
return res.end(JSON.stringify(usage));
|
|
534
437
|
}
|
|
@@ -571,8 +474,6 @@ export default defineConfig({
|
|
|
571
474
|
rollupOptions: {
|
|
572
475
|
input: {
|
|
573
476
|
main: path.resolve(__dirname, "index.html"),
|
|
574
|
-
// Menu-bar popup (used by the Electron tray app)
|
|
575
|
-
popup: path.resolve(__dirname, "electron/popup.html"),
|
|
576
477
|
},
|
|
577
478
|
},
|
|
578
479
|
},
|