mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-18 07:21:33 +00:00
* fix: bucket usage days across DST * docs: document DST-aware usage dates * perf: reuse usage day formatters * fix: preserve usage fallback on older gateways * docs: normalize usage changelog entry * docs: leave release changelog unchanged * fix: handle skipped timezone dates
106 lines
3.4 KiB
TypeScript
106 lines
3.4 KiB
TypeScript
import type { SessionUsageTimeSeries } from "../../../../src/shared/session-usage-timeseries-types.js";
|
|
import type { SessionsUsageResult } from "../../../../src/shared/usage-types.js";
|
|
import { GatewayRequestError, type GatewayBrowserClient } from "../../api/gateway.ts";
|
|
|
|
type SessionRequestClient = Pick<GatewayBrowserClient, "request">;
|
|
|
|
export type SessionUsageQuery = {
|
|
startDate: string;
|
|
endDate: string;
|
|
scope: "instance" | "family";
|
|
timeZone: "local" | "utc";
|
|
agentId?: string;
|
|
};
|
|
|
|
function formatUtcOffset(timezoneOffsetMinutes: number): string {
|
|
const offsetFromUtcMinutes = -timezoneOffsetMinutes;
|
|
const sign = offsetFromUtcMinutes >= 0 ? "+" : "-";
|
|
const absMinutes = Math.abs(offsetFromUtcMinutes);
|
|
const hours = Math.floor(absMinutes / 60);
|
|
const minutes = absMinutes % 60;
|
|
return minutes === 0
|
|
? `UTC${sign}${hours}`
|
|
: `UTC${sign}${hours}:${minutes.toString().padStart(2, "0")}`;
|
|
}
|
|
|
|
export function buildSessionUsageDateParams(timeZone: "local" | "utc") {
|
|
return timeZone === "utc"
|
|
? { mode: "utc" }
|
|
: {
|
|
mode: "specific",
|
|
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
utcOffset: formatUtcOffset(new Date().getTimezoneOffset()),
|
|
};
|
|
}
|
|
|
|
function buildSessionUsageParams(query: SessionUsageQuery): Record<string, unknown> {
|
|
return {
|
|
startDate: query.startDate,
|
|
endDate: query.endDate,
|
|
...(query.agentId ? { agentId: query.agentId } : { agentScope: "all" }),
|
|
...buildSessionUsageDateParams(query.timeZone),
|
|
groupBy: query.scope,
|
|
includeHistorical: query.scope === "family",
|
|
limit: 1000,
|
|
includeContextWeight: true,
|
|
};
|
|
}
|
|
|
|
function isOlderGatewayWithoutUsageTimeZone(
|
|
error: unknown,
|
|
params: Record<string, unknown>,
|
|
): boolean {
|
|
return (
|
|
typeof params.timeZone === "string" &&
|
|
typeof params.utcOffset === "string" &&
|
|
error instanceof GatewayRequestError &&
|
|
error.gatewayCode === "INVALID_REQUEST" &&
|
|
error.message.includes("invalid sessions.usage params:") &&
|
|
error.message.includes("unexpected property 'timeZone'")
|
|
);
|
|
}
|
|
|
|
export async function requestSessionsUsage(
|
|
client: SessionRequestClient,
|
|
params: Record<string, unknown>,
|
|
): Promise<SessionsUsageResult> {
|
|
try {
|
|
return await client.request<SessionsUsageResult>("sessions.usage", params);
|
|
} catch (error) {
|
|
if (!isOlderGatewayWithoutUsageTimeZone(error, params)) {
|
|
throw error;
|
|
}
|
|
// Protocol v4 gateways predating timeZone reject the additive field.
|
|
// Retry with the accompanying fixed offset for mixed-version clients.
|
|
const legacyParams = { ...params };
|
|
delete legacyParams.timeZone;
|
|
return await client.request<SessionsUsageResult>("sessions.usage", legacyParams);
|
|
}
|
|
}
|
|
|
|
export function requestSessionUsage(
|
|
client: SessionRequestClient,
|
|
query: SessionUsageQuery,
|
|
): Promise<SessionsUsageResult> {
|
|
return requestSessionsUsage(client, buildSessionUsageParams(query));
|
|
}
|
|
|
|
export function requestSessionUsageTimeSeries(
|
|
client: SessionRequestClient,
|
|
key: string,
|
|
): Promise<SessionUsageTimeSeries | null> {
|
|
return client
|
|
.request<SessionUsageTimeSeries | undefined>("sessions.usage.timeseries", { key })
|
|
.then((result) => result ?? null);
|
|
}
|
|
|
|
export function requestSessionUsageLogs(
|
|
client: SessionRequestClient,
|
|
key: string,
|
|
): Promise<{ logs?: unknown }> {
|
|
return client.request<{ logs?: unknown }>("sessions.usage.logs", {
|
|
key,
|
|
limit: 1000,
|
|
});
|
|
}
|