@cassius0924/dsh-usage-dashboard 0.8.3 → 0.9.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/lib/index.js CHANGED
@@ -2,18 +2,61 @@
2
2
  var USAGE_WINDOW_DAYS = [7, 30, 90, 365];
3
3
  var SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
4
4
  var isValidSessionId = (id) => SESSION_ID_PATTERN.test(id);
5
- var BREAKER_DEFAULT_SETTINGS = { enabled: true, sessions: [] };
6
- var BREAKER_API = {
7
- state: "/api/dsh-usage-dashboard/breaker",
8
- release: "/api/dsh-usage-dashboard/breaker/release"
9
- };
10
- var isBreakerSettingsRequest = (value) => {
11
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
12
- const candidate = value;
13
- if (typeof candidate.enabled !== "boolean") return false;
14
- if (candidate.sessionId === void 0) return true;
15
- return typeof candidate.sessionId === "string" && isValidSessionId(candidate.sessionId);
16
- };
5
+
6
+ // src/memo.ts
7
+ function memoize(ttlMs, compute) {
8
+ const state = { entry: void 0, inflight: void 0 };
9
+ const run = () => {
10
+ if (state.inflight !== void 0) return state.inflight;
11
+ const task = compute().then(
12
+ (value) => {
13
+ state.inflight = void 0;
14
+ if (value.ok) state.entry = { value, setAt: Date.now() };
15
+ return value;
16
+ },
17
+ (err) => {
18
+ state.inflight = void 0;
19
+ throw err;
20
+ }
21
+ );
22
+ state.inflight = task;
23
+ return task;
24
+ };
25
+ return {
26
+ get: () => {
27
+ const hit = state.entry;
28
+ if (hit !== void 0 && hit.value.ok && Date.now() - hit.setAt < ttlMs) {
29
+ return Promise.resolve(hit.value);
30
+ }
31
+ return run();
32
+ },
33
+ refresh: () => run()
34
+ };
35
+ }
36
+
37
+ // src/trust-fence.ts
38
+ function isLoopbackHostname(hostname) {
39
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
40
+ }
41
+ function isTrustedApiRequest(req) {
42
+ const host = req.headers.host;
43
+ if (host === void 0) return false;
44
+ let hostUrl;
45
+ try {
46
+ hostUrl = new URL(`http://${host}`);
47
+ } catch {
48
+ return false;
49
+ }
50
+ if (!isLoopbackHostname(hostUrl.hostname)) return false;
51
+ if (req.headers["sec-fetch-site"] === "cross-site") return false;
52
+ const origin = req.headers.origin;
53
+ if (origin === void 0) return true;
54
+ try {
55
+ return new URL(origin).host === hostUrl.host;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
17
60
 
18
61
  // src/pricing.ts
19
62
  var PEAK_PRICING_FROM_MS = Date.UTC(2026, 7, 16, 16, 0, 0);
@@ -109,338 +152,6 @@ function pricingInfo(nowMs) {
109
152
  };
110
153
  }
111
154
 
112
- // src/breaker.ts
113
- var BEIJING_OFFSET_MS2 = 8 * 36e5;
114
- var HOUR_MS = 36e5;
115
- var DAY_MS = 864e5;
116
- var BREAKER_PEAK_WINDOWS = ["09:00\u201312:00", "14:00\u201318:00"];
117
- function nextResumeAt(nowMs) {
118
- if (!isPeak(nowMs)) return nowMs;
119
- const beijingMs = nowMs + BEIJING_OFFSET_MS2;
120
- const dayStartMs = Math.floor(beijingMs / DAY_MS) * DAY_MS;
121
- return dayStartMs + 18 * HOUR_MS - BEIJING_OFFSET_MS2;
122
- }
123
- function isUserTurn(messages) {
124
- return messages.some((message) => message.source?.kind === "user");
125
- }
126
- function normalizeBreakerSettings(settings) {
127
- const sessions = [...new Set(settings.sessions.filter(isValidSessionId))].sort();
128
- return { enabled: settings.enabled, sessions };
129
- }
130
- function breakerProject(settings, nowMs, holding) {
131
- const peak = isPeak(nowMs);
132
- const normalized = normalizeBreakerSettings(settings);
133
- const parkedSessions = [...new Set(holding.filter(isValidSessionId))].sort();
134
- return {
135
- enabled: normalized.enabled,
136
- sessions: normalized.sessions,
137
- holding: parkedSessions,
138
- now: new Date(nowMs).toISOString(),
139
- peak,
140
- resumeAt: peak ? new Date(nextResumeAt(nowMs)).toISOString() : null,
141
- peakWindows: [...BREAKER_PEAK_WINDOWS]
142
- };
143
- }
144
-
145
- // src/breaker-gate.ts
146
- var MAX_BREAKER_TIMER_DELAY_MS = 2147483647;
147
- var systemBreakerTimers = {
148
- schedule: (callback, delayMs) => {
149
- const handle = setTimeout(callback, delayMs);
150
- handle.unref?.();
151
- return handle;
152
- },
153
- cancel: (handle) => {
154
- clearTimeout(handle);
155
- }
156
- };
157
- var clampBreakerTimerDelay = (delayMs) => {
158
- if (!Number.isFinite(delayMs)) return 0;
159
- return Math.max(0, Math.min(Math.trunc(delayMs), MAX_BREAKER_TIMER_DELAY_MS));
160
- };
161
- var MAX_TRACKED_TURNS = 512;
162
- var BreakerGate = class {
163
- #settings;
164
- #sessions;
165
- #now;
166
- #timers;
167
- #held = /* @__PURE__ */ new Set();
168
- #turns = /* @__PURE__ */ new Map();
169
- #timer;
170
- #disposed = false;
171
- constructor(options = {}) {
172
- this.#settings = normalizeBreakerSettings(options.initialSettings ?? { ...BREAKER_DEFAULT_SETTINGS });
173
- this.#sessions = new Set(this.#settings.sessions);
174
- this.#now = options.now ?? Date.now;
175
- this.#timers = options.timers ?? systemBreakerTimers;
176
- }
177
- get settings() {
178
- return normalizeBreakerSettings(this.#settings);
179
- }
180
- /** Steps parked right now, across every session. */
181
- get held() {
182
- return this.#held.size;
183
- }
184
- /** Session ids with at least one parked step, sorted for the wire projection. */
185
- get heldSessions() {
186
- return [...new Set([...this.#held].map((record) => record.sessionId))].sort();
187
- }
188
- /**
189
- * Park one proposed step until it may proceed. Resolves immediately when the
190
- * step is not subject to the breaker (session not opted in, off-peak,
191
- * machine-started turn, turn already released by hand) or when the turn is
192
- * aborted/disposed.
193
- *
194
- * The listener wrapping this must call `next()` after it resolves — this
195
- * method never throws and never returns a decision of its own.
196
- *
197
- * `sessionId` is the agent id from the payload; an agent without one cannot
198
- * be matched against the opt-in list, so its steps pass.
199
- */
200
- async hold(sessionId, turn, messages, signal) {
201
- if (this.#disposed || signal.aborted || sessionId === void 0 || sessionId === "") return;
202
- const key = `${sessionId}:${turn}`;
203
- let state = this.#turns.get(key);
204
- const user = isUserTurn(messages) || state?.user === true;
205
- if (state === void 0) {
206
- state = { user, released: false };
207
- this.rememberTurn(key, state);
208
- } else {
209
- state.user = user;
210
- }
211
- if (state.released || !this.#settings.enabled || !this.#sessions.has(sessionId) || !isPeak(this.#now()) || !state.user) return;
212
- await new Promise((resolve) => {
213
- const onAbort = () => {
214
- this.releaseRecord(record, false);
215
- };
216
- const record = {
217
- sessionId,
218
- turn,
219
- resolve,
220
- detach: () => {
221
- signal.removeEventListener("abort", onAbort);
222
- }
223
- };
224
- signal.addEventListener("abort", onAbort);
225
- this.#held.add(record);
226
- this.armTimer();
227
- });
228
- }
229
- /**
230
- * Let parked steps continue now. `sessionId` narrows it to one session; no
231
- * argument releases every parked step (the UI's "continue all" button). The
232
- * released turns are remembered, because re-parking their next step would
233
- * contradict the user's instruction.
234
- * @returns how many parked steps were released.
235
- */
236
- release(sessionId) {
237
- const released = [...this.#held].filter((record) => sessionId === void 0 || record.sessionId === sessionId);
238
- for (const record of released) this.releaseRecord(record, true);
239
- return released.length;
240
- }
241
- /**
242
- * Apply new settings. Turning off the master switch — or a session's own
243
- * switch — releases the affected parked steps immediately and without
244
- * remembering a bypass: that is the user's decision, not a request for one
245
- * particular turn to run.
246
- */
247
- update(settings) {
248
- this.#settings = normalizeBreakerSettings(settings);
249
- this.#sessions = new Set(this.#settings.sessions);
250
- if (this.#disposed) return;
251
- const nowMs = this.#now();
252
- for (const record of [...this.#held]) {
253
- if (!this.#settings.enabled || !this.#sessions.has(record.sessionId) || !isPeak(nowMs)) {
254
- this.releaseRecord(record, false);
255
- }
256
- }
257
- if (this.#held.size > 0) this.armTimer();
258
- else this.clearTimer();
259
- }
260
- /** Release everything and stop the timer; called when the plugin unloads. */
261
- dispose() {
262
- this.#disposed = true;
263
- this.clearTimer();
264
- for (const record of [...this.#held]) this.releaseRecord(record, false);
265
- this.#turns.clear();
266
- }
267
- /** (Re)arm the single settle timer for the current peak window. */
268
- armTimer() {
269
- this.clearTimer();
270
- if (this.#disposed || this.#held.size === 0) return;
271
- const nowMs = this.#now();
272
- const delayMs = nextResumeAt(nowMs) - nowMs;
273
- if (!(delayMs > 0)) return;
274
- const timer = { handle: void 0 };
275
- timer.handle = this.#timers.schedule(() => {
276
- if (this.#timer === timer) this.#timer = void 0;
277
- this.onTimer();
278
- }, clampBreakerTimerDelay(delayMs));
279
- this.#timer = timer;
280
- }
281
- onTimer() {
282
- if (this.#disposed || this.#held.size === 0) return;
283
- if (isPeak(this.#now())) {
284
- this.armTimer();
285
- return;
286
- }
287
- for (const record of [...this.#held]) this.releaseRecord(record, false);
288
- }
289
- clearTimer() {
290
- const timer = this.#timer;
291
- if (timer === void 0) return;
292
- this.#timer = void 0;
293
- this.#timers.cancel(timer.handle);
294
- }
295
- releaseRecord(record, remember) {
296
- if (!this.#held.delete(record)) return;
297
- record.detach();
298
- if (remember) this.markReleased(record.sessionId, record.turn);
299
- record.resolve();
300
- if (this.#held.size === 0) this.clearTimer();
301
- }
302
- markReleased(sessionId, turn) {
303
- const key = `${sessionId}:${turn}`;
304
- const state = this.#turns.get(key);
305
- if (state !== void 0) {
306
- state.released = true;
307
- return;
308
- }
309
- this.rememberTurn(key, { user: false, released: true });
310
- }
311
- rememberTurn(key, state) {
312
- if (this.#turns.size >= MAX_TRACKED_TURNS) {
313
- const oldest = this.#turns.keys().next();
314
- if (!oldest.done) this.#turns.delete(oldest.value);
315
- }
316
- this.#turns.set(key, state);
317
- }
318
- };
319
-
320
- // src/breaker-settings.ts
321
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
322
- import { homedir } from "node:os";
323
- import { dirname, join } from "node:path";
324
- var PLUGIN_LABEL = "dsh-usage-dashboard";
325
- var STORAGE_RELATIVE_PATH = join("storages", "dsh-usage-dashboard", "breaker.json");
326
- function breakerSettingsPath(env = process.env) {
327
- const configured = env.DSH_HOME?.trim();
328
- const home = configured !== void 0 && configured !== "" ? configured : join(homedir(), ".dsh");
329
- return join(home, STORAGE_RELATIVE_PATH);
330
- }
331
- var errorText = (error) => error instanceof Error ? error.message : String(error);
332
- var defaultWarn = (message) => {
333
- console.warn(message);
334
- };
335
- function settingsOf(raw) {
336
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
337
- const candidate = raw;
338
- if (!Array.isArray(candidate.sessions)) return void 0;
339
- if (candidate.enabled !== void 0 && typeof candidate.enabled !== "boolean") return void 0;
340
- const ids = candidate.sessions.filter((entry) => typeof entry === "string");
341
- return normalizeBreakerSettings({
342
- enabled: candidate.enabled ?? true,
343
- sessions: ids
344
- });
345
- }
346
- async function loadBreakerSettings(options = {}) {
347
- const filePath = options.filePath ?? breakerSettingsPath();
348
- const warn = options.warn ?? defaultWarn;
349
- let text;
350
- try {
351
- text = await readFile(filePath, "utf8");
352
- } catch (error) {
353
- const code = error.code;
354
- if (code !== "ENOENT") {
355
- warn(`[${PLUGIN_LABEL}] \u8BFB\u53D6\u9AD8\u5CF0\u6682\u505C\u8BBE\u7F6E\u5931\u8D25\uFF0C\u6309\u9ED8\u8BA4\u503C\u5904\u7406\uFF1A${errorText(error)}`);
356
- }
357
- return { ...BREAKER_DEFAULT_SETTINGS };
358
- }
359
- let raw;
360
- try {
361
- raw = JSON.parse(text);
362
- } catch (error) {
363
- warn(`[${PLUGIN_LABEL}] \u9AD8\u5CF0\u6682\u505C\u8BBE\u7F6E\u4E0D\u662F\u5408\u6CD5 JSON\uFF0C\u6309\u9ED8\u8BA4\u503C\u5904\u7406\uFF1A${errorText(error)}`);
364
- return { ...BREAKER_DEFAULT_SETTINGS };
365
- }
366
- const settings = settingsOf(raw);
367
- if (settings === void 0) {
368
- warn(`[${PLUGIN_LABEL}] \u9AD8\u5CF0\u6682\u505C\u8BBE\u7F6E\u7ED3\u6784\u4E0D\u5408\u6CD5\uFF0C\u6309\u9ED8\u8BA4\u503C\u5904\u7406`);
369
- return { ...BREAKER_DEFAULT_SETTINGS };
370
- }
371
- return settings;
372
- }
373
- async function saveBreakerSettings(settings, options = {}) {
374
- const filePath = options.filePath ?? breakerSettingsPath();
375
- const normalized = normalizeBreakerSettings(settings);
376
- await mkdir(dirname(filePath), { recursive: true });
377
- const tempPath = `${filePath}.${process.pid}.${Date.now().toString(36)}.tmp`;
378
- try {
379
- await writeFile(tempPath, `${JSON.stringify(normalized, null, 2)}
380
- `, "utf8");
381
- await rename(tempPath, filePath);
382
- } catch (error) {
383
- await rm(tempPath, { force: true }).catch(() => {
384
- });
385
- throw error;
386
- }
387
- }
388
-
389
- // src/memo.ts
390
- function memoize(ttlMs, compute) {
391
- const state = { entry: void 0, inflight: void 0 };
392
- const run = () => {
393
- if (state.inflight !== void 0) return state.inflight;
394
- const task = compute().then(
395
- (value) => {
396
- state.inflight = void 0;
397
- if (value.ok) state.entry = { value, setAt: Date.now() };
398
- return value;
399
- },
400
- (err) => {
401
- state.inflight = void 0;
402
- throw err;
403
- }
404
- );
405
- state.inflight = task;
406
- return task;
407
- };
408
- return {
409
- get: () => {
410
- const hit = state.entry;
411
- if (hit !== void 0 && hit.value.ok && Date.now() - hit.setAt < ttlMs) {
412
- return Promise.resolve(hit.value);
413
- }
414
- return run();
415
- },
416
- refresh: () => run()
417
- };
418
- }
419
-
420
- // src/trust-fence.ts
421
- function isLoopbackHostname(hostname) {
422
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
423
- }
424
- function isTrustedApiRequest(req) {
425
- const host = req.headers.host;
426
- if (host === void 0) return false;
427
- let hostUrl;
428
- try {
429
- hostUrl = new URL(`http://${host}`);
430
- } catch {
431
- return false;
432
- }
433
- if (!isLoopbackHostname(hostUrl.hostname)) return false;
434
- if (req.headers["sec-fetch-site"] === "cross-site") return false;
435
- const origin = req.headers.origin;
436
- if (origin === void 0) return true;
437
- try {
438
- return new URL(origin).host === hostUrl.host;
439
- } catch {
440
- return false;
441
- }
442
- }
443
-
444
155
  // src/usage.ts
445
156
  var pad2 = (n) => n < 10 ? `0${n}` : String(n);
446
157
  function errorMessage(err) {
@@ -1032,141 +743,9 @@ var name = "dsh-usage-dashboard";
1032
743
  var inject = ["webServer", "credentials", "sessionPersistence"];
1033
744
  var BALANCE_TTL_MS = 6e4;
1034
745
  var USAGE_TTL_MS = 5 * 6e4;
1035
- var BREAKER_BODY_LIMIT_BYTES = 8 * 1024;
1036
- var NO_STORE = { "cache-control": "no-store" };
1037
- var errorText2 = (error) => error instanceof Error ? error.message : String(error);
1038
- async function readBody(req, limit) {
1039
- const chunks = [];
1040
- let size = 0;
1041
- try {
1042
- for await (const chunk of req) {
1043
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1044
- size += buffer.length;
1045
- if (size > limit) {
1046
- req.resume();
1047
- return { kind: "too-large" };
1048
- }
1049
- chunks.push(buffer);
1050
- }
1051
- } catch {
1052
- return { kind: "unreadable" };
1053
- }
1054
- return { kind: "ok", text: Buffer.concat(chunks).toString("utf8") };
1055
- }
1056
- function apply(ctx, options = {}) {
746
+ function apply(ctx) {
1057
747
  const balanceMemo = memoize(BALANCE_TTL_MS, () => fetchBalance(ctx.credentials, ctx.get("deepseekAccount")));
1058
748
  const usageMemo = memoize(USAGE_TTL_MS, () => fetchUsage(ctx.sessionPersistence));
1059
- const seams = options ?? {};
1060
- const warn = seams.warn ?? ((message) => {
1061
- console.warn(message);
1062
- });
1063
- const now = seams.now ?? Date.now;
1064
- const settingsFile = seams.settingsFile ?? breakerSettingsPath();
1065
- const gate = new BreakerGate({ now, timers: seams.timers });
1066
- let settingsTouched = false;
1067
- void loadBreakerSettings({ filePath: settingsFile, warn }).then((settings) => {
1068
- if (!settingsTouched) gate.update(settings);
1069
- }).catch((error) => {
1070
- warn(`[${name}] \u9AD8\u5CF0\u6682\u505C\u8BBE\u7F6E\u52A0\u8F7D\u5931\u8D25\uFF0C\u6309\u9ED8\u8BA4\u503C\u5904\u7406\uFF1A${errorText2(error)}`);
1071
- });
1072
- const projection = () => breakerProject(gate.settings, now(), gate.heldSessions);
1073
- const hostEvents = ctx;
1074
- ctx.effect(() => {
1075
- const dispose = hostEvents.on("agent/pre-step", async (payload, next) => {
1076
- try {
1077
- await gate.hold(payload.agent?.id, payload.turn, payload.messages, payload.signal);
1078
- } catch (error) {
1079
- warn(`[${name}] \u9AD8\u5CF0\u6682\u505C\u6302\u8D77\u5931\u8D25\uFF0C\u672C\u6B21 step \u76F4\u63A5\u653E\u884C\uFF1A${errorText2(error)}`);
1080
- }
1081
- return next();
1082
- });
1083
- return () => {
1084
- dispose();
1085
- gate.dispose();
1086
- };
1087
- }, "dsh-usage-dashboard: \u9AD8\u5CF0\u6682\u505C");
1088
- const writeSettings = async (req, res) => {
1089
- const body = await readBody(req, BREAKER_BODY_LIMIT_BYTES);
1090
- if (body.kind === "too-large") {
1091
- writeJson(res, 413, { ok: false, error: "\u8BF7\u6C42\u4F53\u8D85\u8FC7 8KB \u9650\u5236" }, NO_STORE);
1092
- return;
1093
- }
1094
- if (body.kind === "unreadable") {
1095
- writeJson(res, 400, { ok: false, error: "\u65E0\u6CD5\u8BFB\u53D6\u8BF7\u6C42\u4F53" }, NO_STORE);
1096
- return;
1097
- }
1098
- let raw;
1099
- try {
1100
- raw = JSON.parse(body.text);
1101
- } catch {
1102
- writeJson(res, 400, { ok: false, error: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5 JSON" }, NO_STORE);
1103
- return;
1104
- }
1105
- if (!isBreakerSettingsRequest(raw)) {
1106
- writeJson(res, 400, { ok: false, error: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F\u5408\u6CD5\u7684\u5F00\u5173\u8BF7\u6C42" }, NO_STORE);
1107
- return;
1108
- }
1109
- const previous = gate.settings;
1110
- let next;
1111
- if (raw.sessionId === void 0) {
1112
- next = normalizeBreakerSettings({ enabled: raw.enabled, sessions: previous.sessions });
1113
- } else {
1114
- const sessions = new Set(previous.sessions);
1115
- if (raw.enabled) sessions.add(raw.sessionId);
1116
- else sessions.delete(raw.sessionId);
1117
- next = normalizeBreakerSettings({ enabled: previous.enabled, sessions: [...sessions] });
1118
- }
1119
- settingsTouched = true;
1120
- gate.update(next);
1121
- try {
1122
- await saveBreakerSettings(next, { filePath: settingsFile });
1123
- } catch (error) {
1124
- gate.update(previous);
1125
- warn(`[${name}] \u9AD8\u5CF0\u6682\u505C\u8BBE\u7F6E\u4FDD\u5B58\u5931\u8D25\uFF0C\u5DF2\u56DE\u9000\uFF1A${errorText2(error)}`);
1126
- writeJson(res, 500, { ok: false, error: "\u8BBE\u7F6E\u4FDD\u5B58\u5931\u8D25" }, NO_STORE);
1127
- return;
1128
- }
1129
- writeJson(res, 200, { ok: true, data: projection() }, NO_STORE);
1130
- };
1131
- const releaseHeld = async (req, res) => {
1132
- const body = await readBody(req, BREAKER_BODY_LIMIT_BYTES);
1133
- if (body.kind === "too-large") {
1134
- writeJson(res, 413, { ok: false, error: "\u8BF7\u6C42\u4F53\u8D85\u8FC7 8KB \u9650\u5236" }, NO_STORE);
1135
- return;
1136
- }
1137
- if (body.kind === "unreadable") {
1138
- writeJson(res, 400, { ok: false, error: "\u65E0\u6CD5\u8BFB\u53D6\u8BF7\u6C42\u4F53" }, NO_STORE);
1139
- return;
1140
- }
1141
- let target;
1142
- if (body.text.trim() !== "") {
1143
- let raw;
1144
- try {
1145
- raw = JSON.parse(body.text);
1146
- } catch {
1147
- writeJson(res, 400, { ok: false, error: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5 JSON" }, NO_STORE);
1148
- return;
1149
- }
1150
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1151
- writeJson(res, 400, { ok: false, error: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F JSON \u5BF9\u8C61" }, NO_STORE);
1152
- return;
1153
- }
1154
- const candidate = raw;
1155
- if (Object.keys(candidate).some((key) => key !== "sessionId")) {
1156
- writeJson(res, 400, { ok: false, error: "\u8BF7\u6C42\u5B57\u6BB5\u4E0D\u5408\u6CD5" }, NO_STORE);
1157
- return;
1158
- }
1159
- if (candidate.sessionId !== void 0) {
1160
- if (typeof candidate.sessionId !== "string" || !isValidSessionId(candidate.sessionId)) {
1161
- writeJson(res, 400, { ok: false, error: "\u4F1A\u8BDD id \u683C\u5F0F\u4E0D\u5408\u6CD5" }, NO_STORE);
1162
- return;
1163
- }
1164
- target = candidate.sessionId;
1165
- }
1166
- }
1167
- gate.release(target);
1168
- writeJson(res, 200, { ok: true, data: projection() }, NO_STORE);
1169
- };
1170
749
  ctx.effect(() => ctx.webServer.register({
1171
750
  kind: "prefix",
1172
751
  path: "/api/dsh-usage-dashboard",
@@ -1175,38 +754,12 @@ function apply(ctx, options = {}) {
1175
754
  writeJson(res, 403, { ok: false, error: "forbidden" });
1176
755
  return;
1177
756
  }
1178
- const url = new URL(req.url ?? "/", "http://dsh.internal");
1179
- const pathname = url.pathname;
1180
- const method = req.method ?? "GET";
1181
- if (pathname === BREAKER_API.state) {
1182
- if (method === "GET") {
1183
- writeJson(res, 200, { ok: true, data: projection() }, NO_STORE);
1184
- return;
1185
- }
1186
- if (method === "PUT") {
1187
- await writeSettings(req, res);
1188
- return;
1189
- }
1190
- writeJson(res, 405, { ok: false, error: "method not allowed" }, { ...NO_STORE, allow: "GET, PUT" });
1191
- return;
1192
- }
1193
- if (pathname === BREAKER_API.release) {
1194
- if (method === "POST") {
1195
- await releaseHeld(req, res);
1196
- return;
1197
- }
1198
- writeJson(res, 405, { ok: false, error: "method not allowed" }, { ...NO_STORE, allow: "POST" });
1199
- return;
1200
- }
1201
- const knownReadPath = pathname === "/api/dsh-usage-dashboard/balance" || pathname === "/api/dsh-usage-dashboard/usage" || pathname === "/api/dsh-usage-dashboard/session";
1202
- if (!knownReadPath) {
1203
- writeJson(res, 404, { ok: false, error: "not found" });
1204
- return;
1205
- }
1206
- if (method !== "GET") {
757
+ if (req.method !== "GET") {
1207
758
  writeJson(res, 405, { ok: false, error: "method not allowed" });
1208
759
  return;
1209
760
  }
761
+ const url = new URL(req.url ?? "/", "http://dsh.internal");
762
+ const pathname = url.pathname;
1210
763
  const force = url.searchParams.get("refresh") === "1";
1211
764
  if (pathname === "/api/dsh-usage-dashboard/balance") {
1212
765
  const payload = await (force ? balanceMemo.refresh() : balanceMemo.get());
@@ -1225,6 +778,8 @@ function apply(ctx, options = {}) {
1225
778
  }
1226
779
  const payload = await fetchSessionUsage(ctx.sessionPersistence, id);
1227
780
  writeJson(res, 200, payload, { "cache-control": "no-store" });
781
+ } else {
782
+ writeJson(res, 404, { ok: false, error: "not found" });
1228
783
  }
1229
784
  }
1230
785
  }), "dsh-usage-dashboard: /api routes");