refactor: reuse shared coercion helpers (#86419)

* refactor: share talk event metric extraction

* refactor: reuse shared coercion helpers

* refactor: reuse shared primitive guards

* refactor: reuse shared record guard

* refactor: reuse shared primitive helpers

* refactor: reuse shared string guards

* refactor: reuse shared non-empty string guard

* refactor: share plugin primitive coercion helpers

* refactor: reuse plugin coercion helpers

* refactor: reuse plugin coercion helpers in more plugins

* refactor: reuse channel coercion helpers

* refactor: reuse monitor coercion helpers

* refactor: reuse provider coercion helpers

* refactor: reuse core coercion helpers

* refactor: reuse runtime coercion helpers

* refactor: reuse helper coercion in codex paths

* refactor: reuse helper coercion in runtime paths

* refactor: reuse codex app-server coercion helpers

* refactor: reuse codex record helpers

* refactor: reuse migration and qa record helpers

* refactor: reuse feishu and core helper guards

* refactor: reuse browser and policy coercion helpers

* refactor: reuse memory wiki record helper

* refactor: share boolean coercion helpers

* refactor: reuse finite number coercion

* refactor: reuse trimmed string list helpers

* refactor: reuse string list normalization

* refactor: reuse remaining string list helpers

* refactor: reuse string entry normalizer

* refactor: share sorted string helpers

* refactor: share string list normalization

* test: preserve command registry browser imports

* refactor: reuse trimmed list helpers

* refactor: reuse string dedupe helpers

* refactor: reuse local dedupe helpers

* refactor: reuse more string dedupe helpers

* refactor: reuse command string dedupe helpers

* refactor: dedupe memory path lists with helper

* refactor: expose string dedupe helpers to plugins

* refactor: reuse core string dedupe helpers

* refactor: reuse shared unique value helpers

* refactor: reuse unique helpers in agent utilities

* refactor: reuse unique helpers in config plumbing

* refactor: reuse unique helpers in extensions

* refactor: reuse unique helpers in core utilities

* refactor: reuse unique helpers in qa plugins

* refactor: reuse unique helpers in memory plugins

* refactor: reuse unique helpers in channel plugins

* refactor: reuse unique helpers in core tails

* refactor: reuse unique helper in comfy workflow

* refactor: reuse unique helpers in test utilities

* refactor: expose unique value helper to plugins

* refactor: reuse unique helpers for numeric lists

* refactor: replace index dedupe filters

* refactor: reuse string entry normalization

* refactor: reuse string normalization in plugin helpers

* refactor: reuse string normalization in extension helpers

* refactor: reuse string normalization in channel parsers

* refactor: reuse string normalization in memory search

* refactor: reuse string normalization in provider parsers

* refactor: reuse string normalization in qa helpers

* refactor: reuse string normalization in infra parsers

* refactor: reuse string normalization in messaging parsers

* refactor: reuse string normalization in core parsers

* refactor: reuse string normalization in extension parsers

* refactor: reuse string normalization in remaining parsers

* refactor: reuse string normalization in final parser spots

* refactor: reuse string normalization in qa media helpers

* refactor: reuse normalization in provider and media lists

* refactor: reuse normalization for remaining set filters

* refactor: reuse normalization in policy allowlists

* refactor: reuse normalization in session and owner lists

* refactor: centralize primitive string lists

* refactor: reuse lowercase entry helpers

* refactor: reuse sorted string helpers

* refactor: reuse unique trimmed helpers

* refactor: reuse string normalization helpers

* refactor: reuse catalog string helpers

* refactor: reuse remaining string helpers

* refactor: simplify remaining list normalization

* refactor: reuse codex auth order normalization

* chore: refresh plugin sdk api baseline

* fix: make shared string sorting deterministic

* chore: refresh plugin sdk api baseline

* fix: align host env security ordering
This commit is contained in:
Peter Steinberger
2026-05-25 21:20:41 +01:00
committed by GitHub
parent a98660eebd
commit 77d9ac30bb
730 changed files with 2576 additions and 3788 deletions

View File

@@ -154,6 +154,10 @@ Skills own workflows; root owns hard policy and routing.
- Inline simple one-use objects/spreads when clearer. Extract only when it removes duplication or hard logic.
- Tests prove behavior/regressions, not every internal branch.
- For non-trivial refactors, check `git diff --numstat` before closeout. If LOC grew, trim or explain why.
- Prefer existing narrow helpers over repeated casts/guards. Add local helpers when 2+ nearby call sites share real boundary logic.
- Prefer ctor parameter properties for injected deps/config. Do not ban them for erasable-syntax purity.
- Prefer `satisfies` for registries/config maps; derive types from schemas when a runtime schema already exists.
- Table-drive repetitive tests when it reduces code and keeps failure names clear.
- Dynamic import: no static+dynamic import for same prod module. Use `*.runtime.ts` lazy boundary. After edits: `pnpm build`; check `[INEFFECTIVE_DYNAMIC_IMPORT]`.
- Cycles: keep `pnpm check:import-cycles` + architecture/madge green.
- Classes: no prototype mixins/mutations. Prefer inheritance/composition. Tests prefer per-instance stubs.

View File

@@ -6,7 +6,6 @@ import Foundation
enum HostEnvSecurityPolicy {
static let blockedInheritedKeys: Set<String> = [
"_JAVA_OPTIONS",
"AMQP_URL",
"ANSIBLE_CALLBACK_PLUGINS",
"ANSIBLE_COLLECTIONS_PATH",
@@ -31,12 +30,11 @@ enum HostEnvSecurityPolicy {
"AZURE_CLIENT_SECRET",
"BASH_ENV",
"BROWSER",
"BUN_CONFIG_REGISTRY",
"BUNDLE_GEMFILE",
"BUN_CONFIG_REGISTRY",
"BZR_EDITOR",
"BZR_PLUGIN_PATH",
"BZR_SSH",
"C_INCLUDE_PATH",
"CARGO_BUILD_RUSTC",
"CARGO_BUILD_RUSTC_WRAPPER",
"CARGO_HOME",
@@ -46,8 +44,8 @@ enum HostEnvSecurityPolicy {
"CGO_CFLAGS",
"CGO_LDFLAGS",
"CLASSPATH",
"CMAKE_C_COMPILER",
"CMAKE_CXX_COMPILER",
"CMAKE_C_COMPILER",
"CMAKE_TOOLCHAIN_FILE",
"COMPOSER_HOME",
"CONFIG_SHELL",
@@ -58,6 +56,7 @@ enum HostEnvSecurityPolicy {
"CPLUS_INCLUDE_PATH",
"CURL_HOME",
"CXX",
"C_INCLUDE_PATH",
"DATABASE_URL",
"DENO_DIR",
"DOTNET_ADDITIONAL_DEPS",
@@ -75,6 +74,8 @@ enum HostEnvSecurityPolicy {
"GEM_HOME",
"GEM_PATH",
"GH_TOKEN",
"GITHUB_TOKEN",
"GITLAB_TOKEN",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_ASKPASS",
"GIT_COMMON_DIR",
@@ -95,8 +96,6 @@ enum HostEnvSecurityPolicy {
"GIT_SSL_NO_VERIFY",
"GIT_TEMPLATE_DIR",
"GIT_WORK_TREE",
"GITHUB_TOKEN",
"GITLAB_TOKEN",
"GLIBC_TUNABLES",
"GOENV",
"GOFLAGS",
@@ -145,8 +144,8 @@ enum HostEnvSecurityPolicy {
"PERL5DBCMD",
"PERL5LIB",
"PERL5OPT",
"PHP_INI_SCAN_DIR",
"PHPRC",
"PHP_INI_SCAN_DIR",
"PIP_CONFIG_FILE",
"PIP_EXTRA_INDEX_URL",
"PIP_FIND_LINKS",
@@ -160,17 +159,17 @@ enum HostEnvSecurityPolicy {
"PYTHONPATH",
"PYTHONSTARTUP",
"PYTHONUSERBASE",
"R_ENVIRON",
"R_ENVIRON_USER",
"R_LIBS_USER",
"R_PROFILE",
"R_PROFILE_USER",
"REDIS_URL",
"RUBYLIB",
"RUBYOPT",
"RUBYSHELL",
"RUSTC_WRAPPER",
"RUSTFLAGS",
"R_ENVIRON",
"R_ENVIRON_USER",
"R_LIBS_USER",
"R_PROFILE",
"R_PROFILE_USER",
"SBT_OPTS",
"SHELL",
"SHELLOPTS",
@@ -192,7 +191,8 @@ enum HostEnvSecurityPolicy {
"VIRTUAL_ENV",
"VISUAL",
"WGETRC",
"YARN_RC_FILENAME"
"YARN_RC_FILENAME",
"_JAVA_OPTIONS"
]
static let blockedInheritedPrefixes: [String] = [
@@ -202,7 +202,6 @@ enum HostEnvSecurityPolicy {
]
static let blockedKeys: Set<String> = [
"_JAVA_OPTIONS",
"ANT_OPTS",
"BASH_ENV",
"BROWSER",
@@ -213,8 +212,8 @@ enum HostEnvSecurityPolicy {
"CARGO_BUILD_RUSTC_WRAPPER",
"CATALINA_OPTS",
"CC",
"CMAKE_C_COMPILER",
"CMAKE_CXX_COMPILER",
"CMAKE_C_COMPILER",
"CMAKE_TOOLCHAIN_FILE",
"CONFIG_SHELL",
"CONFIG_SITE",
@@ -275,14 +274,14 @@ enum HostEnvSecurityPolicy {
"PYTHONBREAKPOINT",
"PYTHONHOME",
"PYTHONPATH",
"R_ENVIRON",
"R_ENVIRON_USER",
"R_PROFILE",
"R_PROFILE_USER",
"RUBYLIB",
"RUBYOPT",
"RUBYSHELL",
"RUSTC_WRAPPER",
"R_ENVIRON",
"R_ENVIRON_USER",
"R_PROFILE",
"R_PROFILE_USER",
"SBT_OPTS",
"SHELL",
"SHELLOPTS",
@@ -291,7 +290,8 @@ enum HostEnvSecurityPolicy {
"SVN_EDITOR",
"SVN_SSH",
"VAGRANT_VAGRANTFILE",
"VIMINIT"
"VIMINIT",
"_JAVA_OPTIONS"
]
static let blockedOverrideKeys: Set<String> = [
@@ -321,9 +321,8 @@ enum HostEnvSecurityPolicy {
"AZURE_AUTH_LOCATION",
"AZURE_CLIENT_ID",
"AZURE_CLIENT_SECRET",
"BUN_CONFIG_REGISTRY",
"BUNDLE_GEMFILE",
"C_INCLUDE_PATH",
"BUN_CONFIG_REGISTRY",
"CARGO_BUILD_RUSTC_WRAPPER",
"CARGO_HOME",
"CFLAGS",
@@ -336,6 +335,7 @@ enum HostEnvSecurityPolicy {
"CPLUS_INCLUDE_PATH",
"CURL_CA_BUNDLE",
"CURL_HOME",
"C_INCLUDE_PATH",
"DATABASE_URL",
"DENO_DIR",
"DOCKER_CERT_PATH",
@@ -347,6 +347,8 @@ enum HostEnvSecurityPolicy {
"GEM_HOME",
"GEM_PATH",
"GH_TOKEN",
"GITHUB_TOKEN",
"GITLAB_TOKEN",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_ASKPASS",
"GIT_COMMON_DIR",
@@ -362,8 +364,6 @@ enum HostEnvSecurityPolicy {
"GIT_SSL_CAPATH",
"GIT_SSL_NO_VERIFY",
"GIT_WORK_TREE",
"GITHUB_TOKEN",
"GITLAB_TOKEN",
"GOENV",
"GOFLAGS",
"GONOPROXY",
@@ -378,8 +378,8 @@ enum HostEnvSecurityPolicy {
"HGRCPATH",
"HISTFILE",
"HOME",
"HTTP_PROXY",
"HTTPS_PROXY",
"HTTP_PROXY",
"KUBECONFIG",
"LDFLAGS",
"LESSCLOSE",
@@ -391,10 +391,10 @@ enum HostEnvSecurityPolicy {
"MANPAGER",
"MFLAGS",
"MONGODB_URI",
"NO_PROXY",
"NODE_AUTH_TOKEN",
"NODE_EXTRA_CA_CERTS",
"NODE_TLS_REJECT_UNAUTHORIZED",
"NO_PROXY",
"NPM_TOKEN",
"OBJC_INCLUDE_PATH",
"OPENSSL_CONF",
@@ -402,8 +402,8 @@ enum HostEnvSecurityPolicy {
"PAGER",
"PERL5DB",
"PERL5DBCMD",
"PHP_INI_SCAN_DIR",
"PHPRC",
"PHP_INI_SCAN_DIR",
"PIP_CONFIG_FILE",
"PIP_EXTRA_INDEX_URL",
"PIP_FIND_LINKS",
@@ -413,11 +413,11 @@ enum HostEnvSecurityPolicy {
"PROMPT_COMMAND",
"PYTHONSTARTUP",
"PYTHONUSERBASE",
"R_LIBS_USER",
"REDIS_URL",
"REQUESTS_CA_BUNDLE",
"RUSTC_WRAPPER",
"RUSTFLAGS",
"R_LIBS_USER",
"SSH_ASKPASS",
"SSH_AUTH_SOCK",
"SSL_CERT_DIR",

View File

@@ -1,2 +1,2 @@
1d3e6177eeac57fc43736f7d5f76d8f825e1859ca625d268e97dc30b5567ea34 plugin-sdk-api-baseline.json
6c093ff7c10bd81ee9d2c4fc5d07b206bc3a1f5acd0bad491cfc9e0df6689f6b plugin-sdk-api-baseline.jsonl
374f1fec7d6fa8c00865dcb58b68d89ec10e85e81ef536c5746167a83d10bcc7 plugin-sdk-api-baseline.json
ffc6a2faf381d1bb118845e010b2798397c3d41fff400f52ee57b6dc197c8af3 plugin-sdk-api-baseline.jsonl

View File

@@ -19,7 +19,9 @@ import {
type AcpRuntimeTurnResult,
} from "acpx/runtime";
import { redactSensitiveText } from "openclaw/plugin-sdk/security-runtime";
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
import { AcpRuntimeError, type AcpRuntime, type AcpRuntimeErrorCode } from "../runtime-api.js";
import { splitCommandParts } from "./command-line.js";
import {
createAcpxProcessLeaseId,
hashAcpxProcessCommand,
@@ -32,7 +34,6 @@ import {
isOpenClawLeaseAwareAcpxProcessCommand,
type AcpxProcessCleanupDeps,
} from "./process-reaper.js";
import { splitCommandParts } from "./command-line.js";
type AcpSessionStore = AcpRuntimeOptions["sessionStore"];
type AcpSessionRecord = Parameters<AcpSessionStore["save"]>[0];
@@ -189,7 +190,7 @@ function selectCurrentSessionLease(params: {
sessionKeys: string[];
rootPid?: number;
}): AcpxProcessLease | undefined {
const sessionKeys = new Set(params.sessionKeys.map((entry) => entry.trim()).filter(Boolean));
const sessionKeys = new Set(normalizeStringEntries(params.sessionKeys));
const candidates = params.leases.filter((lease) => sessionKeys.has(lease.sessionKey));
if (params.rootPid) {
return candidates.find((lease) => lease.rootPid === params.rootPid);

View File

@@ -20,6 +20,12 @@ import {
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import { parseAgentSessionKey, parseThreadSessionSuffix } from "openclaw/plugin-sdk/routing";
import { isPathInside, replaceFileAtomic } from "openclaw/plugin-sdk/security-runtime";
import {
asOptionalRecord as asRecord,
normalizeOptionalString,
normalizeStringEntries,
uniqueStrings,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { tempWorkspace, resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
const DEFAULT_TIMEOUT_MS = 15_000;
@@ -313,11 +319,6 @@ function withToggleStoreLock<T>(statePath: string, task: () => Promise<T>): Prom
return withLock(task);
}
function asRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
type ActiveMemoryThinkingLevel =
| "off"
| "minimal"
@@ -571,10 +572,6 @@ function resolveCanonicalSessionKeyFromSessionId(params: {
}
}
function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function formatRuntimeToolsAllowSource(toolsAllow: readonly string[]): string {
return `runtime toolsAllow: ${toolsAllow.join(", ")}`;
}
@@ -877,9 +874,7 @@ function normalizePluginConfig(
: [];
return {
enabled: raw.enabled !== false,
agents: Array.isArray(raw.agents)
? raw.agents.map((agentId) => agentId.trim()).filter(Boolean)
: [],
agents: Array.isArray(raw.agents) ? normalizeStringEntries(raw.agents) : [],
model: typeof raw.model === "string" && raw.model.trim() ? raw.model.trim() : undefined,
modelFallback:
typeof raw.modelFallback === "string" && raw.modelFallback.trim()
@@ -1518,11 +1513,11 @@ function buildPluginDebugLine(params: {
warning && action && !cleaned
? `${warning} ${action}`
: [warning, action && !cleaned ? action : ""]
.filter((value, index, values) => Boolean(value) && values.indexOf(value) === index)
.filter((value): value is string => Boolean(value))
.join(" | ");
const messages = [warningAction, cleaned]
.filter((value, index, values) => Boolean(value) && values.indexOf(value) === index)
.join(" | ");
const messages = uniqueStrings(
[warningAction, cleaned].filter((value): value is string => Boolean(value)),
).join(" | ");
const trailing = messages;
if (prefix && trailing) {
return `${ACTIVE_MEMORY_DEBUG_PREFIX} ${prefix} | ${trailing}`;

View File

@@ -1,6 +1,7 @@
import { randomUUID } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { dispatchGatewayMethod } from "openclaw/plugin-sdk/gateway-method-runtime";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { isAdminHttpRpcAllowedMethod, listAdminHttpRpcAllowedMethods } from "./methods.js";
const DEFAULT_RPC_BODY_BYTES = 1024 * 1024;
@@ -38,10 +39,6 @@ type ParsedRequest = {
params?: unknown;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function createError(code: string, message: string): RpcError {
return { code, message };
}

View File

@@ -4,7 +4,10 @@ import {
type MemoryEmbeddingProvider,
type MemoryEmbeddingProviderCreateOptions,
} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asOptionalRecord as asRecord,
normalizeLowercaseStringOrEmpty,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { refreshAwsSharedConfigCacheForBedrock } from "./aws-credential-refresh.js";
// ---------------------------------------------------------------------------
@@ -258,12 +261,6 @@ function asNumberArray(value: unknown): number[] {
return value;
}
function asRecord(value: unknown): Record<string, unknown> | undefined {
return typeof value === "object" && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function asNumberArrayBatch(value: unknown): number[][] {
if (!Array.isArray(value)) {
throw malformedBedrockEmbeddingResponse();

View File

@@ -3,7 +3,10 @@ import {
type OpenClawConfig,
type ProviderAuthResult,
} from "openclaw/plugin-sdk/provider-auth";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
isRecord,
normalizeLowercaseStringOrEmpty,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveClaudeCliAnthropicModelRefs } from "./claude-model-refs.js";
import {
readClaudeCliCredentialsForSetup,
@@ -31,10 +34,6 @@ function toAnthropicSelectedModelRef(raw: string): string | undefined {
return resolved?.rewriteRef ?? resolved?.selectedRef;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function rewriteModelSelection(model: AgentDefaultsModel): {
value: AgentDefaultsModel;
primary?: string;

View File

@@ -1,4 +1,8 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
import {
isRecord,
normalizeLowercaseStringOrEmpty,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import {
resolveClaudeCliAnthropicModelRefs,
resolveKnownAnthropicModelRef,
@@ -8,10 +12,6 @@ import { CLAUDE_CLI_BACKEND_ID, CLAUDE_CLI_DEFAULT_ALLOWLIST_REFS } from "./cli-
const ANTHROPIC_PROVIDER_API = "anthropic-messages";
const ANTHROPIC_API_KEY_DEFAULT_ALLOWLIST_REFS = ["anthropic/claude-sonnet-4-6"] as const;
function normalizeLowercaseStringOrEmpty(value: unknown): string {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
function normalizeProviderId(provider: string): string {
const normalized = normalizeLowercaseStringOrEmpty(provider);
if (normalized === "bedrock" || normalized === "aws-bedrock") {
@@ -20,10 +20,6 @@ function normalizeProviderId(provider: string): string {
return normalized;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function resolveAnthropicDefaultAuthMode(
config: OpenClawConfig,
env: NodeJS.ProcessEnv,

View File

@@ -13,7 +13,9 @@ import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
import {
normalizeFastMode,
normalizeLowercaseStringOrEmpty,
normalizeStringEntries,
readStringValue,
uniqueStrings,
} from "openclaw/plugin-sdk/string-coerce-runtime";
const log = createSubsystemLogger("anthropic-stream");
@@ -48,10 +50,7 @@ function parseHeaderList(value: unknown): string[] {
if (typeof value !== "string") {
return [];
}
return value
.split(",")
.map((item) => item.trim())
.filter(Boolean);
return normalizeStringEntries(value.split(","));
}
function mergeAnthropicBetaHeader(
@@ -63,7 +62,7 @@ function mergeAnthropicBetaHeader(
(key) => normalizeLowercaseStringOrEmpty(key) === "anthropic-beta",
);
const existing = existingKey ? parseHeaderList(merged[existingKey]) : [];
const values = Array.from(new Set([...existing, ...betas]));
const values = uniqueStrings([...existing, ...betas]);
const key = existingKey ?? "anthropic-beta";
merged[key] = values.join(",");
return merged;
@@ -138,7 +137,7 @@ export function createAnthropicBetaHeadersWrapper(
const piAiBetas = isOauth
? (PI_AI_OAUTH_ANTHROPIC_BETAS as readonly string[])
: (PI_AI_DEFAULT_ANTHROPIC_BETAS as readonly string[]);
const allBetas = [...new Set([...piAiBetas, ...effectiveBetas])];
const allBetas = uniqueStrings([...piAiBetas, ...effectiveBetas]);
return underlying(model, context, {
...options,
headers: mergeAnthropicBetaHeader(options?.headers, allBetas),

View File

@@ -5,6 +5,7 @@ import type {
WebSearchProviderToolDefinition,
} from "openclaw/plugin-sdk/provider-web-search";
import { createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-config-contract";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
const BRAVE_CREDENTIAL_PATH = "plugins.entries.brave.config.webSearch.apiKey";
@@ -61,10 +62,6 @@ const BraveSearchSchema = {
},
} satisfies Record<string, unknown>;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function resolveProviderWebSearchPluginConfig(
config: unknown,
pluginId: string,

View File

@@ -2,10 +2,7 @@ import {
createWebSearchProviderContractFields,
type WebSearchProviderPlugin,
} from "openclaw/plugin-sdk/provider-web-search-config-contract";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
function resolveLegacyTopLevelBraveCredential(
config: unknown,

View File

@@ -10,6 +10,8 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import {
normalizeOptionalString,
readStringValue,
uniqueStrings,
uniqueValues,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
import { redactToolPayloadText } from "../logging/redact.js";
@@ -558,7 +560,7 @@ async function terminateChromeMcpProcessTree(
const killProcess = deps?.killProcess ?? ((pid, signal) => process.kill(pid, signal));
const sleep = deps?.sleep ?? sleepTimeout;
const pids = Array.from(new Set([...descendantPids.toReversed(), rootPid])).filter(
const pids = uniqueValues([...descendantPids.toReversed(), rootPid]).filter(
(pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid,
);
const signaled: number[] = [];
@@ -1107,7 +1109,7 @@ export async function closeChromeMcpSession(profileName: string): Promise<boolea
}
export async function stopAllChromeMcpSessions(): Promise<void> {
const names = [...new Set([...sessions.keys()].map((key) => JSON.parse(key)[0] as string))];
const names = uniqueStrings([...sessions.keys()].map((key) => JSON.parse(key)[0] as string));
for (const name of names) {
await closeChromeMcpSession(name).catch(() => {});
}

View File

@@ -1,3 +1,4 @@
import { uniqueValues } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { CDPSession, Page } from "playwright-core";
type PageCdpSend = (method: string, params?: Record<string, unknown>) => Promise<unknown>;
@@ -72,7 +73,7 @@ export async function markBackendDomRefsOnPage(opts: {
await send("DOM.enable").catch(() => {});
const backendNodeIds = [...new Set(refs.map((entry) => Math.floor(entry.backendDOMNodeId)))];
const backendNodeIds = uniqueValues(refs.map((entry) => Math.floor(entry.backendDOMNodeId)));
const pushed = (await send("DOM.pushNodesByBackendIdsToFrontend", {
backendNodeIds,
}).catch(() => ({}))) as { nodeIds?: number[] };

View File

@@ -1,11 +1,8 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { ensurePageState, getPageForTargetId } from "./pw-session.js";
import { normalizeTimeoutMs } from "./pw-tools-core.shared.js";
import { matchBrowserUrlPattern } from "./url-pattern.js";
function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === "string" ? value.trim() || undefined : undefined;
}
export async function responseBodyViaPlaywright(opts: {
cdpUrl: string;
targetId?: string;

View File

@@ -1,3 +1,4 @@
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveBrowserNavigationProxyMode } from "../browser-proxy-mode.js";
import { toBrowserErrorResponse } from "../errors.js";
import {
@@ -10,10 +11,6 @@ import type { BrowserRouteContext, ProfileContext } from "../server-context.js";
import type { BrowserRequest, BrowserResponse } from "./types.js";
import { getProfileContext, jsonError } from "./utils.js";
function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === "string" ? value.trim() || undefined : undefined;
}
export const SELECTOR_UNSUPPORTED_MESSAGE = [
"Error: 'selector' is not supported. Use 'ref' from snapshot instead.",
"",

View File

@@ -1,3 +1,7 @@
import {
normalizeOptionalString,
readStringValue,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type { ResolvedBrowserProfile } from "../config.js";
import {
DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH,
@@ -11,14 +15,6 @@ import {
} from "../profile-capabilities.js";
import { toBoolean, toNumber, toStringOrEmpty } from "./utils.js";
function readStringValue(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}
function normalizeOptionalString(value: unknown): string | undefined {
return readStringValue(value)?.trim() || undefined;
}
type BrowserSnapshotPlan = {
format: "ai" | "aria";
mode?: "efficient";

View File

@@ -1,3 +1,7 @@
import {
normalizeOptionalString,
readStringValue,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type { BrowserRouteContext } from "../server-context.js";
import {
readBody,
@@ -8,14 +12,6 @@ import {
import type { BrowserRequest, BrowserResponse, BrowserRouteRegistrar } from "./types.js";
import { asyncBrowserRoute, jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js";
function readStringValue(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}
function normalizeOptionalString(value: unknown): string | undefined {
return readStringValue(value)?.trim() || undefined;
}
type StorageKind = "local" | "session";
export function parseStorageKind(raw: string): StorageKind | null {

View File

@@ -1,3 +1,4 @@
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { SsrFPolicy } from "../../infra/net/ssrf.js";
import { withCdpSocket } from "../cdp.helpers.js";
import { getChromeWebSocketUrl } from "../chrome.js";
@@ -57,7 +58,7 @@ function readPermissions(raw: unknown): string[] | null {
if (permissions.length !== raw.length) {
return null;
}
return [...new Set(permissions)];
return uniqueStrings(permissions);
}
async function grantPermissions(params: {

View File

@@ -1,3 +1,4 @@
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { SsrFPolicy } from "../infra/net/ssrf.js";
export function withAllowedHostname(
@@ -6,6 +7,6 @@ export function withAllowedHostname(
): SsrFPolicy {
return {
...ssrfPolicy,
allowedHostnames: Array.from(new Set([...(ssrfPolicy?.allowedHostnames ?? []), hostname])),
allowedHostnames: uniqueStrings([...(ssrfPolicy?.allowedHostnames ?? []), hostname]),
};
}

View File

@@ -1,4 +1,5 @@
import fsPromises from "node:fs/promises";
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
import { redactCdpUrl } from "../browser/cdp.helpers.js";
import { loadBrowserConfigForRuntimeRefresh } from "../browser/config-refresh-source.js";
import { resolveBrowserConfig } from "../browser/config.js";
@@ -40,7 +41,7 @@ const DEFAULT_BROWSER_PROXY_TIMEOUT_MS = 20_000;
const BROWSER_PROXY_STATUS_TIMEOUT_MS = 750;
function normalizeProfileAllowlist(raw?: string[]): string[] {
return Array.isArray(raw) ? raw.map((entry) => entry.trim()).filter(Boolean) : [];
return Array.isArray(raw) ? normalizeStringEntries(raw) : [];
}
function resolveBrowserProxyConfig() {

View File

@@ -13,7 +13,7 @@ import {
waitProviderOperationPollInterval,
type ProviderOperationTimeoutMs,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -43,10 +43,6 @@ type BytePlusTaskResponse = {
type BytePlusTaskStatus = "running" | "failed" | "queued" | "succeeded" | "cancelled";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
async function readBytePlusJsonResponse<T>(
response: Pick<Response, "json">,
label: string,

View File

@@ -1,12 +1,8 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { asOptionalRecord as readRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
type MutableRecord = Record<string, unknown>;
function readRecord(value: unknown): MutableRecord | undefined {
return isRecord(value) ? (value as MutableRecord) : undefined;
}
function mergeHostConfig(params: {
legacyHost: MutableRecord;
existingHost: MutableRecord | undefined;

View File

@@ -5,6 +5,11 @@ import {
resolvePluginConfigObject,
} from "openclaw/plugin-sdk/plugin-config-runtime";
import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
import {
asBoolean as readBoolean,
isRecord,
readStringValue as readString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
export type CanvasHostConfig = {
enabled?: boolean;
@@ -22,18 +27,6 @@ type CanvasPluginConfigSchema = {
uiHints: Record<string, { label: string; help?: string; advanced?: boolean }>;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function readBoolean(value: unknown): boolean | undefined {
return typeof value === "boolean" ? value : undefined;
}
function readString(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}
function readPositiveInteger(value: unknown): number | undefined {
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
}

View File

@@ -1,4 +1,5 @@
import { embeddedAgentLog } from "openclaw/plugin-sdk/agent-harness-runtime";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { JsonValue, v2 } from "./protocol.js";
export const CODEX_APP_INVENTORY_CACHE_TTL_MS = 60 * 60 * 1_000;
@@ -260,10 +261,6 @@ function fingerprintInventoryCacheKey(key: string): string {
return hash.toString(16).padStart(8, "0");
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function redactErrorData(value: unknown, depth = 0): JsonValue | undefined {
if (value === undefined) {
return undefined;

View File

@@ -9,6 +9,7 @@ import {
type NativeHookRelayRegistrationHandle,
runBeforeToolCallHook,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { normalizeTrimmedStringList } from "openclaw/plugin-sdk/string-coerce-runtime";
import { formatCodexDisplayText } from "../command-formatters.js";
import {
approvalRequestExplicitlyUnavailable,
@@ -877,10 +878,7 @@ function summarizeNetworkPolicyAmendments(value: JsonValue | undefined): string
}
function readStringArray(record: JsonObject, key: string): string[] {
const value = record[key];
return Array.isArray(value)
? value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean)
: [];
return normalizeTrimmedStringList(record[key]);
}
function sanitizePermissionHostValue(value: string): string {

View File

@@ -3,6 +3,7 @@ import {
type CompactEmbeddedPiSessionParams,
type EmbeddedPiCompactResult,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { asOptionalRecord as readRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
defaultCodexAppServerClientFactory,
type CodexAppServerClientFactory,
@@ -117,12 +118,6 @@ function readAgentIdFromSessionKey(sessionKey: string | undefined): string | und
return parts[1]?.trim() || undefined;
}
function readRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
async function compactCodexNativeThread(
params: CompactEmbeddedPiSessionParams,
options: { pluginConfig?: unknown; clientFactory?: CodexAppServerClientFactory } = {},

View File

@@ -1,6 +1,7 @@
import { createHmac, randomBytes } from "node:crypto";
import { readFileSync } from "node:fs";
import { hostname as readHostName } from "node:os";
import { normalizeTrimmedStringList } from "openclaw/plugin-sdk/string-coerce-runtime";
import { detectWindowsSpawnCommandInlineArgs } from "openclaw/plugin-sdk/windows-spawn";
import { z } from "zod";
import type { CodexSandboxPolicy, CodexServiceTier } from "./protocol.js";
@@ -991,12 +992,7 @@ function normalizeHeaders(value: unknown): Record<string, string> {
}
function normalizeStringList(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
return value
.map((entry) => readNonEmptyString(entry))
.filter((entry): entry is string => entry !== undefined);
return normalizeTrimmedStringList(value);
}
function readBooleanEnv(value: string | undefined): boolean | undefined {

View File

@@ -20,6 +20,10 @@ import {
wrapToolWithBeforeToolCallHook,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import {
asOptionalRecord as readRecord,
isRecord,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type { CodexDynamicToolsLoading } from "./config.js";
import { invalidInlineImageText, sanitizeInlineImageDataUrl } from "./image-payload-sanitizer.js";
import {
@@ -435,14 +439,6 @@ function extractInternalSourceReplyPayload(
: undefined;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function readRecord(value: unknown): Record<string, unknown> | undefined {
return isRecord(value) ? value : undefined;
}
function readPositiveInteger(value: unknown): number | undefined {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
return undefined;

View File

@@ -21,6 +21,11 @@ import {
type ToolProgressDetailMode,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
import {
asBoolean,
asFiniteNumber,
normalizeStringEntries,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveCodexLocalRuntimeAttribution } from "./local-runtime-attribution.js";
import {
readCodexNotificationThreadId,
@@ -807,9 +812,7 @@ export class CodexAppServerEventProjector {
}
private buildToolMediaUrls(toolTelemetry: CodexAppServerToolTelemetry): string[] | undefined {
const mediaUrls = new Set(
toolTelemetry.toolMediaUrls?.map((url) => url.trim()).filter(Boolean) ?? [],
);
const mediaUrls = new Set(normalizeStringEntries(toolTelemetry.toolMediaUrls ?? []));
if ((toolTelemetry.messagingToolSentMediaUrls?.length ?? 0) === 0) {
for (const mediaUrl of this.nativeGeneratedMediaUrls) {
mediaUrls.add(mediaUrl);
@@ -1581,13 +1584,11 @@ function readNullableString(record: JsonObject, key: string): string | null | un
}
function readNumber(record: JsonObject, key: string): number | undefined {
const value = record[key];
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
return asFiniteNumber(record[key]);
}
function readBoolean(record: JsonObject, key: string): boolean | undefined {
const value = record[key];
return typeof value === "boolean" ? value : undefined;
return asBoolean(record[key]);
}
function readBooleanAlias(record: JsonObject, keys: readonly string[]): boolean | undefined {

View File

@@ -1,3 +1,5 @@
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
const DATA_URL_PREFIX = "data:";
const IMAGE_OMITTED_TEXT = "omitted image payload: invalid inline image data";
const IMAGE_SIGNATURES: Array<{
@@ -105,8 +107,7 @@ function parseImageDataUrl(value: string):
function metadataAllowsImageBase64(metadata: string[]): boolean {
const [mimeType, ...options] = metadata;
const isImageMimeType =
mimeType !== undefined && mimeType.toLowerCase().startsWith("image/");
const isImageMimeType = mimeType !== undefined && mimeType.toLowerCase().startsWith("image/");
return isImageMimeType && options.some((part) => part.toLowerCase() === "base64");
}
@@ -137,10 +138,6 @@ export function invalidInlineImageText(label: string): string {
return `[${label}] ${IMAGE_OMITTED_TEXT}`;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function sanitizeImageContentRecord(
record: Record<string, unknown>,
label: string,

View File

@@ -1,3 +1,4 @@
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { resolveCodexAppServerAuthProfileIdForAgent } from "./auth-bridge.js";
import type { CodexAppServerClient } from "./client.js";
import type { CodexAppServerStartOptions } from "./config.js";
@@ -156,7 +157,7 @@ function readReasoningEfforts(value: CodexReasoningEffortOption[]): string[] {
const efforts = value
.map((entry) => readNonEmptyString(entry.reasoningEffort))
.filter((entry): entry is string => entry !== undefined);
return [...new Set(efforts)];
return uniqueStrings(efforts);
}
function readNonEmptyString(value: unknown): string | undefined {

View File

@@ -10,6 +10,7 @@ import {
type AgentHarnessTaskRuntime,
type AgentHarnessTaskRecord,
} from "openclaw/plugin-sdk/agent-harness-task-runtime";
import { asFiniteNumber, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { CodexAppServerClient } from "./client.js";
import {
extractCodexNativeSubagentCompletions,
@@ -842,11 +843,6 @@ function readString(record: JsonObject | undefined, key: string): string | undef
return typeof value === "string" ? value : undefined;
}
function normalizeOptionalString(value: string | undefined): string | undefined {
const normalized = value?.trim();
return normalized || undefined;
}
function readStringArray(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
@@ -1037,8 +1033,7 @@ function readTranscriptParentThreadId(payload: JsonObject): string | undefined {
}
function readNumber(record: JsonObject, key: string): number | undefined {
const value = record[key];
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
return asFiniteNumber(record[key]);
}
function secondsToMillis(value: number | undefined): number | undefined {

View File

@@ -1,3 +1,4 @@
import { asFiniteNumber } from "openclaw/plugin-sdk/string-coerce-runtime";
import { isJsonObject, type JsonObject, type JsonValue } from "./protocol.js";
const CODEX_LIMIT_ID = "codex";
@@ -603,8 +604,7 @@ function readNullableString(record: JsonObject, key: string): string | undefined
}
function readNumber(record: JsonObject, key: string): number | undefined {
const value = record[key];
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
return asFiniteNumber(record[key]);
}
function normalizeText(value: string | null | undefined): string | undefined {

View File

@@ -55,6 +55,7 @@ import {
} from "openclaw/plugin-sdk/diagnostic-runtime";
import { isToolAllowed } from "openclaw/plugin-sdk/sandbox";
import { pathExists } from "openclaw/plugin-sdk/security-runtime";
import { asBoolean } from "openclaw/plugin-sdk/string-coerce-runtime";
import { defaultCodexAppInventoryCache } from "./app-inventory-cache.js";
import { handleCodexAppServerApprovalRequest } from "./approval-bridge.js";
import {
@@ -4990,8 +4991,7 @@ function readString(record: JsonObject, key: string): string | undefined {
}
function readBoolean(record: JsonObject, key: string): boolean | undefined {
const value = record[key];
return typeof value === "boolean" ? value : undefined;
return asBoolean(record[key]);
}
async function readMirroredSessionHistoryMessages(

View File

@@ -10,16 +10,12 @@ import {
type EmbeddedRunAttemptParams,
type SessionWriteLockAcquireTimeoutConfig,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
type MirroredAgentMessage = Extract<AgentMessage, { role: "user" | "assistant" | "toolResult" }>;
const MIRROR_IDENTITY_META_KEY = "mirrorIdentity" as const;
function normalizeOptionalString(value: string | null | undefined): string | undefined {
const normalized = value?.trim();
return normalized ? normalized : undefined;
}
function buildSenderLabel(params: {
senderId?: string;
senderName?: string;

View File

@@ -10,6 +10,7 @@ import {
type AuthProfileStore,
} from "openclaw/plugin-sdk/agent-runtime";
import type { PluginCommandContext } from "openclaw/plugin-sdk/plugin-entry";
import { normalizeUniqueStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
import { CODEX_CONTROL_METHODS, type CodexControlMethod } from "./app-server/capabilities.js";
import { isJsonObject, type JsonObject, type JsonValue } from "./app-server/protocol.js";
import { rememberCodexRateLimits } from "./app-server/rate-limit-cache.js";
@@ -149,7 +150,7 @@ function resolveDisplayAuthOrder(params: {
resolveOrder(params.store.order, OPENAI_CODEX_PROVIDER_ID) ??
resolveOrder(params.config?.auth?.order, OPENAI_CODEX_PROVIDER_ID);
if (codexOrder && codexOrder.length > 0) {
return { order: dedupe(codexOrder), explicit: true };
return { order: normalizeUniqueStringEntries(codexOrder), explicit: true };
}
const order = resolveAuthProfileOrder({
cfg: params.config,
@@ -573,17 +574,3 @@ function formatRelativeReset(untilMs: number, nowMs: number): string {
const days = Math.ceil(durationMs / dayMs);
return `in ${days} ${days === 1 ? "day" : "days"}`;
}
function dedupe(values: string[]): string[] {
const seen = new Set<string>();
const result: string[] = [];
for (const value of values) {
const trimmed = value.trim();
if (!trimmed || seen.has(trimmed)) {
continue;
}
seen.add(trimmed);
result.push(trimmed);
}
return result;
}

View File

@@ -1,6 +1,7 @@
import crypto from "node:crypto";
import { resolveAgentDir, resolveSessionAgentIds } from "openclaw/plugin-sdk/agent-runtime";
import type { PluginCommandContext, PluginCommandResult } from "openclaw/plugin-sdk/plugin-entry";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { CODEX_CONTROL_METHODS, type CodexControlMethod } from "./app-server/capabilities.js";
import {
installCodexComputerUse,
@@ -2104,8 +2105,3 @@ function normalizeComputerUseStringOverrides(
}
return normalized;
}
function normalizeOptionalString(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed || undefined;
}

View File

@@ -1,5 +1,6 @@
import process from "node:process";
import type { PluginConversationBinding } from "openclaw/plugin-sdk/plugin-entry";
import { asOptionalRecord as readRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
const BINDING_DATA_VERSION = 1;
@@ -112,12 +113,6 @@ export function resolveCodexDefaultWorkspaceDir(pluginConfig: unknown): string {
return configured ?? process.cwd();
}
function readRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function readString(record: Record<string, unknown> | undefined, key: string) {
const value = record?.[key];
return typeof value === "string" && value.trim() ? value.trim() : undefined;

View File

@@ -1,3 +1,4 @@
import { asOptionalRecord as readRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
readCodexNotificationThreadId,
readCodexNotificationTurnId,
@@ -173,12 +174,6 @@ function readNotificationTurnId(params: JsonObject): string | undefined {
return readCodexNotificationTurnId(params);
}
function readRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function readString(record: Record<string, unknown> | JsonObject | undefined, key: string) {
const value = record?.[key];
return typeof value === "string" && value.trim() ? value.trim() : undefined;

View File

@@ -1,6 +1,7 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { PluginHookInboundClaimEvent } from "openclaw/plugin-sdk/plugin-entry";
import { normalizeSingleOrTrimmedStringList } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { CodexUserInput } from "./app-server/protocol.js";
type InboundMedia = {
@@ -28,10 +29,14 @@ function extractInboundMedia(event: PluginHookInboundClaimEvent): InboundMedia[]
// OpenClaw channels expose either local staged files or remote URLs. Keep
// them separate so Codex can receive the cheaper localImage input when a file
// is already present, while still supporting remote-only transports.
const paths = readStringArray(metadata.mediaPaths).concat(readStringArray(metadata.mediaPath));
const urls = readStringArray(metadata.mediaUrls).concat(readStringArray(metadata.mediaUrl));
const mimeTypes = readStringArray(metadata.mediaTypes).concat(
readStringArray(metadata.mediaType),
const paths = normalizeSingleOrTrimmedStringList(metadata.mediaPaths).concat(
normalizeSingleOrTrimmedStringList(metadata.mediaPath),
);
const urls = normalizeSingleOrTrimmedStringList(metadata.mediaUrls).concat(
normalizeSingleOrTrimmedStringList(metadata.mediaUrl),
);
const mimeTypes = normalizeSingleOrTrimmedStringList(metadata.mediaTypes).concat(
normalizeSingleOrTrimmedStringList(metadata.mediaType),
);
const count = Math.max(paths.length, urls.length, mimeTypes.length);
const media: InboundMedia[] = [];
@@ -94,13 +99,3 @@ function readLocalMediaPath(value: string | undefined): string | undefined {
}
return /^[a-z][a-z0-9+.-]*:/i.test(value) ? undefined : value;
}
function readStringArray(value: unknown): string[] {
if (typeof value === "string" && value.trim()) {
return [value.trim()];
}
if (!Array.isArray(value)) {
return [];
}
return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
}

View File

@@ -20,6 +20,7 @@ import type {
MigrationPlan,
MigrationProviderContext,
} from "openclaw/plugin-sdk/plugin-entry";
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { defaultCodexAppInventoryCache } from "../app-server/app-inventory-cache.js";
import {
resolveCodexAppServerAuthAccountCacheKey,
@@ -176,8 +177,8 @@ export async function applyCodexMigrationPlan(params: {
reportDir,
};
if (items.some(isCodexPluginLoadWarningItem)) {
result.warnings = [...new Set([...(result.warnings ?? []), CODEX_PLUGIN_LOAD_WARNING])];
result.nextSteps = [...new Set([CODEX_PLUGIN_LOAD_WARNING, ...(result.nextSteps ?? [])])];
result.warnings = uniqueStrings([...(result.warnings ?? []), CODEX_PLUGIN_LOAD_WARNING]);
result.nextSteps = uniqueStrings([CODEX_PLUGIN_LOAD_WARNING, ...(result.nextSteps ?? [])]);
}
await writeMigrationReport(result, { title: "Codex Migration Report" });
return result;

View File

@@ -17,6 +17,10 @@ import {
type OpenClawConfig,
type ProviderAuthResult,
} from "openclaw/plugin-sdk/provider-auth";
import {
isRecord,
normalizeOptionalString as readString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { readJsonObject } from "./helpers.js";
import type { CodexSource } from "./source.js";
import type { resolveCodexMigrationTargets } from "./targets.js";
@@ -65,14 +69,6 @@ type CodexAuthConfigApplyResult = "configured" | "conflict" | "unavailable";
class CodexAuthConfigConflict extends Error {}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function readString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
const payload = token.split(".")[1];
if (!payload) {

View File

@@ -12,6 +12,7 @@ import type {
MigrationPlan,
MigrationProviderContext,
} from "openclaw/plugin-sdk/plugin-entry";
import { asBoolean, isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { CODEX_PLUGINS_MARKETPLACE_NAME } from "../app-server/config.js";
import { buildCodexAuthItems } from "./auth.js";
import { exists, sanitizeName } from "./helpers.js";
@@ -261,11 +262,7 @@ function readExistingAllowDestructiveActions(
...CODEX_PLUGIN_NATIVE_CONFIG_PATH,
"allow_destructive_actions",
]);
return typeof value === "boolean" ? value : undefined;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
return asBoolean(value);
}
export function buildCodexPluginsConfigValue(

View File

@@ -8,6 +8,7 @@ import type {
OpenClawPluginNodeInvokePolicy,
} from "openclaw/plugin-sdk/plugin-entry";
import type { PluginRuntime } from "openclaw/plugin-sdk/plugin-runtime";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import {
materializeWindowsSpawnProgram,
@@ -705,7 +706,3 @@ function readNodeId(node: CodexCliSessionNodeInfo): string {
function formatNodeLabel(node: CodexCliSessionNodeInfo): string {
return [node.displayName, node.nodeId, node.remoteIp].filter(Boolean).join(" / ") || "node";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}

View File

@@ -25,9 +25,11 @@ import {
type SsrFPolicy,
} from "openclaw/plugin-sdk/ssrf-runtime";
import {
asBoolean,
isRecord,
normalizeOptionalLowercaseString,
normalizeOptionalString,
uniqueStrings,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveUserPath } from "openclaw/plugin-sdk/text-utility-runtime";
@@ -112,8 +114,7 @@ export function setComfyFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | n
}
function readConfigBoolean(config: ComfyProviderConfig, key: string): boolean | undefined {
const value = config[key];
return typeof value === "boolean" ? value : undefined;
return asBoolean(config[key]);
}
function readConfigInteger(config: ComfyProviderConfig, key: string): number | undefined {
@@ -822,6 +823,6 @@ export async function runComfyWorkflow(params: {
assets,
model: providerModel,
promptId,
outputNodeIds: Array.from(new Set(outputFiles.map((entry) => entry.nodeId))),
outputNodeIds: uniqueStrings(outputFiles.map((entry) => entry.nodeId)),
};
}

View File

@@ -1,3 +1,4 @@
import { normalizeStringEntries, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
definePluginEntry,
type ProviderAuthContext,
@@ -43,11 +44,8 @@ function validateBaseUrl(value: string): string | undefined {
}
function parseModelIds(input: string): string[] {
const parsed = input
.split(/[\n,]/)
.map((model) => model.trim())
.filter(Boolean);
return Array.from(new Set(parsed));
const parsed = normalizeStringEntries(input.split(/[\n,]/));
return uniqueStrings(parsed);
}
function buildModelDefinition(modelId: string) {

View File

@@ -9,6 +9,7 @@ import {
resolveProviderHttpRequestConfig,
requireTranscriptionText,
} from "openclaw/plugin-sdk/provider-http";
import { asOptionalRecord as asRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
export const DEFAULT_DEEPGRAM_AUDIO_BASE_URL = "https://api.deepgram.com/v1";
export const DEFAULT_DEEPGRAM_AUDIO_MODEL = "nova-3";
@@ -18,12 +19,6 @@ function resolveModel(model?: string): string {
return trimmed || DEFAULT_DEEPGRAM_AUDIO_MODEL;
}
function asRecord(value: unknown): Record<string, unknown> | undefined {
return typeof value === "object" && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function readDeepgramTranscript(payload: Record<string, unknown>): string | undefined {
const results = asRecord(payload.results);
if (!results) {

View File

@@ -6,7 +6,12 @@ import {
type RealtimeTranscriptionSessionCreateRequest,
} from "openclaw/plugin-sdk/realtime-transcription";
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asOptionalRecord as readRecord,
normalizeOptionalString,
parseBooleanValue as readBoolean,
parseFiniteNumber as readFiniteNumber,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { DEFAULT_DEEPGRAM_AUDIO_BASE_URL, DEFAULT_DEEPGRAM_AUDIO_MODEL } from "./audio.js";
type DeepgramRealtimeTranscriptionEncoding = "linear16" | "mulaw" | "alaw";
@@ -55,45 +60,12 @@ const DEEPGRAM_REALTIME_MAX_RECONNECT_ATTEMPTS = 5;
const DEEPGRAM_REALTIME_RECONNECT_DELAY_MS = 1000;
const DEEPGRAM_REALTIME_MAX_QUEUED_BYTES = 2 * 1024 * 1024;
function readRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function readNestedDeepgramConfig(rawConfig: RealtimeTranscriptionProviderConfig) {
const raw = readRecord(rawConfig);
const providers = readRecord(raw?.providers);
return readRecord(providers?.deepgram ?? raw?.deepgram ?? raw) ?? {};
}
function readFiniteNumber(value: unknown): number | undefined {
const next =
typeof value === "number"
? value
: typeof value === "string"
? Number.parseFloat(value)
: undefined;
return Number.isFinite(next) ? next : undefined;
}
function readBoolean(value: unknown): boolean | undefined {
if (typeof value === "boolean") {
return value;
}
if (typeof value !== "string") {
return undefined;
}
const normalized = value.trim().toLowerCase();
if (["1", "true", "yes", "on"].includes(normalized)) {
return true;
}
if (["0", "false", "no", "off"].includes(normalized)) {
return false;
}
return undefined;
}
function normalizeDeepgramEncoding(
value: unknown,
): DeepgramRealtimeTranscriptionEncoding | undefined {

View File

@@ -7,7 +7,10 @@ import {
postJsonRequest,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asFiniteNumber as coerceProviderNumber,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type {
GeneratedVideoAsset,
VideoGenerationProvider,
@@ -79,14 +82,6 @@ function parseVideoDataUrl(url: string): GeneratedVideoAsset | undefined {
};
}
function coerceProviderNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function coerceProviderString(value: unknown): string | undefined {
return normalizeOptionalString(value);
}
function resolveDurationSeconds(value: number | undefined): number | undefined {
if (typeof value !== "number" || !Number.isFinite(value)) {
return undefined;
@@ -115,11 +110,12 @@ function buildDeepInfraVideoBody(
body.seed = seed;
}
const negativePrompt =
coerceProviderString(options.negative_prompt) ?? coerceProviderString(options.negativePrompt);
normalizeOptionalString(options.negative_prompt) ??
normalizeOptionalString(options.negativePrompt);
if (negativePrompt) {
body.negative_prompt = negativePrompt;
}
const style = coerceProviderString(options.style);
const style = normalizeOptionalString(options.style);
if (style) {
body.style = style;
}

View File

@@ -1,4 +1,5 @@
import { resolveGlobalMap } from "openclaw/plugin-sdk/global-singleton";
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { DiscordComponentEntry, DiscordModalEntry } from "./components.js";
import { getOptionalDiscordRuntime } from "./runtime.js";
@@ -275,7 +276,7 @@ function resolveComponentConsumptionIds(entry: DiscordComponentEntry): string[]
return [entry.id];
}
const ids = entry.consumptionGroupEntryIds?.filter((id) => typeof id === "string" && id) ?? [];
return ids.length > 0 ? Array.from(new Set(ids)) : [entry.id];
return ids.length > 0 ? uniqueStrings(ids) : [entry.id];
}
function deleteComponentConsumptionGroup(entry: DiscordComponentEntry): void {

View File

@@ -1,4 +1,8 @@
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-payload";
import {
asOptionalRecord as readRecord,
normalizeOptionalString as readString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
export type DiscordInboundEventDeliveryEnd = () => void;
@@ -77,16 +81,6 @@ export function notifyDiscordInboundEventOutboundSuccess(params: {
event.markInboundEventDelivered();
}
function readRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function readString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
export function withDiscordInboundEventDeliveryMetadata(
payload: ReplyPayload,
params: {

View File

@@ -1,5 +1,6 @@
import type { APIMessage, APIUser } from "discord-api-types/v10";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import { readStringValue as readString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { getChannelMessage, Message as DiscordMessage, type Message } from "../internal/discord.js";
import { resolveDiscordMessageText, type DiscordChannelInfo } from "./message-utils.js";
@@ -93,10 +94,6 @@ function readMessageFallback(message: Message): MessageFallback {
};
}
function readString(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}
function normalizeStringArray(value: unknown): string[] {
return Array.isArray(value)
? value.flatMap((entry) => (typeof entry === "string" ? [entry] : []))

View File

@@ -7,6 +7,7 @@ import type { SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
uniqueStrings,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type { Message } from "../internal/discord.js";
import {
@@ -81,7 +82,7 @@ function mergeHostnameList(...lists: Array<string[] | undefined>): string[] | un
if (merged.length === 0) {
return undefined;
}
return Array.from(new Set(merged));
return uniqueStrings(merged);
}
function resolveDiscordMediaSsrFPolicy(policy?: SsrFPolicy): SsrFPolicy {

View File

@@ -5,7 +5,10 @@ import {
} from "openclaw/plugin-sdk/command-auth-native";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { danger, warn, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeStringEntriesLower,
} from "openclaw/plugin-sdk/string-coerce-runtime";
export type GetPluginCommandSpecs =
typeof import("openclaw/plugin-sdk/plugin-runtime").getPluginCommandSpecs;
@@ -32,9 +35,7 @@ async function appendPluginCommandSpecs(params: {
getPluginCommandSpecs?: GetPluginCommandSpecs;
}): Promise<NativeCommandSpec[]> {
const merged = [...params.commandSpecs];
const existingNames = new Set(
merged.map((spec) => normalizeLowercaseStringOrEmpty(spec.name)).filter(Boolean),
);
const existingNames = new Set(normalizeStringEntriesLower(merged.map((spec) => spec.name)));
const getPluginCommandSpecs =
params.getPluginCommandSpecs ?? (await loadPluginRuntime()).getPluginCommandSpecs;
for (const pluginCommand of getPluginCommandSpecs("discord", { config: params.cfg })) {

View File

@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
uniqueStrings,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { parseDiscordTarget } from "../targets.js";
import { resolveChannelIdForBinding } from "./thread-bindings.discord-api.js";
@@ -349,6 +350,6 @@ export async function reconcileAcpThreadBindingsOnStartup(params: {
return {
checked: acpBindings.length,
removed,
staleSessionKeys: [...new Set(staleSessionKeys)],
staleSessionKeys: uniqueStrings(staleSessionKeys),
};
}

View File

@@ -5,15 +5,11 @@ import {
resolveNativeCommandsEnabled,
resolveNativeSkillsEnabled,
} from "openclaw/plugin-sdk/native-command-config-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { ResolvedDiscordAccount } from "./accounts.js";
import type { OpenClawConfig } from "./runtime-api.js";
import { isDiscordMutableAllowEntry } from "./security-doctor.js";
function normalizeOptionalString(value: string | null | undefined): string | undefined {
const normalized = value?.trim();
return normalized ? normalized : undefined;
}
function addDiscordNameBasedEntries(params: {
target: Set<string>;
values: unknown;

View File

@@ -4,6 +4,7 @@ import type { MarkdownTableMode, OpenClawConfig } from "openclaw/plugin-sdk/conf
import type { OutboundMediaAccess } from "openclaw/plugin-sdk/media-runtime";
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
import type { ChunkMode } from "openclaw/plugin-sdk/reply-chunking";
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveDiscordAccount } from "./accounts.js";
import { registerDiscordComponentEntries } from "./components-registry.js";
import {
@@ -218,7 +219,7 @@ async function buildDiscordComponentPayload(params: {
}
const attachmentNames = extractComponentAttachmentNames(spec);
const uniqueAttachmentNames = [...new Set(attachmentNames)];
const uniqueAttachmentNames = uniqueStrings(attachmentNames);
if (uniqueAttachmentNames.length > 1) {
throw new Error(
"Discord component attachments currently support a single file. Use media-gallery for multiple files.",

View File

@@ -1,4 +1,7 @@
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
normalizeOptionalLowercaseString,
normalizeStringEntries,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { loadWebMediaRaw } from "openclaw/plugin-sdk/web-media";
import { createGuildEmoji, createGuildSticker, listGuildEmojis } from "./internal/discord.js";
import { normalizeEmojiName, resolveDiscordRest } from "./send.shared.js";
@@ -21,7 +24,7 @@ export async function uploadEmojiDiscord(payload: DiscordEmojiUpload, opts: Disc
throw new Error("Discord emoji uploads require a PNG, JPG, or GIF image");
}
const image = `data:${contentType};base64,${media.buffer.toString("base64")}`;
const roleIds = (payload.roleIds ?? []).map((id) => id.trim()).filter(Boolean);
const roleIds = normalizeStringEntries(payload.roleIds ?? []);
return await createGuildEmoji(rest, payload.guildId, {
body: {
name: normalizeEmojiName(payload.name, "Emoji name"),

View File

@@ -14,6 +14,7 @@ import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime"
import type { ChunkMode } from "openclaw/plugin-sdk/reply-chunking";
import { resolveTextChunksWithFallback } from "openclaw/plugin-sdk/reply-payload";
import type { RetryRunner } from "openclaw/plugin-sdk/retry-runtime";
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
import { loadWebMedia } from "openclaw/plugin-sdk/web-media";
import { chunkDiscordTextWithMode } from "./chunk.js";
import { createDiscordClient, resolveDiscordRest, type DiscordClientOpts } from "./client.js";
@@ -80,7 +81,7 @@ function normalizeReactionEmoji(raw: string) {
}
function normalizeStickerIds(raw: string[]) {
const ids = raw.map((entry) => entry.trim()).filter(Boolean);
const ids = normalizeStringEntries(raw);
if (ids.length === 0) {
throw new Error("At least one sticker id is required");
}

View File

@@ -30,6 +30,7 @@ import {
} from "openclaw/plugin-sdk/realtime-voice";
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
import { asBoolean, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { maybeControlDiscordVoiceAgentRun } from "./agent-control.js";
import {
convertDiscordPcm48kStereoToRealtimePcm24kMono,
@@ -197,8 +198,7 @@ function readProviderConfigBoolean(
config: RealtimeVoiceProviderConfig | undefined,
key: string,
): boolean | undefined {
const value = config?.[key];
return typeof value === "boolean" ? value : undefined;
return asBoolean(config?.[key]);
}
export function resolveDiscordVoiceMode(voice: DiscordAccountConfig["voice"]): DiscordVoiceMode {
@@ -324,7 +324,7 @@ function resolveDiscordRealtimeWakeNames(params: {
const configured = rawConfigured
.map((name) => normalizeSupportedRealtimeVoiceActivationName(name))
.filter((name): name is string => Boolean(name));
return sortRealtimeVoiceActivationNames(Array.from(new Set(configured)));
return sortRealtimeVoiceActivationNames(uniqueStrings(configured));
}
const agent = params.cfg.agents?.list?.find((candidate) => candidate.id === params.agentId);
const configuredAgentNames = [agent?.name, agent?.identity?.name]
@@ -339,7 +339,7 @@ function resolveDiscordRealtimeWakeNames(params: {
: [normalizeSupportedRealtimeVoiceActivationName(params.agentId), ...productWakeNames].filter(
(name): name is string => Boolean(name),
);
return sortRealtimeVoiceActivationNames(Array.from(new Set(defaults)));
return sortRealtimeVoiceActivationNames(uniqueStrings(defaults));
}
function matchesPendingAgentProxyQuestion(consultMessage: string, question: string): boolean {

View File

@@ -7,7 +7,7 @@ import {
} from "openclaw/plugin-sdk/agent-runtime";
import type { OpenClawConfig, TtsConfig } from "openclaw/plugin-sdk/config-contracts";
import { parseTtsDirectives } from "openclaw/plugin-sdk/speech";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { getDiscordRuntime } from "../runtime.js";
import { sanitizeVoiceReplyTextForSpeech } from "./sanitize.js";
@@ -40,7 +40,7 @@ function mergeTtsConfig(base: TtsConfig, override?: TtsConfig): TtsConfig {
const baseProviders = base.providers ?? {};
const overrideProviders = override.providers ?? {};
const mergedProviders = Object.fromEntries(
[...new Set([...Object.keys(baseProviders), ...Object.keys(overrideProviders)])].map(
uniqueStrings([...Object.keys(baseProviders), ...Object.keys(overrideProviders)]).map(
(providerId) => {
const baseProvider = baseProviders[providerId] ?? {};
const overrideProvider = overrideProviders[providerId] ?? {};

View File

@@ -7,7 +7,11 @@ import {
type RealtimeTranscriptionWebSocketTransport,
} from "openclaw/plugin-sdk/realtime-transcription";
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asOptionalRecord as readRecord,
normalizeOptionalString,
parseFiniteNumber as readFiniteNumber,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveElevenLabsApiKeyWithProfileFallback } from "./config-api.js";
import { normalizeElevenLabsBaseUrl } from "./shared.js";
@@ -57,28 +61,12 @@ const ELEVENLABS_REALTIME_MAX_RECONNECT_ATTEMPTS = 5;
const ELEVENLABS_REALTIME_RECONNECT_DELAY_MS = 1000;
const ELEVENLABS_REALTIME_MAX_QUEUED_BYTES = 2 * 1024 * 1024;
function readRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function readNestedElevenLabsConfig(rawConfig: RealtimeTranscriptionProviderConfig) {
const raw = readRecord(rawConfig);
const providers = readRecord(raw?.providers);
return readRecord(providers?.elevenlabs ?? raw?.elevenlabs ?? raw) ?? {};
}
function readFiniteNumber(value: unknown): number | undefined {
const next =
typeof value === "number"
? value
: typeof value === "string"
? Number.parseFloat(value)
: undefined;
return Number.isFinite(next) ? next : undefined;
}
function normalizeCommitStrategy(value: unknown): "manual" | "vad" | undefined {
const normalized = normalizeOptionalString(value)?.toLowerCase();
if (!normalized) {

View File

@@ -21,6 +21,7 @@ import {
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
} from "openclaw/plugin-sdk/ssrf-runtime";
import {
isRecord,
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -62,10 +63,6 @@ function matchesTrustedHostSuffix(hostname: string, trustedSuffix: string): bool
return normalizedHost === normalizedSuffix || normalizedHost.endsWith(`.${normalizedSuffix}`);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function parseFalImageGenerationResponse(payload: unknown): {
images: Record<string, unknown>[];
prompt?: string;

View File

@@ -11,6 +11,7 @@ import {
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
} from "openclaw/plugin-sdk/ssrf-runtime";
import {
isRecord,
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -97,10 +98,6 @@ export function setFalVideoFetchGuardForTesting(impl: typeof fetchWithSsrFGuard
falFetchGuard = impl ?? fetchWithSsrFGuard;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function normalizeFalVideoUrl(value: unknown): string | undefined {
const normalized = normalizeOptionalString(value);
if (!normalized && value !== undefined && value !== null) {

View File

@@ -18,7 +18,7 @@ import {
warnMissingProviderGroupPolicyFallbackOnce,
} from "openclaw/plugin-sdk/runtime-group-policy";
import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveFeishuRuntimeAccount } from "./accounts.js";
import {
checkBotMentioned,
@@ -595,7 +595,7 @@ export async function handleFeishuMessage(params: {
const configAllowFrom = feishuCfg?.allowFrom ?? [];
const rawBroadcastAgents = isGroup ? resolveBroadcastAgents(cfg, ctx.chatId) : null;
const broadcastAgents = rawBroadcastAgents
? [...new Set(rawBroadcastAgents.map((id) => normalizeAgentId(id)))]
? uniqueStrings(rawBroadcastAgents.map((id) => normalizeAgentId(id)))
: null;
// Parse message create_time early so every downstream consumer (pending

View File

@@ -1,6 +1,7 @@
import {
isRecord as sharedIsRecord,
normalizeOptionalString,
normalizeStringEntries,
readStringValue,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { FEISHU_COMMENT_FILE_TYPES, type CommentFileType } from "./comment-target.js";
@@ -237,10 +238,7 @@ function parseCommentLinkedDocumentPath(pathname: string): {
urlKind: ParsedCommentResolvedDocumentType | "wiki";
token: string;
} | null {
const segments = pathname
.split("/")
.map((segment) => segment.trim())
.filter(Boolean);
const segments = normalizeStringEntries(pathname.split("/"));
const offset = segments[0]?.toLowerCase() === "space" ? 1 : 0;
const kind = COMMENT_LINK_KIND_ALIASES.get(segments[offset]?.toLowerCase() ?? "");
const token = normalizeString(segments[offset + 1]);

View File

@@ -1,15 +1,10 @@
import { asNullableRecord as readRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { FeishuMessageEvent } from "./event-types.js";
import { normalizeFeishuExternalKey } from "./external-keys.js";
import { parsePostContent } from "./post.js";
type FeishuMessageDedupeInput = Pick<FeishuMessageEvent, "message">;
function readRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function readExternalKey(value: unknown): string | undefined {
return normalizeFeishuExternalKey(typeof value === "string" ? value : "");
}

View File

@@ -5,7 +5,7 @@ import { basename } from "node:path";
import type * as Lark from "@larksuiteoapi/node-sdk";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { Type } from "typebox";
import type { OpenClawPluginApi } from "../runtime-api.js";
import { listEnabledFeishuAccounts } from "./accounts.js";
@@ -235,7 +235,8 @@ function normalizeConvertedBlockTree(
const rootIds = (
firstLevelIds && firstLevelIds.length > 0 ? firstLevelIds : inferredTopLevelIds
).filter((id, index, arr) => typeof id === "string" && byId.has(id) && arr.indexOf(id) === index);
).filter((id): id is string => typeof id === "string" && byId.has(id));
const uniqueRootIds = uniqueStrings(rootIds);
const orderedBlocks: FeishuDocxBlock[] = [];
const visited = new Set<string>();
@@ -255,7 +256,7 @@ function normalizeConvertedBlockTree(
}
};
for (const rootId of rootIds) {
for (const rootId of uniqueRootIds) {
visit(rootId);
}
@@ -268,7 +269,7 @@ function normalizeConvertedBlockTree(
}
}
return { orderedBlocks, rootIds: rootIds.filter((id): id is string => typeof id === "string") };
return { orderedBlocks, rootIds: uniqueRootIds };
}
async function insertBlocks(

View File

@@ -1,3 +1,4 @@
import { isRecord, readStringValue as readString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { ClawdbotConfig, HistoryEntry, RuntimeEnv } from "../runtime-api.js";
import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js";
import { maybeHandleFeishuQuickActionMenu } from "./card-ux-launcher.js";
@@ -18,18 +19,10 @@ type FeishuBotMenuEvent = {
};
};
function readString(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}
function readStringOrNumber(value: unknown): string | number | undefined {
return typeof value === "string" || typeof value === "number" ? value : undefined;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function parseFeishuBotMenuEvent(value: unknown): FeishuBotMenuEvent | null {
if (!isRecord(value)) {
return null;

View File

@@ -1,4 +1,5 @@
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { asBoolean as readBoolean } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { ClawdbotConfig } from "../runtime-api.js";
import { raceWithTimeoutAndAbort } from "./async.js";
import { createFeishuClient } from "./client.js";
@@ -163,10 +164,6 @@ type ResolvedWholeCommentTimelineEntry = {
content: ParsedCommentContent;
};
function readBoolean(value: unknown): boolean | undefined {
return typeof value === "boolean" ? value : undefined;
}
function safeJsonStringify(value: unknown): string {
try {
return JSON.stringify(value);

View File

@@ -1,3 +1,4 @@
import { isRecord, readStringValue as readString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { ClawdbotConfig, HistoryEntry, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
import { resolveFeishuMessageDedupeKey } from "./dedupe-key.js";
import type { FeishuMessageEvent } from "./event-types.js";
@@ -9,14 +10,6 @@ import {
import { createSequentialQueue } from "./sequential-queue.js";
import type { FeishuChatType } from "./types.js";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function readString(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}
type FeishuMessageReceiveHandlerContext = {
cfg: ClawdbotConfig;
core: PluginRuntime;

View File

@@ -16,7 +16,11 @@ import {
sendTextMediaPayload,
} from "openclaw/plugin-sdk/reply-payload";
import { statRegularFileSync } from "openclaw/plugin-sdk/security-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
isRecord,
normalizeLowercaseStringOrEmpty,
normalizeStringEntries,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveFeishuAccount } from "./accounts.js";
import { createFeishuClient } from "./client.js";
import { cleanupAmbientCommentTypingReaction } from "./comment-reaction.js";
@@ -80,10 +84,6 @@ function shouldUseCard(text: string): boolean {
return /```[\s\S]*?```/.test(text) || /\|.+\|[\r\n]+\|[-:| ]+\|/.test(text);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function markRenderedFeishuCard(card: Record<string, unknown>): Record<string, unknown> {
Object.defineProperty(card, RENDERED_FEISHU_CARD, {
value: true,
@@ -92,6 +92,21 @@ function markRenderedFeishuCard(card: Record<string, unknown>): Record<string, u
return card;
}
function escapeFeishuCardMarkdownText(text: string): string {
return text.replace(/[&<>]/g, (char) => {
switch (char) {
case "&":
return "&amp;";
case "<":
return "&lt;";
case ">":
return "&gt;";
default:
return char;
}
});
}
function resolveSafeFeishuButtonUrl(url: unknown): string | undefined {
const trimmed = typeof url === "string" ? url.trim() : "";
if (!trimmed) {
@@ -179,18 +194,7 @@ function sanitizeNativeFeishuCardElements(element: unknown): Record<string, unkn
return [
{
tag: "markdown",
content: element.content.replace(/[&<>]/g, (char) => {
switch (char) {
case "&":
return "&amp;";
case "<":
return "&lt;";
case ">":
return "&gt;";
default:
return char;
}
}),
content: escapeFeishuCardMarkdownText(element.content),
},
];
}
@@ -527,9 +531,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
});
}
const mediaUrls = resolvePayloadMediaUrls(ctx.payload)
.map((entry) => entry.trim())
.filter(Boolean);
const mediaUrls = normalizeStringEntries(resolvePayloadMediaUrls(ctx.payload));
return attachChannelToResult(
"feishu",
await sendPayloadMediaSequenceAndFinalize({

View File

@@ -1,5 +1,6 @@
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
import {
isRecord,
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -67,10 +68,6 @@ function isWithdrawnReplyError(err: unknown): boolean {
return false;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
type FeishuCreateMessageClient = {
im: {
message: {

View File

@@ -13,6 +13,7 @@ import {
type OpenClawConfig,
type SecretInput,
} from "openclaw/plugin-sdk/setup";
import { normalizeOptionalString as normalizeString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveDefaultFeishuAccountId, resolveFeishuAccount } from "./accounts.js";
import type { AppRegistrationResult } from "./app-registration.js";
import type { FeishuConfig, FeishuDomain } from "./types.js";
@@ -27,14 +28,6 @@ const FEISHU_SETUP_FLOW_KEY = "_flow";
// Helpers
// ---------------------------------------------------------------------------
function normalizeString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
}
function isFeishuConfigured(cfg: OpenClawConfig): boolean {
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;

View File

@@ -1,4 +1,8 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import {
asNullableRecord as asRecord,
normalizeOptionalLowercaseString as normalizeProviderId,
} from "openclaw/plugin-sdk/string-coerce-runtime";
type LegacyConfigRule = {
path: Array<string | number>;
@@ -6,16 +10,6 @@ type LegacyConfigRule = {
match: (value: unknown) => boolean;
};
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function normalizeProviderId(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : undefined;
}
function hasOwn(record: Record<string, unknown>, key: string): boolean {
return Object.prototype.hasOwnProperty.call(record, key);
}

View File

@@ -4,8 +4,10 @@ import {
type RealtimeVoiceAgentConsultToolPolicy,
} from "openclaw/plugin-sdk/realtime-voice";
import {
asRecord,
normalizeOptionalLowercaseString,
normalizeOptionalString,
normalizeOptionalTrimmedStringList,
} from "openclaw/plugin-sdk/string-coerce-runtime";
export type GoogleMeetTransport = "chrome" | "chrome-node" | "twilio";
@@ -260,12 +262,6 @@ const GOOGLE_MEET_PREVIEW_ACK_KEYS = [
"GOOGLE_MEET_PREVIEW_ACK",
] as const;
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
function resolveBoolean(value: unknown, fallback: boolean): boolean {
return typeof value === "boolean" ? value : fallback;
}
@@ -318,13 +314,7 @@ function readEnvNumber(env: NodeJS.ProcessEnv, keys: readonly string[]): number
}
function resolveStringArray(value: unknown): string[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const normalized = value
.map((entry) => normalizeOptionalString(entry))
.filter((entry): entry is string => Boolean(entry));
return normalized.length > 0 ? normalized : undefined;
return normalizeOptionalTrimmedStringList(value);
}
function resolveProvidersConfig(value: unknown): Record<string, Record<string, unknown>> {

View File

@@ -1,4 +1,5 @@
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { exportGoogleDriveDocumentText, extractGoogleDriveDocumentId } from "./drive.js";
import { googleApiError } from "./google-api-errors.js";
@@ -797,9 +798,10 @@ function mergeAttendanceRows(
grouped.set(key, { ...row, participants: [row.participant] });
continue;
}
existing.participants = [
...new Set([...(existing.participants ?? [existing.participant]), row.participant]),
];
existing.participants = uniqueStrings([
...(existing.participants ?? [existing.participant]),
row.participant,
]);
existing.sessions.push(...row.sessions);
existing.displayName ??= row.displayName;
existing.user ??= row.user;

View File

@@ -2,6 +2,10 @@ import { spawn, spawnSync, type ChildProcess } from "node:child_process";
import { randomUUID } from "node:crypto";
import { setTimeout as sleep } from "node:timers/promises";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import {
asRecord,
normalizeOptionalString as readString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import {
DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND,
DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND,
@@ -33,16 +37,6 @@ type NodeBridgeSession = {
const sessions = new Map<string, NodeBridgeSession>();
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
function readString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function readStringArray(value: unknown): string[] | undefined {
if (!Array.isArray(value)) {
return undefined;

View File

@@ -3,7 +3,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk/plugin-runtime";
import { sleep } from "openclaw/plugin-sdk/runtime-env";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import type {
GoogleMeetConfig,
GoogleMeetMode,
@@ -207,7 +207,7 @@ function collectChromeAudioCommands(config: GoogleMeetConfig): string[] {
config.chrome.audioOutputCommand?.[0],
config.chrome.bargeInInputCommand?.[0],
];
return [...new Set(commands.filter((value): value is string => Boolean(value?.trim())))];
return uniqueStrings(commands.filter((value): value is string => Boolean(value?.trim())));
}
async function commandExists(runtime: PluginRuntime, command: string): Promise<boolean> {

View File

@@ -2,6 +2,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
import { asRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { GoogleMeetConfig, GoogleMeetMode, GoogleMeetTransport } from "./config.js";
type SetupCheck = {
@@ -273,13 +274,3 @@ export function addGoogleMeetSetupCheck(
checks,
};
}
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}

View File

@@ -2,6 +2,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { callGatewayFromCli } from "openclaw/plugin-sdk/gateway-runtime";
import type { PluginRuntime } from "openclaw/plugin-sdk/plugin-runtime";
import type { RuntimeLogger } from "openclaw/plugin-sdk/plugin-runtime";
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { GoogleMeetConfig, GoogleMeetMode } from "../config.js";
import {
startNodeAgentAudioBridge,
@@ -313,7 +314,7 @@ function mergeBrowserNotes(
}
return {
...browser,
notes: [...new Set([...(browser.notes ?? []), ...notes])],
notes: uniqueStrings([...(browser.notes ?? []), ...notes]),
};
}

View File

@@ -9,6 +9,7 @@ import {
withRemoteHttpResponse,
} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import { createProviderHttpError } from "openclaw/plugin-sdk/provider-http";
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { GeminiEmbeddingClient, GeminiTextEmbeddingRequest } from "./embedding-provider.js";
type EmbeddingBatchExecutionParams = {
@@ -221,11 +222,9 @@ function parseGeminiBatchOutput(text: string): GeminiBatchOutputLine[] {
if (!text.trim()) {
return [];
}
return text
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.map((line) => JSON.parse(line) as GeminiBatchOutputLine);
return normalizeStringEntries(text.split("\n")).map(
(line) => JSON.parse(line) as GeminiBatchOutputLine,
);
}
async function waitForGeminiBatch(params: {

View File

@@ -20,7 +20,10 @@ import {
readProviderJsonObjectResponse,
} from "openclaw/plugin-sdk/provider-http";
import type { SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asOptionalRecord as asRecord,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
export type GeminiEmbeddingClient = {
baseUrl: string;
@@ -91,12 +94,6 @@ type GeminiEmbeddingRequest = {
};
export type GeminiTextEmbeddingRequest = GeminiEmbeddingRequest;
function asRecord(value: unknown): Record<string, unknown> | undefined {
return typeof value === "object" && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function malformedGeminiEmbeddingResponse(): Error {
return new Error("gemini embeddings failed: malformed JSON response");
}

View File

@@ -11,6 +11,7 @@ import {
sanitizeConfiguredModelProviderRequest,
} from "openclaw/plugin-sdk/provider-http";
import {
isRecord,
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -41,10 +42,6 @@ const GOOGLE_SUPPORTED_ASPECT_RATIOS = [
const GOOGLE_IMAGE_MALFORMED_RESPONSE = "Google image generation response malformed";
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function normalizeGoogleImageModel(model: string | undefined): string {
const trimmed = model?.trim();
return normalizeGoogleModelId(trimmed || DEFAULT_GOOGLE_IMAGE_MODEL);

View File

@@ -3,6 +3,7 @@ import type {
ProviderThinkingProfile,
} from "openclaw/plugin-sdk/core";
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { normalizeAntigravityModelId, normalizeGoogleModelId } from "./model-id.js";
import { isGoogleGemini3ProModel, isGoogleGemini3ThinkingLevelModel } from "./thinking-api.js";
@@ -17,10 +18,6 @@ type GoogleProviderConfigLike = GoogleApiCarrier & {
export const DEFAULT_GOOGLE_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
const GOOGLE_MODEL_ID_PROVIDERS = new Set(["google", "google-gemini-cli", "google-vertex"]);
function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function trimTrailingSlashes(value: string): string {
return value.replace(/\/+$/, "");
}

View File

@@ -37,7 +37,11 @@ import {
resamplePcm,
} from "openclaw/plugin-sdk/realtime-voice";
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asBoolean,
asFiniteNumber,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { createGoogleGenAI } from "./google-genai-runtime.js";
const GOOGLE_REALTIME_DEFAULT_MODEL = "gemini-2.5-flash-native-audio-preview-12-2025";
@@ -126,14 +130,6 @@ function trimToUndefined(value: unknown): string | undefined {
return normalizeOptionalString(value);
}
function asFiniteNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function asBoolean(value: unknown): boolean | undefined {
return typeof value === "boolean" ? value : undefined;
}
function asSensitivity(value: unknown): GoogleRealtimeSensitivity | undefined {
const normalized = normalizeOptionalString(value)?.toLowerCase();
return normalized === "low" || normalized === "high" ? normalized : undefined;

View File

@@ -23,6 +23,7 @@ import {
wrapWebContent,
writeCachedSearchPayload,
} from "openclaw/plugin-sdk/provider-web-search";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
resolveGeminiConfig,
resolveGeminiBaseUrl,
@@ -60,10 +61,6 @@ type GeminiGroundingResponse = {
};
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function throwMalformedGeminiResponse(): never {
throw new Error("Gemini API error: malformed JSON response");
}

View File

@@ -1,3 +1,7 @@
import {
isRecord,
normalizeOptionalString as trimToUndefined,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { normalizeGoogleApiBaseUrl } from "../provider-policy.js";
const DEFAULT_GEMINI_WEB_SEARCH_MODEL = "gemini-2.5-flash";
@@ -10,14 +14,6 @@ export type GeminiConfig = {
providerBaseUrl?: unknown;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function trimToUndefined(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
}
export function resolveGeminiConfig(searchConfig?: Record<string, unknown>): GeminiConfig {
const gemini = searchConfig?.gemini;
return isRecord(gemini) ? gemini : {};

View File

@@ -6,6 +6,7 @@ import {
type WebSearchProviderPlugin,
type WebSearchProviderToolDefinition,
} from "openclaw/plugin-sdk/provider-web-search-config-contract";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
resolveGeminiApiKey,
resolveGeminiBaseUrl,
@@ -66,10 +67,6 @@ function createGeminiToolDefinition(
};
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function resolveGoogleModelProviderConfig(
config?: OpenClawConfig,
): Record<string, unknown> | undefined {

View File

@@ -21,7 +21,10 @@ import {
transformTransportMessages,
type WritableTransportStream,
} from "openclaw/plugin-sdk/provider-transport-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { parseGeminiAuth } from "./gemini-auth.js";
import { normalizeGoogleApiBaseUrl } from "./provider-policy.js";
import {
@@ -140,10 +143,6 @@ type GoogleSseChunk = {
let toolCallCounter = 0;
const GEMINI_THOUGHT_SIGNATURE_VALIDATOR_SKIP = "skip_thought_signature_validator";
function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function requiresToolCallId(modelId: string): boolean {
return modelId.startsWith("claude-") || modelId.startsWith("gpt-oss-");
}
@@ -202,9 +201,7 @@ function hasGeminiThoughtSignatureTruncationFootprint(value: string): boolean {
);
}
function sanitizeGeminiThoughtSignature(
thoughtSignature: string | undefined,
): string | undefined {
function sanitizeGeminiThoughtSignature(thoughtSignature: string | undefined): string | undefined {
if (typeof thoughtSignature !== "string") {
return undefined;
}
@@ -552,9 +549,7 @@ function convertGoogleMessages(model: GoogleTransportModel, context: Context) {
: undefined;
parts.push({
text: sanitizeTransportPayloadText(block.text),
...(sanitizedTextSignature
? { thoughtSignature: sanitizedTextSignature }
: {}),
...(sanitizedTextSignature ? { thoughtSignature: sanitizedTextSignature } : {}),
});
continue;
}

View File

@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
import { readFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
type GoogleAuthorizedUserCredentials = {
type: "authorized_user";
@@ -46,10 +47,6 @@ export function resetGoogleVertexAuthorizedUserTokenCacheForTest(): void {
cachedGoogleVertexAdcToken = undefined;
}
function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
export function isGoogleVertexCredentialsMarker(
apiKey: string | undefined,
): apiKey is undefined | typeof GCP_VERTEX_CREDENTIALS_MARKER {

View File

@@ -1,6 +1,7 @@
import { spawn } from "node:child_process";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { extname, join } from "node:path";
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import { createIMessageRpcClient } from "./client.js";
import { extractMarkdownFormatRuns } from "./markdown-format.js";
@@ -222,10 +223,7 @@ async function runIMessageCliJson(
if (killEscalation) {
clearTimeout(killEscalation);
}
const lines = stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const lines = normalizeStringEntries(stdout.split(/\r?\n/));
const last = lines.at(-1);
let parsed: Record<string, unknown> | null = null;
if (last) {

View File

@@ -24,6 +24,7 @@ import { createChannelHistoryWindow, type HistoryEntry } from "openclaw/plugin-s
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-runtime";
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
import { evaluateSupplementalContextVisibility } from "openclaw/plugin-sdk/security-runtime";
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
import { sanitizeTerminalText } from "openclaw/plugin-sdk/text-chunking";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { resolveIMessageConversationRoute } from "../conversation-route.js";
@@ -94,7 +95,7 @@ function mergeIMessageGroupAllowFromWithLegacyChatTargets(params: {
if (legacyChatTargets.length === 0) {
return params.groupAllowFrom;
}
return Array.from(new Set([...params.groupAllowFrom, ...legacyChatTargets]));
return uniqueStrings([...params.groupAllowFrom, ...legacyChatTargets]);
}
const imessageIngressIdentity = defineStableChannelIngressIdentity({

View File

@@ -4,7 +4,10 @@ import { runCommandWithTimeout } from "openclaw/plugin-sdk/process-runtime";
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { detectBinary } from "openclaw/plugin-sdk/setup";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
normalizeLowercaseStringOrEmpty,
normalizeStringEntries,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { createIMessageRpcClient } from "./client.js";
import { DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS } from "./constants.js";
import {
@@ -107,10 +110,7 @@ function parseStatusPayload(stdout: string): {
payload: Record<string, unknown> | null;
firstLineSnippet?: string;
} {
const lines = stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const lines = normalizeStringEntries(stdout.split(/\r?\n/));
for (const line of lines.toReversed()) {
try {
const value = JSON.parse(line);

View File

@@ -1,6 +1,7 @@
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
normalizeStringEntriesLower,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { hasIrcControlChars } from "./control-chars.js";
import type { IrcInboundMessage } from "./types.js";
@@ -93,7 +94,7 @@ export function resolveIrcAllowlistMatch(params: {
message: IrcInboundMessage;
allowNameMatching?: boolean;
}): { allowed: boolean; source?: string } {
const allowFrom = new Set(params.allowFrom.map(normalizeLowercaseStringOrEmpty).filter(Boolean));
const allowFrom = new Set(normalizeStringEntriesLower(params.allowFrom));
if (allowFrom.has("*")) {
return { allowed: true, source: "wildcard" };
}

View File

@@ -83,7 +83,7 @@ export function setIrcGroupAccess(
return updateIrcAccountConfig(cfg, accountId, { enabled: true, groupPolicy: policy });
}
const normalizedEntries = [
...new Set(entries.map((entry) => normalizeGroupEntry(entry)).filter(Boolean)),
...new Set(entries.flatMap((entry) => normalizeGroupEntry(entry) ?? [])),
];
const groups = Object.fromEntries(normalizedEntries.map((entry) => [entry, {}]));
return updateIrcAccountConfig(cfg, accountId, {

View File

@@ -14,7 +14,9 @@ import {
} from "openclaw/plugin-sdk/setup";
import {
normalizeOptionalString,
normalizeStringEntries,
normalizeStringifiedOptionalString,
uniqueStrings,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveDefaultIrcAccountId, resolveIrcAccount } from "./accounts.js";
import {
@@ -40,10 +42,7 @@ const USE_ENV_FLAG = "__ircUseEnv";
const TLS_FLAG = "__ircTls";
function parseListInput(raw: string): string[] {
return raw
.split(/[\n,;]+/g)
.map((entry) => entry.trim())
.filter(Boolean);
return normalizeStringEntries(raw.split(/[\n,;]+/g));
}
function normalizeGroupEntry(raw: string): string | null {
@@ -74,10 +73,9 @@ const promptIrcAllowFrom = createPromptParsedAllowFromForAccount<CoreConfig>({
message: t("wizard.irc.allowFromPrompt"),
placeholder: "alice, bob!ident@example.org",
parseEntries: (raw) => ({
entries: parseListInput(raw)
.map((entry) => normalizeIrcAllowEntry(entry))
.map((entry) => entry.trim())
.filter(Boolean),
entries: normalizeStringEntries(
parseListInput(raw).map((entry) => normalizeIrcAllowEntry(entry)),
),
}),
getExistingAllowFrom: ({ cfg }) => cfg.channels?.irc?.allowFrom ?? [],
applyAllowFrom: ({ cfg, allowFrom }) => setIrcAllowFrom(cfg, allowFrom),
@@ -372,7 +370,11 @@ export const ircSetupWizard: ChannelSetupWizard = {
setPolicy: ({ cfg, accountId, policy }) =>
setIrcGroupAccess(cfg as CoreConfig, accountId, policy, [], normalizeGroupEntry),
resolveAllowlist: async ({ entries }) =>
[...new Set(entries.map((entry) => normalizeGroupEntry(entry)).filter(Boolean))] as string[],
uniqueStrings(
entries
.map((entry) => normalizeGroupEntry(entry))
.filter((entry): entry is string => Boolean(entry)),
),
applyAllowlist: ({ cfg, accountId, resolved }) =>
setIrcGroupAccess(
cfg as CoreConfig,

View File

@@ -1,4 +1,5 @@
import { createScopedChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
listLineAccountIds,
resolveDefaultLineAccountId,
@@ -21,9 +22,5 @@ export const lineConfigAdapter = createScopedChannelConfigAdapter<
defaultAccountId: resolveDefaultLineAccountId,
clearBaseFields: ["channelSecret", "tokenFile", "secretFile"],
resolveAllowFrom: (account) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
allowFrom
.map((entry) => String(entry).trim())
.filter(Boolean)
.map(normalizeLineAllowFrom),
formatAllowFrom: (allowFrom) => normalizeStringEntries(allowFrom).map(normalizeLineAllowFrom),
});

View File

@@ -1,8 +1,7 @@
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
export function buildLineQuickReplyFallbackText(labels: readonly string[] | undefined): string {
const normalized = (labels ?? [])
.map((label) => label.trim())
.filter(Boolean)
.slice(0, 13);
const normalized = normalizeStringEntries(labels ?? []).slice(0, 13);
if (normalized.length === 0) {
return "Choose an option.";
}

Some files were not shown because too many files have changed in this diff Show More