Files
openclaw/src/auto-reply/usage-bar/translator.ts
T
Peter Steinberger e7a9f33d89 refactor(core): adopt normalization-core leaf helpers across production (#120350)
* refactor(core): adopt normalization-core leaf helpers across production

* fix(ci): keep plugin contract source-resolvable

* fix(errors): preserve adapter-owned error fields

* fix(errors): short-circuit existing errors before stringifying

* fix(errors): skip throwing structured getters

* ci: retrigger checks on current base

* fix(errors): guard structured error enumeration

* fix: harden error detail copying
2026-08-08 12:00:49 -07:00

314 lines
9.1 KiB
TypeScript

import {
asSafeIntegerInRange,
expectDefined,
isRecord as isObject,
parseStrictInteger,
} from "@openclaw/normalization-core";
export type UsageBarTemplate = Record<string, unknown>;
export type UsageContract = Record<string, unknown>;
type Vocab = Record<string, unknown>;
function toGlyphs(scale: unknown): string[] {
if (Array.isArray(scale)) {
return scale.filter((g): g is string => typeof g === "string");
}
if (typeof scale === "string") {
return Array.from(scale);
}
return [];
}
function num(value: unknown): string {
if (value === null || value === undefined || value === "") {
return "";
}
const n = Number(value);
if (!Number.isFinite(n)) {
return "";
}
if (Math.abs(n) >= 1000) {
const v = n / 1000;
return Math.abs(v) < 10 ? `${v.toFixed(1)}k` : `${Math.round(v)}k`;
}
return String(Math.trunc(n));
}
function fixed(value: unknown, digits: number): string {
if (value === null || value === undefined || value === "") {
return "";
}
const n = Number(value);
if (!Number.isFinite(n)) {
return "";
}
return n.toFixed(digits);
}
function dur(value: unknown): string {
if (value === null || value === undefined || value === "") {
return "";
}
const raw = Number(value);
if (!Number.isFinite(raw)) {
return "";
}
const s = Math.max(0, Math.trunc(raw));
if (s >= 86400) {
return `${(s / 86400).toFixed(1)}d`;
}
if (s >= 3600) {
const m = Math.floor((s % 3600) / 60);
return `${Math.floor(s / 3600)}h${String(m).padStart(2, "0")}m`;
}
return `${Math.floor(s / 60)}m`;
}
function pct(value: unknown): string {
if (value === null || value === undefined || value === "") {
return "";
}
const n = Number(value);
return Number.isFinite(n) ? `${Math.round(n)}%` : "";
}
function inv(value: unknown): unknown {
if (value === null || value === undefined || value === "") {
return value;
}
const n = Number(value);
if (!Number.isFinite(n)) {
return value;
}
return 100 - Math.max(0, Math.min(100, n));
}
function norm(value: unknown): number {
const n = Number(value);
if (value === null || value === undefined || !Number.isFinite(n)) {
return 0;
}
return Math.max(0, Math.min(100, n)) / 100;
}
function meter(value: unknown, width: number, scale: unknown): string {
const glyphs = toGlyphs(scale);
if (glyphs.length < 2 || width < 1) {
return "";
}
const empty = expectDefined(glyphs[0], "glyphs entry at 0");
const full = expectDefined(glyphs[glyphs.length - 1], "glyphs entry at glyphs.length 1");
const total = norm(value) * width;
const fullc = Math.trunc(total);
const cells: string[] = [];
for (let i = 0; i < Math.min(fullc, width); i++) {
cells.push(full);
}
if (cells.length < width) {
cells.push(
expectDefined(
glyphs[Math.round((total - fullc) * (glyphs.length - 1))],
"glyphs entry at math.round((total fullc) * (glyphs.length 1))",
),
);
}
while (cells.length < width) {
cells.push(empty);
}
return cells.slice(0, width).join("");
}
const VERB_NAMES = new Set(["num", "fixed", "dur", "pct", "inv", "alias", "meter"]);
function parseBoundedIntegerArg(
raw: string | undefined,
options: { defaultValue: number; min: number; max: number },
): number | undefined {
const value = raw === undefined ? options.defaultValue : parseStrictInteger(raw);
return asSafeIntegerInRange(value, options);
}
function applyVerb(name: string, args: string[], value: unknown, vocab: Vocab): unknown {
switch (name) {
case "num":
return num(value);
case "fixed": {
const digits = parseBoundedIntegerArg(args[0], { defaultValue: 2, min: 0, max: 100 });
return digits === undefined ? "" : fixed(value, digits);
}
case "dur":
return dur(value);
case "pct":
return pct(value);
case "inv":
return inv(value);
case "alias": {
const aliases = isObject(vocab["_aliases"]) ? vocab["_aliases"] : {};
const table =
args[0] && isObject(aliases[args[0]]) ? (aliases[args[0]] as Record<string, unknown>) : {};
const key = String(value);
if (Object.hasOwn(table, key)) {
return table[key];
}
const lower = key.toLowerCase();
return Object.hasOwn(table, lower) ? table[lower] : value;
}
case "meter": {
const rawWidth = args[0]?.trim() ? args[0] : undefined;
const width = parseBoundedIntegerArg(rawWidth, { defaultValue: 5, min: 1, max: 100 });
const scale = args.length > 1 ? vocab[expectDefined(args[1], "args entry at 1")] : undefined;
return width === undefined ? "" : meter(value, width, scale);
}
default:
return String(value);
}
}
function getPath(ctx: unknown, path: string): unknown {
let cur: unknown = ctx;
for (const part of path.split(".")) {
if (!isObject(cur)) {
return undefined;
}
cur = cur[part];
if (cur === null || cur === undefined) {
return undefined;
}
}
return cur;
}
const TOKEN = /\{([^}]+)\}/g;
function interp(text: string, ctx: unknown, vocab: Vocab): string {
return text.replace(TOKEN, (_match, body: string) => {
const parts = body.split("|");
let val = getPath(ctx, (parts[0] ?? "").trim());
const ops: Array<{ name: string; args: string[] }> = [];
let fallback: string | undefined;
for (const segRaw of parts.slice(1)) {
const seg = segRaw.trim();
const name = expectDefined(seg.split(":")[0], 'seg.split(":") entry at 0');
if (VERB_NAMES.has(name)) {
ops.push({ name, args: seg.split(":").slice(1) });
} else {
fallback = seg;
}
}
if (val === null || val === undefined || val === "") {
return fallback ?? "";
}
for (const op of ops) {
val = applyVerb(op.name, op.args, val, vocab);
}
return String(val);
});
}
type Segment = Record<string, unknown>;
function renderSegment(seg: Segment, ctx: unknown, vocab: Vocab): string | null {
if ("when" in seg) {
const v = getPath(ctx, String(seg.when));
if (v === null || v === undefined || v === false || v === "") {
return null;
}
}
if ("map" in seg) {
const v = getPath(ctx, String(seg.map));
const key = typeof v === "boolean" ? String(v) : String(v);
const cases = isObject(seg.cases) ? seg.cases : {};
const hit = Object.hasOwn(cases, key) ? cases[key] : cases["_default"];
return typeof hit === "string" ? hit : null;
}
if ("each" in seg) {
const arr = getPath(ctx, String(seg.each));
const items = Array.isArray(arr) ? arr : [];
const itemTpl = typeof seg.item === "string" ? seg.item : "";
const names = Array.isArray(seg.item_scales) ? (seg.item_scales as string[]) : undefined;
const parts: string[] = [];
items.forEach((el, i) => {
let iv = vocab;
if (names && names.length > 0) {
iv = {
...vocab,
"*": vocab[
expectDefined(
names[Math.min(i, names.length - 1)],
"names entry at math.min(i, names.length 1)",
)
],
};
}
const r = interp(itemTpl, el, iv);
if (r) {
parts.push(r);
}
});
const join = typeof seg.join === "string" ? seg.join : " ";
const body = parts.join(join);
if (!body) {
return null;
}
const prefix = typeof seg.text === "string" ? seg.text : "";
return prefix ? `${prefix} ${body}` : body;
}
if ("text" in seg) {
return interp(String(seg.text), ctx, vocab) || null;
}
return null;
}
function resolveLayout(
template: UsageBarTemplate,
surface: unknown,
): { sep: string; pieces: Segment[] } {
const output = template.output;
if (isObject(output)) {
const surfaces = isObject(output.surfaces) ? output.surfaces : {};
let pieces = typeof surface === "string" ? surfaces[surface] : undefined;
if (pieces === undefined) {
pieces = output.default;
}
const sep = typeof output.sep === "string" ? output.sep : "";
return { sep, pieces: Array.isArray(pieces) ? (pieces as Segment[]) : [] };
}
const ov =
typeof surface === "string" &&
isObject(template.surfaces) &&
isObject(template.surfaces[surface])
? template.surfaces[surface]
: {};
const sep =
typeof ov.sep === "string" ? ov.sep : typeof template.sep === "string" ? template.sep : " ";
const segments = Array.isArray(ov.segments)
? ov.segments
: Array.isArray(template.segments)
? template.segments
: [];
return { sep, pieces: segments as Segment[] };
}
export function renderUsageBar(template: UsageBarTemplate, contract: UsageContract): string {
try {
const { sep, pieces } = resolveLayout(template, contract.surface);
const vocab: Vocab = {
...(isObject(template.ramps) ? template.ramps : {}),
...(isObject(template.series) ? template.series : {}),
...(isObject(template.scales) ? template.scales : {}),
};
vocab["_aliases"] = isObject(template.aliases) ? template.aliases : {};
const out: string[] = [];
for (const piece of pieces) {
if (isObject(piece)) {
const r = renderSegment(piece, contract, vocab);
if (r) {
out.push(r);
}
}
}
return out.join(sep);
} catch {
return "";
}
}