mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor: consolidate coercion contracts (#122458)
* refactor: consolidate coercion contracts Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics. Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit. * fix: preserve standalone script coercions Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
This commit is contained in:
committed by
GitHub
parent
66fe424590
commit
b080dd1e76
@@ -1,4 +1,5 @@
|
||||
// Control UI module owns transient operator question state.
|
||||
import { asSafeIntegerInRange } from "@openclaw/normalization-core/number-coercion";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeNullableString as readNonEmptyString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type {
|
||||
@@ -61,7 +62,7 @@ type QuestionAnswerValues = Record<string, string[]>;
|
||||
const REFRESH_RETRY_DELAYS_MS = [1_000, 2_000, 4_000] as const;
|
||||
|
||||
function readTimestamp(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
||||
return asSafeIntegerInRange(value, { min: 0 }) ?? null;
|
||||
}
|
||||
|
||||
const MAX_HEADER_GRAPHEMES = 12;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// that is dispatched against the browser plugin's control routes, either
|
||||
// locally or via a browser-capable node. This module narrows the handful of
|
||||
// routes the browser panel needs and keeps route-path knowledge in one place.
|
||||
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||
import { asNullableRecord as asRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { readStringValue } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
@@ -71,10 +72,6 @@ function stringOrEmpty(value: unknown): string {
|
||||
return readStringValue(value) ?? "";
|
||||
}
|
||||
|
||||
function asNullableFiniteNumber(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function normalizeTab(value: unknown): BrowserPanelTab | null {
|
||||
const record = asRecord(value);
|
||||
const targetId = stringOrEmpty(record?.targetId);
|
||||
@@ -236,8 +233,8 @@ export async function readBrowserPageMetrics(
|
||||
fn: "() => ({ cssWidth: window.innerWidth, cssHeight: window.innerHeight, title: document.title, url: location.href })",
|
||||
}),
|
||||
);
|
||||
const cssWidth = asNullableFiniteNumber(result?.cssWidth);
|
||||
const cssHeight = asNullableFiniteNumber(result?.cssHeight);
|
||||
const cssWidth = asFiniteNumber(result?.cssWidth);
|
||||
const cssHeight = asFiniteNumber(result?.cssHeight);
|
||||
if (!cssWidth || !cssHeight || cssWidth <= 0 || cssHeight <= 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -293,10 +290,10 @@ export async function inspectBrowserElementAt(
|
||||
role: stringOrEmpty(result.role),
|
||||
name: stringOrEmpty(result.name),
|
||||
rect: {
|
||||
x: asNullableFiniteNumber(rect?.x) ?? 0,
|
||||
y: asNullableFiniteNumber(rect?.y) ?? 0,
|
||||
width: asNullableFiniteNumber(rect?.width) ?? 0,
|
||||
height: asNullableFiniteNumber(rect?.height) ?? 0,
|
||||
x: asFiniteNumber(rect?.x) ?? 0,
|
||||
y: asFiniteNumber(rect?.y) ?? 0,
|
||||
width: asFiniteNumber(rect?.width) ?? 0,
|
||||
height: asFiniteNumber(rect?.height) ?? 0,
|
||||
},
|
||||
focusable: result.focusable === true,
|
||||
};
|
||||
|
||||
@@ -73,10 +73,10 @@ function isAnySchema(schema: JsonSchema): boolean {
|
||||
function normalizeEnum(values: unknown[]): { enumValues: unknown[]; nullable: boolean } {
|
||||
const filtered = values.filter((value) => value != null);
|
||||
const nullable = filtered.length !== values.length;
|
||||
return { enumValues: uniqueValues(filtered), nullable };
|
||||
return { enumValues: uniqueSchemaValues(filtered), nullable };
|
||||
}
|
||||
|
||||
function uniqueValues(values: unknown[]): unknown[] {
|
||||
function uniqueSchemaValues(values: unknown[]): unknown[] {
|
||||
const unique: unknown[] = [];
|
||||
for (const value of values) {
|
||||
if (!unique.some((existing) => Object.is(existing, value))) {
|
||||
@@ -591,7 +591,7 @@ function normalizeUnion(
|
||||
return {
|
||||
schema: {
|
||||
...schema,
|
||||
enum: uniqueValues(literals),
|
||||
enum: uniqueSchemaValues(literals),
|
||||
nullable,
|
||||
enumIncludesNull: nullable,
|
||||
anyOf: undefined,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { initialState, Task } from "@lit/task";
|
||||
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { ReactiveElement } from "lit";
|
||||
import type { ControlUiGitHubPreview } from "../../../src/gateway/control-ui-contract.js";
|
||||
@@ -49,11 +50,6 @@ function readOptionalGitHubString(
|
||||
return typeof value === "string" && value.trim() ? value : undefined;
|
||||
}
|
||||
|
||||
function optionalNumber(record: Record<string, unknown>, key: string): number | undefined {
|
||||
const value = record[key];
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function parseGitHubIssueOrPullRequestLink(href: string): GitHubLinkTarget | null {
|
||||
let url: URL;
|
||||
try {
|
||||
@@ -97,13 +93,13 @@ function parsePreviewResponse(target: GitHubLinkTarget, value: unknown): GitHubP
|
||||
}
|
||||
return {
|
||||
...target,
|
||||
additions: optionalNumber(value, "additions"),
|
||||
additions: asFiniteNumber(value.additions),
|
||||
avatarDataUrl: safeAvatarDataUrl(value.avatarDataUrl),
|
||||
changedFiles: optionalNumber(value, "changedFiles"),
|
||||
changedFiles: asFiniteNumber(value.changedFiles),
|
||||
closedAt: readOptionalGitHubString(value, "closedAt"),
|
||||
comments: optionalNumber(value, "comments"),
|
||||
comments: asFiniteNumber(value.comments),
|
||||
createdAt: requiredString(value, "createdAt"),
|
||||
deletions: optionalNumber(value, "deletions"),
|
||||
deletions: asFiniteNumber(value.deletions),
|
||||
draft: typeof value.draft === "boolean" ? value.draft : undefined,
|
||||
kind: target.kind,
|
||||
login: readOptionalGitHubString(value, "login") ?? "ghost",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { GatewayRequestError, type GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import type { GatewaySessionRow } from "../../api/types.ts";
|
||||
@@ -50,9 +51,7 @@ export function taskLifecycleSourceUpdatedAt(task: WorkboardTaskSummary): number
|
||||
}
|
||||
|
||||
export function sessionUpdatedAtValue(session: GatewaySessionRow): number | undefined {
|
||||
return typeof session.updatedAt === "number" && Number.isFinite(session.updatedAt)
|
||||
? session.updatedAt
|
||||
: undefined;
|
||||
return asFiniteNumber(session.updatedAt);
|
||||
}
|
||||
|
||||
export function taskSessionKeyMatchesCardSession(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { readSessionMessageIdentity } from "@openclaw/gateway-client/browser";
|
||||
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||
import { asNullableRecord as asRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { resolveToolUseId } from "../../../../src/chat/tool-content.js";
|
||||
import { escapeRegExp } from "../../../../src/shared/regexp.js";
|
||||
@@ -574,8 +575,7 @@ export function queuedSendThreadMessage(item: ChatQueueItem): Record<string, unk
|
||||
}
|
||||
|
||||
export function rawMessageTimestamp(message: unknown): number | null {
|
||||
const timestamp = asRecord(message)?.timestamp;
|
||||
return typeof timestamp === "number" && Number.isFinite(timestamp) ? timestamp : null;
|
||||
return asFiniteNumber(asRecord(message)?.timestamp) ?? null;
|
||||
}
|
||||
|
||||
function chatItemTimestamp(item: ChatItem): number | null {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asNonNegativeFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||
import { asNullableObjectRecord as readCostRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { html, nothing } from "lit";
|
||||
import { isTranscriptOnlyOpenClawAssistantMessage } from "../../../../../src/shared/transcript-only-openclaw-assistant.js";
|
||||
@@ -39,8 +40,7 @@ function readCostValue(
|
||||
cost: Record<string, unknown> | null,
|
||||
key: "input" | "output" | "cacheRead" | "cacheWrite",
|
||||
) {
|
||||
const value = cost?.[key];
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
||||
return asNonNegativeFiniteNumber(cost?.[key]);
|
||||
}
|
||||
|
||||
function latestProviderCostStats(messages: unknown[] | undefined): ProviderCostStats | null {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||
import type { ImageLightboxItem } from "../../../components/image-lightbox.ts";
|
||||
import { t } from "../../../i18n/index.ts";
|
||||
import type { MessageContentItem } from "../../../lib/chat/chat-types.ts";
|
||||
@@ -486,8 +487,7 @@ export function extractImages(message: unknown): ImageBlock[] {
|
||||
}
|
||||
|
||||
function readPairingQrExpiresAtMs(block: Record<string, unknown>): number | undefined {
|
||||
const expiresAtMs = block.expiresAtMs;
|
||||
return typeof expiresAtMs === "number" && Number.isFinite(expiresAtMs) ? expiresAtMs : undefined;
|
||||
return asFiniteNumber(block.expiresAtMs);
|
||||
}
|
||||
|
||||
function isExpiredPairingQrBlock(block: Record<string, unknown>, nowMs = Date.now()): boolean {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Control UI chat module implements stream reconciliation behavior.
|
||||
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||
import {
|
||||
streamSegmentHasItemId,
|
||||
streamSegmentUsesAccumulatedText,
|
||||
@@ -505,12 +506,8 @@ function messageTimestampMs(message: unknown): number | null {
|
||||
if (!message || typeof message !== "object") {
|
||||
return null;
|
||||
}
|
||||
const timestamp = (message as { timestamp?: unknown; ts?: unknown }).timestamp;
|
||||
if (typeof timestamp === "number" && Number.isFinite(timestamp)) {
|
||||
return timestamp;
|
||||
}
|
||||
const ts = (message as { timestamp?: unknown; ts?: unknown }).ts;
|
||||
return typeof ts === "number" && Number.isFinite(ts) ? ts : null;
|
||||
const record = message as { timestamp?: unknown; ts?: unknown };
|
||||
return asFiniteNumber(record.timestamp) ?? asFiniteNumber(record.ts) ?? null;
|
||||
}
|
||||
|
||||
function timestampForInsertedVisibleStream(
|
||||
|
||||
Reference in New Issue
Block a user