mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 01:13:53 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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. */
|
||||
|
||||
28
packages/normalization-core/src/format.test.ts
Normal file
28
packages/normalization-core/src/format.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
41
packages/normalization-core/src/format.ts
Normal file
41
packages/normalization-core/src/format.ts
Normal 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]}`;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Numeric coercion helpers for plugin runtime inputs.
|
||||
|
||||
export { formatByteSize } from "../../packages/normalization-core/src/format.js";
|
||||
export {
|
||||
asDateTimestampMs,
|
||||
asFiniteNumberInRange,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user