refactor: dedupe reader helpers

This commit is contained in:
Peter Steinberger
2026-04-07 04:31:30 +01:00
parent d9fbfa268f
commit 2f115bc645
10 changed files with 63 additions and 60 deletions

View File

@@ -1,4 +1,4 @@
import { normalizeTrimmedStringList } from "openclaw/plugin-sdk/text-runtime";
import { normalizeOptionalTrimmedStringList } from "openclaw/plugin-sdk/text-runtime";
import {
type BrowserConfig,
type BrowserProfileConfig,
@@ -113,13 +113,7 @@ function resolveCdpPortRangeStart(
return start;
}
function normalizeStringList(raw: string[] | undefined): string[] | undefined {
if (!Array.isArray(raw) || raw.length === 0) {
return undefined;
}
const values = normalizeTrimmedStringList(raw);
return values.length > 0 ? values : undefined;
}
const normalizeStringList = normalizeOptionalTrimmedStringList;
function resolveBrowserSsrFPolicy(cfg: BrowserConfig | undefined): SsrFPolicy | undefined {
const rawPolicy = cfg?.ssrfPolicy as

View File

@@ -7,6 +7,7 @@
*/
import type * as Lark from "@larksuiteoapi/node-sdk";
import { readStringValue } from "openclaw/plugin-sdk/text-runtime";
import { cleanBlocksForDescendant } from "./docx-table-ops.js";
import type { FeishuDocxBlock, FeishuDocxBlockChild } from "./docx-types.js";
@@ -25,7 +26,8 @@ function normalizeChildIds(children: string[] | string | undefined): string[] |
if (Array.isArray(children)) {
return children;
}
return typeof children === "string" ? [children] : undefined;
const child = readStringValue(children);
return child ? [child] : undefined;
}
function toDescendantBlock(block: FeishuDocxBlock): DocxDescendantCreateBlock {

View File

@@ -25,10 +25,6 @@ export const EXTERNAL_CODE_PLUGIN_REQUIRED_FIELD_PATHS = [
"openclaw.build.openclawVersion",
] as const;
function getTrimmedString(value: unknown): string | undefined {
return normalizeOptionalString(value);
}
function readOpenClawBlock(packageJson: unknown) {
const root = isRecord(packageJson) ? packageJson : undefined;
const openclaw = isRecord(root?.openclaw) ? root.openclaw : undefined;
@@ -42,26 +38,26 @@ export function normalizeExternalPluginCompatibility(
packageJson: unknown,
): ExternalPluginCompatibility | undefined {
const { root, compat, build, install } = readOpenClawBlock(packageJson);
const version = getTrimmedString(root?.version);
const minHostVersion = getTrimmedString(install?.minHostVersion);
const version = normalizeOptionalString(root?.version);
const minHostVersion = normalizeOptionalString(install?.minHostVersion);
const compatibility: ExternalPluginCompatibility = {};
const pluginApi = getTrimmedString(compat?.pluginApi);
const pluginApi = normalizeOptionalString(compat?.pluginApi);
if (pluginApi) {
compatibility.pluginApiRange = pluginApi;
}
const minGatewayVersion = getTrimmedString(compat?.minGatewayVersion) ?? minHostVersion;
const minGatewayVersion = normalizeOptionalString(compat?.minGatewayVersion) ?? minHostVersion;
if (minGatewayVersion) {
compatibility.minGatewayVersion = minGatewayVersion;
}
const builtWithOpenClawVersion = getTrimmedString(build?.openclawVersion) ?? version;
const builtWithOpenClawVersion = normalizeOptionalString(build?.openclawVersion) ?? version;
if (builtWithOpenClawVersion) {
compatibility.builtWithOpenClawVersion = builtWithOpenClawVersion;
}
const pluginSdkVersion = getTrimmedString(build?.pluginSdkVersion);
const pluginSdkVersion = normalizeOptionalString(build?.pluginSdkVersion);
if (pluginSdkVersion) {
compatibility.pluginSdkVersion = pluginSdkVersion;
}
@@ -72,10 +68,10 @@ export function normalizeExternalPluginCompatibility(
export function listMissingExternalCodePluginFieldPaths(packageJson: unknown): string[] {
const { compat, build } = readOpenClawBlock(packageJson);
const missing: string[] = [];
if (!getTrimmedString(compat?.pluginApi)) {
if (!normalizeOptionalString(compat?.pluginApi)) {
missing.push("openclaw.compat.pluginApi");
}
if (!getTrimmedString(build?.openclawVersion)) {
if (!normalizeOptionalString(build?.openclawVersion)) {
missing.push("openclaw.build.openclawVersion");
}
return missing;

View File

@@ -8,6 +8,7 @@ import { z } from "zod";
import { PROTOCOL_VERSION } from "../../src/gateway/protocol/index.ts";
import { formatErrorMessage } from "../../src/infra/errors.ts";
import { rawDataToString } from "../../src/infra/ws.ts";
import { readStringValue } from "../../src/shared/string-coerce.ts";
export const ClaudeChannelNotificationSchema = z.object({
method: z.literal("notifications/claude/channel"),
@@ -66,8 +67,7 @@ export function extractTextFromGatewayPayload(
if (!first || typeof first !== "object") {
return undefined;
}
const text = (first as { text?: unknown }).text;
return typeof text === "string" ? text : undefined;
return readStringValue((first as { text?: unknown }).text);
}
export async function waitFor<T>(

View File

@@ -2,6 +2,7 @@ import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.
import { normalizeTargetForProvider } from "../infra/outbound/target-normalization.js";
import { splitMediaFromOutput } from "../media/parse.js";
import { pluginRegistrationContractRegistry } from "../plugins/contracts/registry.js";
import { normalizeOptionalString, readStringValue } from "../shared/string-coerce.js";
import { truncateUtf16Safe } from "../utils.js";
import { collectTextContentBlocks } from "./content-blocks.js";
import { type MessagingToolSend } from "./pi-embedded-messaging.js";
@@ -98,12 +99,12 @@ export function sanitizeToolResult(result: unknown): unknown {
return item;
}
const entry = item as Record<string, unknown>;
const type = typeof entry.type === "string" ? entry.type : undefined;
const type = readStringValue(entry.type);
if (type === "text" && typeof entry.text === "string") {
return { ...entry, text: truncateToolText(entry.text) };
}
if (type === "image") {
const data = typeof entry.data === "string" ? entry.data : undefined;
const data = readStringValue(entry.data);
const bytes = data ? data.length : undefined;
const cleaned = { ...entry };
delete cleaned.data;
@@ -181,7 +182,7 @@ function readToolResultDetails(result: unknown): Record<string, unknown> | undef
function readToolResultStatus(result: unknown): string | undefined {
const status = readToolResultDetails(result)?.status;
return typeof status === "string" ? status.trim().toLowerCase() : undefined;
return normalizeOptionalString(status)?.toLowerCase();
}
function isExternalToolResult(result: unknown): boolean {
@@ -372,11 +373,11 @@ export function extractToolErrorMessage(result: unknown): string | undefined {
}
function resolveMessageToolTarget(args: Record<string, unknown>): string | undefined {
const toRaw = typeof args.to === "string" ? args.to : undefined;
const toRaw = readStringValue(args.to);
if (toRaw) {
return toRaw;
}
return typeof args.target === "string" ? args.target : undefined;
return readStringValue(args.target);
}
export function extractMessagingToolSend(
@@ -385,8 +386,7 @@ export function extractMessagingToolSend(
): MessagingToolSend | undefined {
// Provider docking: new provider tools must implement plugin.actions.extractToolSend.
const action = typeof args.action === "string" ? args.action.trim() : "";
const accountIdRaw = typeof args.accountId === "string" ? args.accountId.trim() : undefined;
const accountId = accountIdRaw ? accountIdRaw : undefined;
const accountId = normalizeOptionalString(args.accountId);
if (toolName === "message") {
if (action !== "send" && action !== "thread-reply") {
return undefined;

View File

@@ -2,6 +2,7 @@ import { normalizeChannelId as normalizePluginChannelId } from "../../channels/p
import type { ChannelThreadingAdapter } from "../../channels/plugins/types.core.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { ReplyToMode } from "../../config/types.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import type { OriginatingChannelType } from "../templating.js";
import type { ReplyPayload, ReplyThreadingPolicy } from "../types.js";
import { isSingleUseReplyToMode } from "./reply-reference.js";
@@ -27,7 +28,7 @@ function resolveReplyToModeChannelKey(channel?: OriginatingChannelType): string
if (normalized) {
return normalized;
}
return typeof channel === "string" ? channel.trim().toLowerCase() || undefined : undefined;
return normalizeOptionalString(channel)?.toLowerCase();
}
export function resolveConfiguredReplyToMode(
@@ -159,7 +160,7 @@ export function createReplyToModeFilterForChannel(
mode: ReplyToMode,
channel?: OriginatingChannelType,
) {
const normalized = typeof channel === "string" ? channel.trim().toLowerCase() : undefined;
const normalized = normalizeOptionalString(channel)?.toLowerCase();
const isWebchat = normalized === "webchat";
// Default: allow explicit reply tags/directives even when replyToMode is "off".
// Unknown channels fail closed; internal webchat stays allowed.

View File

@@ -1,4 +1,5 @@
import { stripInboundMetadata } from "../auto-reply/reply/strip-inbound-meta.js";
import { normalizeOptionalString, readStringValue } from "../shared/string-coerce.js";
const DEDUPE_TIMESTAMP_WINDOW_MS = 5 * 60 * 1000;
@@ -7,17 +8,22 @@ function extractComparableText(message: unknown): string | undefined {
return undefined;
}
const record = message as { role?: unknown; text?: unknown; content?: unknown };
const role = typeof record.role === "string" ? record.role : undefined;
const role = readStringValue(record.role);
const parts: string[] = [];
if (typeof record.text === "string") {
parts.push(record.text);
const text = readStringValue(record.text);
if (text !== undefined) {
parts.push(text);
}
if (typeof record.content === "string") {
parts.push(record.content);
const content = readStringValue(record.content);
if (content !== undefined) {
parts.push(content);
} else if (Array.isArray(record.content)) {
for (const block of record.content) {
if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
parts.push(block.text);
if (block && typeof block === "object" && "text" in block) {
const blockText = readStringValue(block.text);
if (blockText !== undefined) {
parts.push(blockText);
}
}
}
}
@@ -48,8 +54,7 @@ function resolveComparableRole(message: unknown): string | undefined {
if (!message || typeof message !== "object") {
return undefined;
}
const role = (message as { role?: unknown }).role;
return typeof role === "string" ? role : undefined;
return readStringValue((message as { role?: unknown }).role);
}
function resolveImportedExternalId(message: unknown): string | undefined {
@@ -62,8 +67,7 @@ function resolveImportedExternalId(message: unknown): string | undefined {
typeof (message as { __openclaw?: unknown }).__openclaw === "object"
? ((message as { __openclaw?: Record<string, unknown> }).__openclaw ?? {})
: undefined;
const externalId = meta?.externalId;
return typeof externalId === "string" && externalId.trim() ? externalId : undefined;
return normalizeOptionalString(meta?.externalId);
}
function isEquivalentImportedMessage(existing: unknown, imported: unknown): boolean {

View File

@@ -1,6 +1,6 @@
import { randomUUID } from "node:crypto";
import { resolveMissingRequestedScope } from "../shared/operator-scope-compat.js";
import { normalizeTrimmedStringList } from "../shared/string-normalization.js";
import { normalizeArrayBackedTrimmedStringList } from "../shared/string-normalization.js";
import { type NodeApprovalScope, resolveNodePairApprovalScopes } from "./node-pairing-authz.js";
import {
createAsyncLock,
@@ -67,14 +67,6 @@ const OPERATOR_ROLE = "operator";
const withLock = createAsyncLock();
function normalizeStringList(values?: string[]): string[] | undefined {
if (!Array.isArray(values)) {
return undefined;
}
const normalized = normalizeTrimmedStringList(values);
return normalized.length > 0 ? normalized : [];
}
function buildPendingNodePairingRequest(params: {
requestId?: string;
req: NodePairingRequestInput;
@@ -89,8 +81,8 @@ function buildPendingNodePairingRequest(params: {
uiVersion: params.req.uiVersion,
deviceFamily: params.req.deviceFamily,
modelIdentifier: params.req.modelIdentifier,
caps: normalizeStringList(params.req.caps),
commands: normalizeStringList(params.req.commands),
caps: normalizeArrayBackedTrimmedStringList(params.req.caps),
commands: normalizeArrayBackedTrimmedStringList(params.req.commands),
permissions: params.req.permissions,
remoteIp: params.req.remoteIp,
silent: params.req.silent,
@@ -111,8 +103,8 @@ function refreshPendingNodePairingRequest(
uiVersion: incoming.uiVersion ?? existing.uiVersion,
deviceFamily: incoming.deviceFamily ?? existing.deviceFamily,
modelIdentifier: incoming.modelIdentifier ?? existing.modelIdentifier,
caps: normalizeStringList(incoming.caps) ?? existing.caps,
commands: normalizeStringList(incoming.commands) ?? existing.commands,
caps: normalizeArrayBackedTrimmedStringList(incoming.caps) ?? existing.caps,
commands: normalizeArrayBackedTrimmedStringList(incoming.commands) ?? existing.commands,
permissions: incoming.permissions ?? existing.permissions,
remoteIp: incoming.remoteIp ?? existing.remoteIp,
// Preserve interactive visibility if either request needs attention.

View File

@@ -1,10 +1,13 @@
import { readStringValue } from "./string-coerce.js";
export function extractFirstTextBlock(message: unknown): string | undefined {
if (!message || typeof message !== "object") {
return undefined;
}
const content = (message as { content?: unknown }).content;
if (typeof content === "string") {
return content;
const inline = readStringValue(content);
if (inline !== undefined) {
return inline;
}
if (!Array.isArray(content) || content.length === 0) {
return undefined;
@@ -13,8 +16,7 @@ export function extractFirstTextBlock(message: unknown): string | undefined {
if (!first || typeof first !== "object") {
return undefined;
}
const text = (first as { text?: unknown }).text;
return typeof text === "string" ? text : undefined;
return readStringValue((first as { text?: unknown }).text);
}
export type AssistantPhase = "commentary" | "final_answer";

View File

@@ -15,6 +15,18 @@ export function normalizeTrimmedStringList(value: unknown): string[] {
);
}
export function normalizeOptionalTrimmedStringList(value: unknown): string[] | undefined {
const normalized = normalizeTrimmedStringList(value);
return normalized.length > 0 ? normalized : undefined;
}
export function normalizeArrayBackedTrimmedStringList(value: unknown): string[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
return normalizeTrimmedStringList(value);
}
export function normalizeSingleOrTrimmedStringList(value: unknown): string[] {
if (Array.isArray(value)) {
return normalizeTrimmedStringList(value);