mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 13:01:42 +03:00
Centralize date/time formatting utilities (#11831)
This commit is contained in:
+22
-81
@@ -2,6 +2,12 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveUserTimezone } from "../agents/date-time.js";
|
||||
import { normalizeChatType } from "../channels/chat-type.js";
|
||||
import { resolveSenderLabel, type SenderLabelParams } from "../channels/sender-label.js";
|
||||
import {
|
||||
resolveTimezone,
|
||||
formatUtcTimestamp,
|
||||
formatZonedTimestamp,
|
||||
} from "../infra/format-time/format-datetime.ts";
|
||||
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
|
||||
|
||||
export type AgentEnvelopeParams = {
|
||||
channel: string;
|
||||
@@ -66,15 +72,6 @@ function normalizeEnvelopeOptions(options?: EnvelopeFormatOptions): NormalizedEn
|
||||
};
|
||||
}
|
||||
|
||||
function resolveExplicitTimezone(value: string): string | undefined {
|
||||
try {
|
||||
new Intl.DateTimeFormat("en-US", { timeZone: value }).format(new Date());
|
||||
return value;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveEnvelopeTimezone(options: NormalizedEnvelopeOptions): ResolvedEnvelopeTimezone {
|
||||
const trimmed = options.timezone?.trim();
|
||||
if (!trimmed) {
|
||||
@@ -90,46 +87,10 @@ function resolveEnvelopeTimezone(options: NormalizedEnvelopeOptions): ResolvedEn
|
||||
if (lowered === "user") {
|
||||
return { mode: "iana", timeZone: resolveUserTimezone(options.userTimezone) };
|
||||
}
|
||||
const explicit = resolveExplicitTimezone(trimmed);
|
||||
const explicit = resolveTimezone(trimmed);
|
||||
return explicit ? { mode: "iana", timeZone: explicit } : { mode: "utc" };
|
||||
}
|
||||
|
||||
function formatUtcTimestamp(date: Date): string {
|
||||
const yyyy = String(date.getUTCFullYear()).padStart(4, "0");
|
||||
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
const dd = String(date.getUTCDate()).padStart(2, "0");
|
||||
const hh = String(date.getUTCHours()).padStart(2, "0");
|
||||
const min = String(date.getUTCMinutes()).padStart(2, "0");
|
||||
return `${yyyy}-${mm}-${dd}T${hh}:${min}Z`;
|
||||
}
|
||||
|
||||
export function formatZonedTimestamp(date: Date, timeZone?: string): string | undefined {
|
||||
const parts = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: "h23",
|
||||
timeZoneName: "short",
|
||||
}).formatToParts(date);
|
||||
const pick = (type: string) => parts.find((part) => part.type === type)?.value;
|
||||
const yyyy = pick("year");
|
||||
const mm = pick("month");
|
||||
const dd = pick("day");
|
||||
const hh = pick("hour");
|
||||
const min = pick("minute");
|
||||
const tz = [...parts]
|
||||
.toReversed()
|
||||
.find((part) => part.type === "timeZoneName")
|
||||
?.value?.trim();
|
||||
if (!yyyy || !mm || !dd || !hh || !min) {
|
||||
return undefined;
|
||||
}
|
||||
return `${yyyy}-${mm}-${dd} ${hh}:${min}${tz ? ` ${tz}` : ""}`;
|
||||
}
|
||||
|
||||
function formatTimestamp(
|
||||
ts: number | Date | undefined,
|
||||
options?: EnvelopeFormatOptions,
|
||||
@@ -152,47 +113,27 @@ function formatTimestamp(
|
||||
if (zone.mode === "local") {
|
||||
return formatZonedTimestamp(date);
|
||||
}
|
||||
return formatZonedTimestamp(date, zone.timeZone);
|
||||
}
|
||||
|
||||
function formatElapsedTime(currentMs: number, previousMs: number): string | undefined {
|
||||
const elapsedMs = currentMs - previousMs;
|
||||
if (!Number.isFinite(elapsedMs) || elapsedMs < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const seconds = Math.floor(elapsedMs / 1000);
|
||||
if (seconds < 60) {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) {
|
||||
return `${hours}h`;
|
||||
}
|
||||
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days}d`;
|
||||
return formatZonedTimestamp(date, { timeZone: zone.timeZone });
|
||||
}
|
||||
|
||||
export function formatAgentEnvelope(params: AgentEnvelopeParams): string {
|
||||
const channel = params.channel?.trim() || "Channel";
|
||||
const parts: string[] = [channel];
|
||||
const resolved = normalizeEnvelopeOptions(params.envelope);
|
||||
const elapsed =
|
||||
resolved.includeElapsed && params.timestamp && params.previousTimestamp
|
||||
? formatElapsedTime(
|
||||
params.timestamp instanceof Date ? params.timestamp.getTime() : params.timestamp,
|
||||
params.previousTimestamp instanceof Date
|
||||
? params.previousTimestamp.getTime()
|
||||
: params.previousTimestamp,
|
||||
)
|
||||
: undefined;
|
||||
let elapsed: string | undefined;
|
||||
if (resolved.includeElapsed && params.timestamp && params.previousTimestamp) {
|
||||
const currentMs =
|
||||
params.timestamp instanceof Date ? params.timestamp.getTime() : params.timestamp;
|
||||
const previousMs =
|
||||
params.previousTimestamp instanceof Date
|
||||
? params.previousTimestamp.getTime()
|
||||
: params.previousTimestamp;
|
||||
const elapsedMs = currentMs - previousMs;
|
||||
elapsed =
|
||||
Number.isFinite(elapsedMs) && elapsedMs >= 0
|
||||
? formatTimeAgo(elapsedMs, { suffix: false })
|
||||
: undefined;
|
||||
}
|
||||
if (params.from?.trim()) {
|
||||
const from = params.from.trim();
|
||||
parts.push(elapsed ? `${from} +${elapsed}` : from);
|
||||
|
||||
@@ -14,17 +14,13 @@ import {
|
||||
import { loadSessionStore, resolveStorePath, updateSessionStore } from "../../config/sessions.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { formatDurationCompact } from "../../infra/format-time/format-duration.ts";
|
||||
import { formatTimeAgo } from "../../infra/format-time/format-relative.ts";
|
||||
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
||||
import { stopSubagentsForRequester } from "./abort.js";
|
||||
import { clearSessionQueues } from "./queue.js";
|
||||
import {
|
||||
formatAgeShort,
|
||||
formatDurationShort,
|
||||
formatRunLabel,
|
||||
formatRunStatus,
|
||||
sortSubagentRuns,
|
||||
} from "./subagents-utils.js";
|
||||
import { formatRunLabel, formatRunStatus, sortSubagentRuns } from "./subagents-utils.js";
|
||||
|
||||
type SubagentTargetResolution = {
|
||||
entry?: SubagentRunRecord;
|
||||
@@ -45,7 +41,7 @@ function formatTimestampWithAge(valueMs?: number) {
|
||||
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) {
|
||||
return "n/a";
|
||||
}
|
||||
return `${formatTimestamp(valueMs)} (${formatAgeShort(Date.now() - valueMs)})`;
|
||||
return `${formatTimestamp(valueMs)} (${formatTimeAgo(Date.now() - valueMs, { fallback: "n/a" })})`;
|
||||
}
|
||||
|
||||
function resolveRequesterSessionKey(params: Parameters<CommandHandler>[0]): string | undefined {
|
||||
@@ -214,8 +210,8 @@ export const handleSubagentsCommand: CommandHandler = async (params, allowTextCo
|
||||
const label = formatRunLabel(entry);
|
||||
const runtime =
|
||||
entry.endedAt && entry.startedAt
|
||||
? formatDurationShort(entry.endedAt - entry.startedAt)
|
||||
: formatAgeShort(Date.now() - (entry.startedAt ?? entry.createdAt));
|
||||
? (formatDurationCompact(entry.endedAt - entry.startedAt) ?? "n/a")
|
||||
: formatTimeAgo(Date.now() - (entry.startedAt ?? entry.createdAt), { fallback: "n/a" });
|
||||
const runId = entry.runId.slice(0, 8);
|
||||
lines.push(
|
||||
`${index + 1}) ${status} · ${label} · ${runtime} · run ${runId} · ${entry.childSessionKey}`,
|
||||
@@ -296,7 +292,7 @@ export const handleSubagentsCommand: CommandHandler = async (params, allowTextCo
|
||||
const { entry: sessionEntry } = loadSubagentSessionEntry(params, run.childSessionKey);
|
||||
const runtime =
|
||||
run.startedAt && Number.isFinite(run.startedAt)
|
||||
? formatDurationShort((run.endedAt ?? Date.now()) - run.startedAt)
|
||||
? (formatDurationCompact((run.endedAt ?? Date.now()) - run.startedAt) ?? "n/a")
|
||||
: "n/a";
|
||||
const outcome = run.outcome
|
||||
? `${run.outcome.status}${run.outcome.error ? ` (${run.outcome.error})` : ""}`
|
||||
|
||||
@@ -5,6 +5,11 @@ import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
|
||||
import { ensureSkillsWatcher, getSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
|
||||
import { type SessionEntry, updateSessionStore } from "../../config/sessions.js";
|
||||
import { buildChannelSummary } from "../../infra/channel-summary.js";
|
||||
import {
|
||||
resolveTimezone,
|
||||
formatUtcTimestamp,
|
||||
formatZonedTimestamp,
|
||||
} from "../../infra/format-time/format-datetime.ts";
|
||||
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
|
||||
import { drainSystemEventEntries } from "../../infra/system-events.js";
|
||||
|
||||
@@ -39,15 +44,6 @@ export async function prependSystemEvents(params: {
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const resolveExplicitTimezone = (value: string): string | undefined => {
|
||||
try {
|
||||
new Intl.DateTimeFormat("en-US", { timeZone: value }).format(new Date());
|
||||
return value;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const resolveSystemEventTimezone = (cfg: OpenClawConfig) => {
|
||||
const raw = cfg.agents?.defaults?.envelopeTimezone?.trim();
|
||||
if (!raw) {
|
||||
@@ -66,49 +62,10 @@ export async function prependSystemEvents(params: {
|
||||
timeZone: resolveUserTimezone(cfg.agents?.defaults?.userTimezone),
|
||||
};
|
||||
}
|
||||
const explicit = resolveExplicitTimezone(raw);
|
||||
const explicit = resolveTimezone(raw);
|
||||
return explicit ? { mode: "iana" as const, timeZone: explicit } : { mode: "local" as const };
|
||||
};
|
||||
|
||||
const formatUtcTimestamp = (date: Date): string => {
|
||||
const yyyy = String(date.getUTCFullYear()).padStart(4, "0");
|
||||
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
const dd = String(date.getUTCDate()).padStart(2, "0");
|
||||
const hh = String(date.getUTCHours()).padStart(2, "0");
|
||||
const min = String(date.getUTCMinutes()).padStart(2, "0");
|
||||
const sec = String(date.getUTCSeconds()).padStart(2, "0");
|
||||
return `${yyyy}-${mm}-${dd}T${hh}:${min}:${sec}Z`;
|
||||
};
|
||||
|
||||
const formatZonedTimestamp = (date: Date, timeZone?: string): string | undefined => {
|
||||
const parts = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hourCycle: "h23",
|
||||
timeZoneName: "short",
|
||||
}).formatToParts(date);
|
||||
const pick = (type: string) => parts.find((part) => part.type === type)?.value;
|
||||
const yyyy = pick("year");
|
||||
const mm = pick("month");
|
||||
const dd = pick("day");
|
||||
const hh = pick("hour");
|
||||
const min = pick("minute");
|
||||
const sec = pick("second");
|
||||
const tz = [...parts]
|
||||
.toReversed()
|
||||
.find((part) => part.type === "timeZoneName")
|
||||
?.value?.trim();
|
||||
if (!yyyy || !mm || !dd || !hh || !min || !sec) {
|
||||
return undefined;
|
||||
}
|
||||
return `${yyyy}-${mm}-${dd} ${hh}:${min}:${sec}${tz ? ` ${tz}` : ""}`;
|
||||
};
|
||||
|
||||
const formatSystemEventTimestamp = (ts: number, cfg: OpenClawConfig) => {
|
||||
const date = new Date(ts);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
@@ -116,12 +73,15 @@ export async function prependSystemEvents(params: {
|
||||
}
|
||||
const zone = resolveSystemEventTimezone(cfg);
|
||||
if (zone.mode === "utc") {
|
||||
return formatUtcTimestamp(date);
|
||||
return formatUtcTimestamp(date, { displaySeconds: true });
|
||||
}
|
||||
if (zone.mode === "local") {
|
||||
return formatZonedTimestamp(date) ?? "unknown-time";
|
||||
return formatZonedTimestamp(date, { displaySeconds: true }) ?? "unknown-time";
|
||||
}
|
||||
return formatZonedTimestamp(date, zone.timeZone) ?? "unknown-time";
|
||||
return (
|
||||
formatZonedTimestamp(date, { timeZone: zone.timeZone, displaySeconds: true }) ??
|
||||
"unknown-time"
|
||||
);
|
||||
};
|
||||
|
||||
const systemLines: string[] = [];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { SubagentRunRecord } from "../../agents/subagent-registry.js";
|
||||
import { formatDurationCompact } from "../../infra/format-time/format-duration.js";
|
||||
import {
|
||||
formatDurationShort,
|
||||
formatRunLabel,
|
||||
formatRunStatus,
|
||||
resolveSubagentLabel,
|
||||
@@ -54,8 +54,8 @@ describe("subagents utils", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("formats duration short for seconds and minutes", () => {
|
||||
expect(formatDurationShort(45_000)).toBe("45s");
|
||||
expect(formatDurationShort(65_000)).toBe("1m5s");
|
||||
it("formats duration compact for seconds and minutes", () => {
|
||||
expect(formatDurationCompact(45_000)).toBe("45s");
|
||||
expect(formatDurationCompact(65_000)).toBe("1m5s");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,42 +1,6 @@
|
||||
import type { SubagentRunRecord } from "../../agents/subagent-registry.js";
|
||||
import { truncateUtf16Safe } from "../../utils.js";
|
||||
|
||||
export function formatDurationShort(valueMs?: number) {
|
||||
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) {
|
||||
return "n/a";
|
||||
}
|
||||
const totalSeconds = Math.round(valueMs / 1000);
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
if (hours > 0) {
|
||||
return `${hours}h${minutes}m`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m${seconds}s`;
|
||||
}
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
export function formatAgeShort(valueMs?: number) {
|
||||
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) {
|
||||
return "n/a";
|
||||
}
|
||||
const minutes = Math.round(valueMs / 60_000);
|
||||
if (minutes < 1) {
|
||||
return "just now";
|
||||
}
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m ago`;
|
||||
}
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 48) {
|
||||
return `${hours}h ago`;
|
||||
}
|
||||
const days = Math.round(hours / 24);
|
||||
return `${days}d ago`;
|
||||
}
|
||||
|
||||
export function resolveSubagentLabel(entry: SubagentRunRecord, fallback = "subagent") {
|
||||
const raw = entry.label?.trim() || entry.task?.trim() || "";
|
||||
return raw || fallback;
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type SessionEntry,
|
||||
type SessionScope,
|
||||
} from "../config/sessions.js";
|
||||
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
|
||||
import { resolveCommitHash } from "../infra/git-commit.js";
|
||||
import { listPluginCommands } from "../plugins/commands.js";
|
||||
import {
|
||||
@@ -134,25 +135,6 @@ export const formatContextUsageShort = (
|
||||
contextTokens: number | null | undefined,
|
||||
) => `Context ${formatTokens(total, contextTokens ?? null)}`;
|
||||
|
||||
const formatAge = (ms?: number | null) => {
|
||||
if (!ms || ms < 0) {
|
||||
return "unknown";
|
||||
}
|
||||
const minutes = Math.round(ms / 60_000);
|
||||
if (minutes < 1) {
|
||||
return "just now";
|
||||
}
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m ago`;
|
||||
}
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 48) {
|
||||
return `${hours}h ago`;
|
||||
}
|
||||
const days = Math.round(hours / 24);
|
||||
return `${days}d ago`;
|
||||
};
|
||||
|
||||
const formatQueueDetails = (queue?: QueueStatus) => {
|
||||
if (!queue) {
|
||||
return "";
|
||||
@@ -386,7 +368,7 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
const updatedAt = entry?.updatedAt;
|
||||
const sessionLine = [
|
||||
`Session: ${args.sessionKey ?? "unknown"}`,
|
||||
typeof updatedAt === "number" ? `updated ${formatAge(now - updatedAt)}` : "no activity",
|
||||
typeof updatedAt === "number" ? `updated ${formatTimeAgo(now - updatedAt)}` : "no activity",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" • ");
|
||||
|
||||
Reference in New Issue
Block a user