refactor: consolidate byte-size formatting (#99768)

* refactor: consolidate byte-size formatting

* fix: stabilize byte formatter import boundary

* fix: keep byte formatter within SDK budget

* fix: emit byte formatter package entry

* refactor: trim byte formatter surface

* fix: use narrow byte formatter import

* refactor: remove byte formatter dependency changes

* fix: refresh rebased byte formatter baseline
This commit is contained in:
Dallin Romney 2026-07-06 09:26:04 -07:00 committed by GitHub
parent b29d472e41
commit 5af7ba92ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 172 additions and 82 deletions

View file

@ -1,2 +1,2 @@
57289c57bab5188013ab1895d26acabada49aff864b3207f1a3ea3e1d92c7791 plugin-sdk-api-baseline.json
445e5e72a321486831c2c936118302dbc2cde793f5e1dc5beab29100cae8150e plugin-sdk-api-baseline.jsonl
1805b1ddbd9036dac247b4eb01144c791e54e3a4826298bca4f85f67e936d8e2 plugin-sdk-api-baseline.json
e1a4138ec4962ecb09472bf97cabebb98477d82af75f701142323706dcaa95f2 plugin-sdk-api-baseline.jsonl

View file

@ -1,8 +1,14 @@
// File Transfer tests cover params plugin behavior.
import { describe, expect, it } from "vitest";
import { readClampedInt, readGatewayCallOptions } from "./params.js";
import { humanSize, readClampedInt, readGatewayCallOptions } from "./params.js";
describe("file-transfer shared params", () => {
it("preserves spaced binary-scaled transfer labels", () => {
expect(humanSize(512)).toBe("512 B");
expect(humanSize(1536)).toBe("1.5 KB");
expect(humanSize(2 * 1024 * 1024)).toBe("2.00 MB");
});
it("normalizes string timeoutMs values for gateway calls", () => {
expect(readGatewayCallOptions({ timeoutMs: "5000" }).timeoutMs).toBe(5000);
});

View file

@ -1,6 +1,7 @@
// Shared param-validation helpers used by all four agent tools.
// Goal: identical validation behavior + identical error shapes everywhere.
import { formatByteSize } from "openclaw/plugin-sdk/number-runtime";
import { readPositiveIntegerParam } from "openclaw/plugin-sdk/param-readers";
type GatewayCallOptions = {
@ -50,11 +51,10 @@ export function readClampedInt(params: {
}
export function humanSize(bytes: number): string {
if (bytes < 1024) {
return `${bytes} B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
}
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
return formatByteSize(bytes, {
style: "legacy-binary",
maxUnit: "mega",
separator: " ",
fractionDigits: (_value, unit) => (unit === "byte" ? null : unit === "kilo" ? 1 : 2),
});
}

View file

@ -1,6 +1,7 @@
// Qqbot plugin module implements register clear storage behavior.
import fs from "node:fs";
import path from "node:path";
import { formatByteSize } from "openclaw/plugin-sdk/number-runtime";
import { getQQBotMediaPath } from "../../utils/platform.js";
import type { SlashCommandRegistry } from "../slash-commands.js";
@ -36,16 +37,12 @@ function scanDirectoryFiles(dirPath: string): { filePath: string; size: number }
}
function formatBytes(bytes: number): string {
if (bytes < 1024) {
return `${bytes} B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
}
if (bytes < 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
return formatByteSize(bytes, {
style: "legacy-binary",
maxUnit: "giga",
separator: " ",
fractionDigits: (_value, unit) => (unit === "byte" ? null : 1),
});
}
function removeEmptyDirs(dirPath: string): void {

View file

@ -31,11 +31,20 @@ import {
checkFileSize,
downloadFile,
fileExistsAsync,
formatFileSize,
getImageMimeType,
getMimeType,
readFileAsync,
} from "./file-utils.js";
describe("formatFileSize", () => {
it("preserves compact binary-scaled upload labels", () => {
expect(formatFileSize(512)).toBe("512B");
expect(formatFileSize(1536)).toBe("1.5KB");
expect(formatFileSize(2 * 1024 * 1024)).toBe("2.0MB");
});
});
describe("qqbot file-utils MIME helpers", () => {
it("uses the shared media MIME table for extension inference", () => {
expect(getMimeType("voice.mp3")).toBe("audio/mpeg");

View file

@ -3,6 +3,7 @@ import crypto from "node:crypto";
import * as fs from "node:fs";
import * as path from "node:path";
import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
import { formatByteSize } from "openclaw/plugin-sdk/number-runtime";
import {
openLocalFileSafely,
readRegularFile,
@ -124,13 +125,12 @@ export async function fileExistsAsync(filePath: string): Promise<boolean> {
/** Format a byte count into a human-readable size string. */
export function formatFileSize(bytes: number): string {
if (bytes < 1024) {
return `${bytes}B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)}KB`;
}
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
return formatByteSize(bytes, {
style: "legacy-binary",
maxUnit: "mega",
separator: "",
fractionDigits: (_value, unit) => (unit === "byte" ? null : 1),
});
}
/** Infer a MIME type from the file extension. */

View file

@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { formatByteSize } from "./format.js";
describe("formatByteSize", () => {
it.each([
[1024, { style: "iec", maxUnit: "mega", separator: " ", fractionDigits: 1 }, "1.0 KiB"],
[1024, { style: "legacy-binary", maxUnit: "mega", separator: "", fractionDigits: 1 }, "1.0KB"],
[
5 * 1024 * 1024,
{ style: "legacy-binary", maxUnit: "kilo", separator: " ", fractionDigits: 1 },
"5120.0 KB",
],
] as const)("formats %s bytes", (bytes, options, expected) => {
expect(formatByteSize(bytes, options)).toBe(expected);
});
it("supports caller-owned dynamic precision and rounding", () => {
expect(
formatByteSize(Math.floor(99.6 * 1024 * 1024), {
style: "legacy-binary",
maxUnit: "giga",
separator: " ",
fractionDigits: (_value, unit) => (unit === "giga" ? 1 : unit === "byte" ? null : 0),
floorUnits: ["kilo", "mega"],
}),
).toBe("99 MB");
});
});

View file

@ -0,0 +1,41 @@
type ByteSizeUnit = "byte" | "kilo" | "mega" | "giga" | "tera";
type ByteSizeStyle = "iec" | "legacy-binary";
type ByteSizeFormatOptions = {
style: ByteSizeStyle;
maxUnit: ByteSizeUnit;
separator: "" | " ";
fractionDigits: number | ((value: number, unit: ByteSizeUnit) => number | null);
floorUnits?: readonly ByteSizeUnit[];
};
const BYTE_SIZE_UNITS: readonly ByteSizeUnit[] = ["byte", "kilo", "mega", "giga", "tera"];
const BYTE_SIZE_STYLES = {
iec: { base: 1024, labels: ["B", "KiB", "MiB", "GiB", "TiB"] },
"legacy-binary": { base: 1024, labels: ["B", "KB", "MB", "GB", "TB"] },
} as const satisfies Record<ByteSizeStyle, { base: number; labels: readonly string[] }>;
/** Formats a byte count with caller-explicit scale, labels, precision, and unit cap. */
export function formatByteSize(bytes: number, options: ByteSizeFormatOptions): string {
const { base, labels } = BYTE_SIZE_STYLES[options.style];
const maxUnitIndex = BYTE_SIZE_UNITS.indexOf(options.maxUnit);
let unitIndex = 0;
let value = bytes;
while (value >= base && unitIndex < maxUnitIndex) {
value /= base;
unitIndex += 1;
}
const unit = BYTE_SIZE_UNITS[unitIndex];
const fractionDigits =
typeof options.fractionDigits === "function"
? options.fractionDigits(value, unit)
: options.fractionDigits;
if (fractionDigits === null) {
return `${value}${options.separator}${labels[unitIndex]}`;
}
if (options.floorUnits?.includes(unit)) {
value = Math.floor(value * 10 ** fractionDigits) / 10 ** fractionDigits;
}
return `${value.toFixed(fractionDigits)}${options.separator}${labels[unitIndex]}`;
}

View file

@ -2,6 +2,7 @@
export * from "./boolean-coercion.js";
export * from "./error-coercion.js";
export * from "./format.js";
export * from "./json-coercion.js";
export * from "./number-coercion.js";
export * from "./record-coerce.js";

View file

@ -192,12 +192,12 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
),
publicExports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS",
10453,
10454,
env,
),
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS",
5218,
5219,
env,
),
publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv(

View file

@ -1,12 +1,12 @@
/**
* Read/write/edit tool wrappers for host and sandbox workspaces.
* Adds workspace-root guards, adaptive read paging, image validation, memory
* append-only writes, and parameter cleanup around the session file tools.
*/
// Read/write/edit tool wrappers for host and sandbox workspaces.
// Adds workspace-root guards, adaptive read paging, image validation, memory
// append-only writes, and parameter cleanup around the session file tools.
import fs from "node:fs/promises";
import path from "node:path";
import { URL } from "node:url";
import { detectMime } from "@openclaw/media-core/mime";
import { formatByteSize } from "@openclaw/normalization-core";
import { isWindowsDrivePath } from "../infra/archive-path.js";
import { toErrorObject } from "../infra/errors.js";
import {
@ -96,13 +96,12 @@ function malformedXmlArgValuePathError(key: string): Error {
}
function formatBytes(bytes: number): string {
if (bytes >= 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}
if (bytes >= 1024) {
return `${Math.round(bytes / 1024)}KB`;
}
return `${bytes}B`;
return formatByteSize(bytes, {
style: "legacy-binary",
maxUnit: "mega",
separator: "",
fractionDigits: (_value, unit) => (unit === "byte" ? null : unit === "kilo" ? 0 : 1),
});
}
function getToolResultText(result: AgentToolResult<unknown>): string | undefined {

View file

@ -4,7 +4,7 @@
* Downscales and recompresses oversized base64 image blocks before provider replay.
*/
import { canonicalizeBase64 } from "@openclaw/media-core/base64";
import { resolveIntegerOption } from "@openclaw/normalization-core/number-coercion";
import { formatByteSize, resolveIntegerOption } from "@openclaw/normalization-core";
import { toErrorObject } from "../infra/errors.js";
import type { ImageContent } from "../llm/types.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
@ -97,10 +97,12 @@ function formatBytesShort(bytes: number): string {
if (!Number.isFinite(bytes) || bytes < 1024) {
return `${Math.max(0, Math.round(bytes))}B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)}KB`;
}
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
return formatByteSize(bytes, {
style: "legacy-binary",
maxUnit: "mega",
separator: "",
fractionDigits: (_value, unit) => (unit === "kilo" ? 1 : 2),
});
}
function fileNameFromPathLike(pathLike: string): string | undefined {

View file

@ -1,4 +1,5 @@
// Commander registration for gateway status, health, diagnostics, discovery, and run commands.
import { formatByteSize } from "@openclaw/normalization-core";
import type { Command } from "commander";
import { formatDocsLink } from "../../../packages/terminal-core/src/links.js";
import { colorize, isRich, theme } from "../../../packages/terminal-core/src/theme.js";
@ -232,15 +233,12 @@ function formatBytes(value: number | undefined): string {
if (value === undefined) {
return "n/a";
}
const units = ["B", "KiB", "MiB", "GiB"];
let amount = value;
let unitIndex = 0;
while (amount >= 1024 && unitIndex < units.length - 1) {
amount /= 1024;
unitIndex += 1;
}
const digits = unitIndex === 0 || amount >= 100 ? 0 : 1;
return `${amount.toFixed(digits)} ${units[unitIndex]}`;
return formatByteSize(value, {
style: "iec",
maxUnit: "giga",
separator: " ",
fractionDigits: (amount, unit) => (unit === "byte" || amount >= 100 ? 0 : 1),
});
}
function formatStabilityEvent(record: DiagnosticStabilityEventRecord): string {

View file

@ -1,5 +1,6 @@
/** Doctor contribution for low disk space around the OpenClaw state directory. */
// Doctor contribution for low disk space around the OpenClaw state directory.
import os from "node:os";
import { formatByteSize } from "@openclaw/normalization-core";
import { note } from "../../packages/terminal-core/src/note.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveStateDir } from "../config/paths.js";
@ -28,16 +29,13 @@ export function formatBytes(bytes: number): string {
if (bytes < 0 || !Number.isFinite(bytes)) {
return "unknown";
}
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}
if (bytes >= 1024 * 1024) {
return `${Math.floor(bytes / (1024 * 1024))} MB`;
}
if (bytes >= 1024) {
return `${Math.floor(bytes / 1024)} KB`;
}
return `${bytes} B`;
return formatByteSize(bytes, {
style: "legacy-binary",
maxUnit: "giga",
separator: " ",
fractionDigits: (_value, unit) => (unit === "byte" ? null : unit === "giga" ? 1 : 0),
floorUnits: ["kilo", "mega"],
});
}
/**

View file

@ -1,5 +1,6 @@
// Numeric coercion helpers for plugin runtime inputs.
export { formatByteSize } from "../../packages/normalization-core/src/format.js";
export {
asDateTimestampMs,
asFiniteNumberInRange,

View file

@ -8,11 +8,22 @@ import {
import {
assistantAvatarFallbackUrl,
buildAgentContext,
formatBytes,
resolveConfiguredCronModelSuggestions,
resolveEffectiveModelFallbacks,
sortLocaleStrings,
} from "./display.ts";
describe("formatBytes", () => {
it("preserves the Control UI byte-size display contract", () => {
expect(formatBytes(undefined)).toBe("-");
expect(formatBytes(512)).toBe("512 B");
expect(formatBytes(1536)).toBe("1.5 KB");
expect(formatBytes(12 * 1024)).toBe("12 KB");
expect(formatBytes(2 * 1024 * 1024)).toBe("2.0 MB");
});
});
describe("resolveEffectiveModelFallbacks", () => {
it("inherits defaults when no entry fallbacks are configured", () => {
const entryModel = undefined;

View file

@ -1,4 +1,5 @@
// Control UI view renders agents utils screen content.
import { formatByteSize } from "@openclaw/normalization-core";
import { html, nothing } from "lit";
import {
expandToolGroups,
@ -236,17 +237,12 @@ export function formatBytes(bytes?: number) {
if (bytes == null || !Number.isFinite(bytes)) {
return "-";
}
if (bytes < 1024) {
return `${bytes} B`;
}
const units = ["KB", "MB", "GB", "TB"];
let size = bytes / 1024;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex += 1;
}
return `${size.toFixed(size < 10 ? 1 : 0)} ${units[unitIndex]}`;
return formatByteSize(bytes, {
style: "legacy-binary",
maxUnit: "tera",
separator: " ",
fractionDigits: (value, unit) => (unit === "byte" ? null : value < 10 ? 1 : 0),
});
}
export function resolveAgentConfig(config: Record<string, unknown> | null, agentId: string) {

View file

@ -1,5 +1,6 @@
import type { AgentSelectionCapability } from "../../app/agent-selection.ts";
// Control UI controller manages skill workshop gateway state.
import { formatByteSize } from "@openclaw/normalization-core";
import type { AgentSelectionCapability } from "../../app/agent-selection.ts";
import type { ApplicationGateway } from "../../app/context.ts";
import {
normalizeAgentId,
@ -251,10 +252,12 @@ function formatBytes(bytes: number): string {
if (!Number.isFinite(bytes) || bytes <= 0) {
return "0 B";
}
if (bytes < 1024) {
return `${bytes} B`;
}
return `${(bytes / 1024).toFixed(1)} KB`;
return formatByteSize(bytes, {
style: "legacy-binary",
maxUnit: "kilo",
separator: " ",
fractionDigits: (_value, unit) => (unit === "byte" ? null : 1),
});
}
function byteLength(value: string): number {