245 lines
7.8 KiB
TypeScript
245 lines
7.8 KiB
TypeScript
import { resolveUserTimezone } from "../agents/date-time.js";
|
|
import { normalizeChatType } from "../channels/chat-type.js";
|
|
import { resolveSenderLabel, type SenderLabelParams } from "../channels/sender-label.js";
|
|
import type { ClawdbotConfig } from "../config/config.js";
|
|
|
|
export type AgentEnvelopeParams = {
|
|
channel: string;
|
|
from?: string;
|
|
timestamp?: number | Date;
|
|
host?: string;
|
|
ip?: string;
|
|
body: string;
|
|
previousTimestamp?: number | Date;
|
|
envelope?: EnvelopeFormatOptions;
|
|
};
|
|
|
|
export type EnvelopeFormatOptions = {
|
|
/**
|
|
* "local" (default), "utc", "user", or an explicit IANA timezone string.
|
|
*/
|
|
timezone?: string;
|
|
/**
|
|
* Include absolute timestamps in the envelope (default: true).
|
|
*/
|
|
includeTimestamp?: boolean;
|
|
/**
|
|
* Include elapsed time suffix when previousTimestamp is provided (default: true).
|
|
*/
|
|
includeElapsed?: boolean;
|
|
/**
|
|
* Optional user timezone used when timezone="user".
|
|
*/
|
|
userTimezone?: string;
|
|
};
|
|
|
|
type NormalizedEnvelopeOptions = {
|
|
timezone: string;
|
|
includeTimestamp: boolean;
|
|
includeElapsed: boolean;
|
|
userTimezone?: string;
|
|
};
|
|
|
|
type ResolvedEnvelopeTimezone =
|
|
| { mode: "utc" }
|
|
| { mode: "local" }
|
|
| { mode: "iana"; timeZone: string };
|
|
|
|
export function resolveEnvelopeFormatOptions(cfg?: ClawdbotConfig): EnvelopeFormatOptions {
|
|
const defaults = cfg?.agents?.defaults;
|
|
return {
|
|
timezone: defaults?.envelopeTimezone,
|
|
includeTimestamp: defaults?.envelopeTimestamp !== "off",
|
|
includeElapsed: defaults?.envelopeElapsed !== "off",
|
|
userTimezone: defaults?.userTimezone,
|
|
};
|
|
}
|
|
|
|
function normalizeEnvelopeOptions(options?: EnvelopeFormatOptions): NormalizedEnvelopeOptions {
|
|
const includeTimestamp = options?.includeTimestamp !== false;
|
|
const includeElapsed = options?.includeElapsed !== false;
|
|
return {
|
|
timezone: options?.timezone?.trim() || "local",
|
|
includeTimestamp,
|
|
includeElapsed,
|
|
userTimezone: options?.userTimezone,
|
|
};
|
|
}
|
|
|
|
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) return { mode: "local" };
|
|
const lowered = trimmed.toLowerCase();
|
|
if (lowered === "utc" || lowered === "gmt") return { mode: "utc" };
|
|
if (lowered === "local" || lowered === "host") return { mode: "local" };
|
|
if (lowered === "user") {
|
|
return { mode: "iana", timeZone: resolveUserTimezone(options.userTimezone) };
|
|
}
|
|
const explicit = resolveExplicitTimezone(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`;
|
|
}
|
|
|
|
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]
|
|
.reverse()
|
|
.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,
|
|
): string | undefined {
|
|
if (!ts) return undefined;
|
|
const resolved = normalizeEnvelopeOptions(options);
|
|
if (!resolved.includeTimestamp) return undefined;
|
|
const date = ts instanceof Date ? ts : new Date(ts);
|
|
if (Number.isNaN(date.getTime())) return undefined;
|
|
const zone = resolveEnvelopeTimezone(resolved);
|
|
if (zone.mode === "utc") return formatUtcTimestamp(date);
|
|
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`;
|
|
}
|
|
|
|
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;
|
|
if (params.from?.trim()) {
|
|
const from = params.from.trim();
|
|
parts.push(elapsed ? `${from} +${elapsed}` : from);
|
|
} else if (elapsed) {
|
|
parts.push(`+${elapsed}`);
|
|
}
|
|
if (params.host?.trim()) parts.push(params.host.trim());
|
|
if (params.ip?.trim()) parts.push(params.ip.trim());
|
|
const ts = formatTimestamp(params.timestamp, resolved);
|
|
if (ts) parts.push(ts);
|
|
const header = `[${parts.join(" ")}]`;
|
|
return `${header} ${params.body}`;
|
|
}
|
|
|
|
export function formatInboundEnvelope(params: {
|
|
channel: string;
|
|
from: string;
|
|
body: string;
|
|
timestamp?: number | Date;
|
|
chatType?: string;
|
|
senderLabel?: string;
|
|
sender?: SenderLabelParams;
|
|
previousTimestamp?: number | Date;
|
|
envelope?: EnvelopeFormatOptions;
|
|
}): string {
|
|
const chatType = normalizeChatType(params.chatType);
|
|
const isDirect = !chatType || chatType === "direct";
|
|
const resolvedSender = params.senderLabel?.trim() || resolveSenderLabel(params.sender ?? {});
|
|
const body = !isDirect && resolvedSender ? `${resolvedSender}: ${params.body}` : params.body;
|
|
return formatAgentEnvelope({
|
|
channel: params.channel,
|
|
from: params.from,
|
|
timestamp: params.timestamp,
|
|
previousTimestamp: params.previousTimestamp,
|
|
envelope: params.envelope,
|
|
body,
|
|
});
|
|
}
|
|
|
|
export function formatInboundFromLabel(params: {
|
|
isGroup: boolean;
|
|
groupLabel?: string;
|
|
groupId?: string;
|
|
directLabel: string;
|
|
directId?: string;
|
|
groupFallback?: string;
|
|
}): string {
|
|
// Keep envelope headers compact: group labels include id, DMs only add id when it differs.
|
|
if (params.isGroup) {
|
|
const label = params.groupLabel?.trim() || params.groupFallback || "Group";
|
|
const id = params.groupId?.trim();
|
|
return id ? `${label} id:${id}` : label;
|
|
}
|
|
|
|
const directLabel = params.directLabel.trim();
|
|
const directId = params.directId?.trim();
|
|
if (!directId || directId === directLabel) return directLabel;
|
|
return `${directLabel} id:${directId}`;
|
|
}
|
|
|
|
export function formatThreadStarterEnvelope(params: {
|
|
channel: string;
|
|
author?: string;
|
|
timestamp?: number | Date;
|
|
body: string;
|
|
envelope?: EnvelopeFormatOptions;
|
|
}): string {
|
|
return formatAgentEnvelope({
|
|
channel: params.channel,
|
|
from: params.author,
|
|
timestamp: params.timestamp,
|
|
envelope: params.envelope,
|
|
body: params.body,
|
|
});
|
|
}
|