refactor: consolidate duplicate utility functions (#12439)

* refactor: consolidate duplicate utility functions

- Add escapeRegExp to src/utils.ts and remove 10 local duplicates
- Rename bash-tools clampNumber to clampWithDefault (different signature)
- Centralize formatError calls to use formatErrorMessage from infra/errors.ts
- Re-export formatErrorMessage from cli/cli-utils.ts to preserve API

* refactor: consolidate remaining escapeRegExp duplicates

* refactor: consolidate sleep, stripAnsi, and clamp duplicates
This commit is contained in:
max
2026-02-08 23:59:43 -08:00
committed by GitHub
parent 8968d9a339
commit ec910a235e
29 changed files with 67 additions and 146 deletions
+1 -10
View File
@@ -6,6 +6,7 @@ import {
type MSTeamsReplyStyle, type MSTeamsReplyStyle,
type ReplyPayload, type ReplyPayload,
SILENT_REPLY_TOKEN, SILENT_REPLY_TOKEN,
sleep,
} from "openclaw/plugin-sdk"; } from "openclaw/plugin-sdk";
import type { MSTeamsAccessTokenProvider } from "./attachments/types.js"; import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
import type { StoredConversationReference } from "./conversation-store.js"; import type { StoredConversationReference } from "./conversation-store.js";
@@ -166,16 +167,6 @@ function clampMs(value: number, maxMs: number): number {
return Math.min(value, maxMs); return Math.min(value, maxMs);
} }
async function sleep(ms: number): Promise<void> {
const delay = Math.max(0, ms);
if (delay === 0) {
return;
}
await new Promise<void>((resolve) => {
setTimeout(resolve, delay);
});
}
function resolveRetryOptions( function resolveRetryOptions(
retry: false | MSTeamsSendRetryOptions | undefined, retry: false | MSTeamsSendRetryOptions | undefined,
): Required<MSTeamsSendRetryOptions> & { enabled: boolean } { ): Required<MSTeamsSendRetryOptions> & { enabled: boolean } {
+1 -4
View File
@@ -2,6 +2,7 @@ import type { Command } from "commander";
import fs from "node:fs"; import fs from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { sleep } from "openclaw/plugin-sdk";
import type { VoiceCallConfig } from "./config.js"; import type { VoiceCallConfig } from "./config.js";
import type { VoiceCallRuntime } from "./runtime.js"; import type { VoiceCallRuntime } from "./runtime.js";
import { resolveUserPath } from "./utils.js"; import { resolveUserPath } from "./utils.js";
@@ -40,10 +41,6 @@ function resolveDefaultStorePath(config: VoiceCallConfig): string {
return path.join(base, "calls.jsonl"); return path.join(base, "calls.jsonl");
} }
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function registerVoiceCallCli(params: { export function registerVoiceCallCli(params: {
program: Command; program: Command;
config: VoiceCallConfig; config: VoiceCallConfig;
+1 -2
View File
@@ -4,6 +4,7 @@ import {
collectWhatsAppStatusIssues, collectWhatsAppStatusIssues,
createActionGate, createActionGate,
DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID,
escapeRegExp,
formatPairingApproveHint, formatPairingApproveHint,
getChatChannelMeta, getChatChannelMeta,
isWhatsAppGroupJid, isWhatsAppGroupJid,
@@ -33,8 +34,6 @@ import { getWhatsAppRuntime } from "./runtime.js";
const meta = getChatChannelMeta("whatsapp"); const meta = getChatChannelMeta("whatsapp");
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = { export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
id: "whatsapp", id: "whatsapp",
meta: { meta: {
+1 -5
View File
@@ -1,4 +1,5 @@
import { spawn, type SpawnOptions } from "node:child_process"; import { spawn, type SpawnOptions } from "node:child_process";
import { stripAnsi } from "openclaw/plugin-sdk";
import type { ZcaResult, ZcaRunOptions } from "./types.js"; import type { ZcaResult, ZcaRunOptions } from "./types.js";
const ZCA_BINARY = "zca"; const ZCA_BINARY = "zca";
@@ -107,11 +108,6 @@ export function runZcaInteractive(args: string[], options?: ZcaRunOptions): Prom
}); });
} }
function stripAnsi(str: string): string {
// oxlint-disable-next-line no-control-regex
return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
}
export function parseJsonOutput<T>(stdout: string): T | null { export function parseJsonOutput<T>(stdout: string): T | null {
try { try {
return JSON.parse(stdout) as T; return JSON.parse(stdout) as T;
+10 -5
View File
@@ -43,7 +43,7 @@ import {
buildDockerExecArgs, buildDockerExecArgs,
buildSandboxEnv, buildSandboxEnv,
chunkString, chunkString,
clampNumber, clampWithDefault,
coerceEnv, coerceEnv,
killSession, killSession,
readEnvInt, readEnvInt,
@@ -105,13 +105,13 @@ function validateHostEnv(env: Record<string, string>): void {
} }
} }
} }
const DEFAULT_MAX_OUTPUT = clampNumber( const DEFAULT_MAX_OUTPUT = clampWithDefault(
readEnvInt("PI_BASH_MAX_OUTPUT_CHARS"), readEnvInt("PI_BASH_MAX_OUTPUT_CHARS"),
200_000, 200_000,
1_000, 1_000,
200_000, 200_000,
); );
const DEFAULT_PENDING_MAX_OUTPUT = clampNumber( const DEFAULT_PENDING_MAX_OUTPUT = clampWithDefault(
readEnvInt("OPENCLAW_BASH_PENDING_MAX_OUTPUT_CHARS"), readEnvInt("OPENCLAW_BASH_PENDING_MAX_OUTPUT_CHARS"),
200_000, 200_000,
1_000, 1_000,
@@ -801,7 +801,7 @@ export function createExecTool(
defaults?: ExecToolDefaults, defaults?: ExecToolDefaults,
// oxlint-disable-next-line typescript/no-explicit-any // oxlint-disable-next-line typescript/no-explicit-any
): AgentTool<any, ExecToolDetails> { ): AgentTool<any, ExecToolDetails> {
const defaultBackgroundMs = clampNumber( const defaultBackgroundMs = clampWithDefault(
defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"), defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"),
10_000, 10_000,
10, 10,
@@ -860,7 +860,12 @@ export function createExecTool(
const yieldWindow = allowBackground const yieldWindow = allowBackground
? backgroundRequested ? backgroundRequested
? 0 ? 0
: clampNumber(params.yieldMs ?? defaultBackgroundMs, defaultBackgroundMs, 10, 120_000) : clampWithDefault(
params.yieldMs ?? defaultBackgroundMs,
defaultBackgroundMs,
10,
120_000,
)
: null; : null;
const elevatedDefaults = defaults?.elevated; const elevatedDefaults = defaults?.elevated;
const elevatedAllowed = Boolean(elevatedDefaults?.enabled && elevatedDefaults.allowed); const elevatedAllowed = Boolean(elevatedDefaults?.enabled && elevatedDefaults.allowed);
+4 -1
View File
@@ -146,7 +146,10 @@ function safeCwd() {
} }
} }
export function clampNumber( /**
* Clamp a number within min/max bounds, using defaultValue if undefined or NaN.
*/
export function clampWithDefault(
value: number | undefined, value: number | undefined,
defaultValue: number, defaultValue: number,
min: number, min: number,
+4 -7
View File
@@ -10,6 +10,7 @@ import type { CliBackendConfig } from "../../config/types.js";
import type { EmbeddedContextFile } from "../pi-embedded-helpers.js"; import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
import { runExec } from "../../process/exec.js"; import { runExec } from "../../process/exec.js";
import { buildTtsSystemPromptHint } from "../../tts/tts.js"; import { buildTtsSystemPromptHint } from "../../tts/tts.js";
import { escapeRegExp } from "../../utils.js";
import { resolveDefaultModelForAgent } from "../model-selection.js"; import { resolveDefaultModelForAgent } from "../model-selection.js";
import { detectRuntimeShell } from "../shell-utils.js"; import { detectRuntimeShell } from "../shell-utils.js";
import { buildSystemPromptParams } from "../system-prompt-params.js"; import { buildSystemPromptParams } from "../system-prompt-params.js";
@@ -17,10 +18,6 @@ import { buildAgentSystemPrompt } from "../system-prompt.js";
const CLI_RUN_QUEUE = new Map<string, Promise<unknown>>(); const CLI_RUN_QUEUE = new Map<string, Promise<unknown>>();
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export async function cleanupResumeProcesses( export async function cleanupResumeProcesses(
backend: CliBackendConfig, backend: CliBackendConfig,
sessionId: string, sessionId: string,
@@ -43,7 +40,7 @@ export async function cleanupResumeProcesses(
const resumeTokens = resumeArgs.map((arg) => arg.replaceAll("{sessionId}", sessionId)); const resumeTokens = resumeArgs.map((arg) => arg.replaceAll("{sessionId}", sessionId));
const pattern = [commandToken, ...resumeTokens] const pattern = [commandToken, ...resumeTokens]
.filter(Boolean) .filter(Boolean)
.map((token) => escapeRegex(token)) .map((token) => escapeRegExp(token))
.join(".*"); .join(".*");
if (!pattern) { if (!pattern) {
return; return;
@@ -95,9 +92,9 @@ function buildSessionMatchers(backend: CliBackendConfig): RegExp[] {
function tokenToRegex(token: string): string { function tokenToRegex(token: string): string {
if (!token.includes("{sessionId}")) { if (!token.includes("{sessionId}")) {
return escapeRegex(token); return escapeRegExp(token);
} }
const parts = token.split("{sessionId}").map((part) => escapeRegex(part)); const parts = token.split("{sessionId}").map((part) => escapeRegExp(part));
return parts.join("\\S+"); return parts.join("\\S+");
} }
+2 -4
View File
@@ -1,3 +1,5 @@
import { escapeRegExp } from "../utils.js";
const ESC = "\x1b"; const ESC = "\x1b";
const CR = "\r"; const CR = "\r";
const TAB = "\t"; const TAB = "\t";
@@ -12,10 +14,6 @@ type Modifiers = {
shift: boolean; shift: boolean;
}; };
function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const namedKeyMap = new Map<string, string>([ const namedKeyMap = new Map<string, string>([
["enter", CR], ["enter", CR],
["return", CR], ["return", CR],
+1 -4
View File
@@ -14,6 +14,7 @@ import type {
} from "./commands-registry.types.js"; } from "./commands-registry.types.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import { resolveConfiguredModelRef } from "../agents/model-selection.js"; import { resolveConfiguredModelRef } from "../agents/model-selection.js";
import { escapeRegExp } from "../utils.js";
import { getChatCommands, getNativeCommandSurfaces } from "./commands-registry.data.js"; import { getChatCommands, getNativeCommandSurfaces } from "./commands-registry.data.js";
export type { export type {
@@ -68,10 +69,6 @@ function getTextAliasMap(): Map<string, TextAliasSpec> {
return map; return map;
} }
function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function buildSkillCommandDefinitions(skillCommands?: SkillCommandSpec[]): ChatCommandDefinition[] { function buildSkillCommandDefinitions(skillCommands?: SkillCommandSpec[]): ChatCommandDefinition[] {
if (!skillCommands || skillCommands.length === 0) { if (!skillCommands || skillCommands.length === 0) {
return []; return [];
+1 -3
View File
@@ -1,6 +1,4 @@
function escapeRegExp(value: string) { import { escapeRegExp } from "../utils.js";
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function extractModelDirective( export function extractModelDirective(
body?: string, body?: string,
+1 -2
View File
@@ -1,4 +1,5 @@
import type { NoticeLevel, ReasoningLevel } from "../thinking.js"; import type { NoticeLevel, ReasoningLevel } from "../thinking.js";
import { escapeRegExp } from "../../utils.js";
import { import {
type ElevatedLevel, type ElevatedLevel,
normalizeElevatedLevel, normalizeElevatedLevel,
@@ -17,8 +18,6 @@ type ExtractedLevel<T> = {
hasDirective: boolean; hasDirective: boolean;
}; };
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const matchLevelDirective = ( const matchLevelDirective = (
body: string, body: string,
names: string[], names: string[],
+1 -4
View File
@@ -1,6 +1,7 @@
import type { MsgContext } from "../templating.js"; import type { MsgContext } from "../templating.js";
import { normalizeChatType } from "../../channels/chat-type.js"; import { normalizeChatType } from "../../channels/chat-type.js";
import { listSenderLabelCandidates, resolveSenderLabel } from "../../channels/sender-label.js"; import { listSenderLabelCandidates, resolveSenderLabel } from "../../channels/sender-label.js";
import { escapeRegExp } from "../../utils.js";
export function formatInboundBodyWithSenderMeta(params: { body: string; ctx: MsgContext }): string { export function formatInboundBodyWithSenderMeta(params: { body: string; ctx: MsgContext }): string {
const body = params.body; const body = params.body;
@@ -51,7 +52,3 @@ function hasSenderMetaLine(body: string, ctx: MsgContext): boolean {
return pattern.test(body); return pattern.test(body);
}); });
} }
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
+1 -4
View File
@@ -3,10 +3,7 @@ import type { MsgContext } from "../templating.js";
import { resolveAgentConfig } from "../../agents/agent-scope.js"; import { resolveAgentConfig } from "../../agents/agent-scope.js";
import { getChannelDock } from "../../channels/dock.js"; import { getChannelDock } from "../../channels/dock.js";
import { normalizeChannelId } from "../../channels/plugins/index.js"; import { normalizeChannelId } from "../../channels/plugins/index.js";
import { escapeRegExp } from "../../utils.js";
function escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function deriveMentionPatterns(identity?: { name?: string; emoji?: string }) { function deriveMentionPatterns(identity?: { name?: string; emoji?: string }) {
const patterns: string[] = []; const patterns: string[] = [];
+2 -4
View File
@@ -1,10 +1,8 @@
import { escapeRegExp } from "../utils.js";
export const HEARTBEAT_TOKEN = "HEARTBEAT_OK"; export const HEARTBEAT_TOKEN = "HEARTBEAT_OK";
export const SILENT_REPLY_TOKEN = "NO_REPLY"; export const SILENT_REPLY_TOKEN = "NO_REPLY";
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function isSilentReplyText( export function isSilentReplyText(
text: string | undefined, text: string | undefined,
token: string = SILENT_REPLY_TOKEN, token: string = SILENT_REPLY_TOKEN,
+2 -5
View File
@@ -1,5 +1,6 @@
import type { BrowserRouteContext } from "../server-context.js"; import type { BrowserRouteContext } from "../server-context.js";
import type { BrowserRequest, BrowserResponse, BrowserRouteRegistrar } from "./types.js"; import type { BrowserRequest, BrowserResponse, BrowserRouteRegistrar } from "./types.js";
import { escapeRegExp } from "../../utils.js";
import { registerBrowserRoutes } from "./index.js"; import { registerBrowserRoutes } from "./index.js";
type BrowserDispatchRequest = { type BrowserDispatchRequest = {
@@ -22,10 +23,6 @@ type RouteEntry = {
handler: (req: BrowserRequest, res: BrowserResponse) => void | Promise<void>; handler: (req: BrowserRequest, res: BrowserResponse) => void | Promise<void>;
}; };
function escapeRegex(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function compileRoute(path: string): { regex: RegExp; paramNames: string[] } { function compileRoute(path: string): { regex: RegExp; paramNames: string[] } {
const paramNames: string[] = []; const paramNames: string[] = [];
const parts = path.split("/").map((part) => { const parts = path.split("/").map((part) => {
@@ -34,7 +31,7 @@ function compileRoute(path: string): { regex: RegExp; paramNames: string[] } {
paramNames.push(name); paramNames.push(name);
return "([^/]+)"; return "([^/]+)";
} }
return escapeRegex(part); return escapeRegExp(part);
}); });
return { regex: new RegExp(`^${parts.join("/")}$`), paramNames }; return { regex: new RegExp(`^${parts.join("/")}$`), paramNames };
} }
+1 -3
View File
@@ -18,7 +18,7 @@ import { resolveSignalAccount } from "../signal/accounts.js";
import { resolveSlackAccount, resolveSlackReplyToMode } from "../slack/accounts.js"; import { resolveSlackAccount, resolveSlackReplyToMode } from "../slack/accounts.js";
import { buildSlackThreadingToolContext } from "../slack/threading-tool-context.js"; import { buildSlackThreadingToolContext } from "../slack/threading-tool-context.js";
import { resolveTelegramAccount } from "../telegram/accounts.js"; import { resolveTelegramAccount } from "../telegram/accounts.js";
import { normalizeE164 } from "../utils.js"; import { escapeRegExp, normalizeE164 } from "../utils.js";
import { resolveWhatsAppAccount } from "../web/accounts.js"; import { resolveWhatsAppAccount } from "../web/accounts.js";
import { normalizeWhatsAppTarget } from "../whatsapp/normalize.js"; import { normalizeWhatsAppTarget } from "../whatsapp/normalize.js";
import { import {
@@ -76,8 +76,6 @@ const formatLower = (allowFrom: Array<string | number>) =>
.filter(Boolean) .filter(Boolean)
.map((entry) => entry.toLowerCase()); .map((entry) => entry.toLowerCase());
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// Channel docks: lightweight channel metadata/behavior for shared code paths. // Channel docks: lightweight channel metadata/behavior for shared code paths.
// //
// Rules: // Rules:
+3 -4
View File
@@ -1,14 +1,13 @@
import type { Command } from "commander"; import type { Command } from "commander";
import { formatErrorMessage } from "../infra/errors.js";
export { formatErrorMessage };
export type ManagerLookupResult<T> = { export type ManagerLookupResult<T> = {
manager: T | null; manager: T | null;
error?: string; error?: string;
}; };
export function formatErrorMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}
export async function withManager<T>(params: { export async function withManager<T>(params: {
getManager: () => Promise<ManagerLookupResult<T>>; getManager: () => Promise<ManagerLookupResult<T>>;
onMissing: (error?: string) => void; onMissing: (error?: string) => void;
+1 -4
View File
@@ -1,10 +1,7 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { stripAnsi } from "../terminal/ansi.js";
import { formatHealthCheckFailure } from "./health-format.js"; import { formatHealthCheckFailure } from "./health-format.js";
const ansiEscape = String.fromCharCode(27);
const ansiRegex = new RegExp(`${ansiEscape}\\[[0-9;]*m`, "g");
const stripAnsi = (input: string) => input.replace(ansiRegex, "");
describe("formatHealthCheckFailure", () => { describe("formatHealthCheckFailure", () => {
it("keeps non-rich output stable", () => { it("keeps non-rich output stable", () => {
const err = new Error("gateway closed (1006 abnormal closure): no close reason"); const err = new Error("gateway closed (1006 abnormal closure): no close reason");
+3 -9
View File
@@ -1,5 +1,6 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import type { SystemPresence } from "../infra/system-presence.js"; import type { SystemPresence } from "../infra/system-presence.js";
import { formatErrorMessage } from "../infra/errors.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import { GatewayClient } from "./client.js"; import { GatewayClient } from "./client.js";
@@ -26,13 +27,6 @@ export type GatewayProbeResult = {
configSnapshot: unknown; configSnapshot: unknown;
}; };
function formatError(err: unknown): string {
if (err instanceof Error) {
return err.message;
}
return String(err);
}
export async function probeGateway(opts: { export async function probeGateway(opts: {
url: string; url: string;
auth?: GatewayProbeAuth; auth?: GatewayProbeAuth;
@@ -65,7 +59,7 @@ export async function probeGateway(opts: {
mode: GATEWAY_CLIENT_MODES.PROBE, mode: GATEWAY_CLIENT_MODES.PROBE,
instanceId, instanceId,
onConnectError: (err) => { onConnectError: (err) => {
connectError = formatError(err); connectError = formatErrorMessage(err);
}, },
onClose: (code, reason) => { onClose: (code, reason) => {
close = { code, reason }; close = { code, reason };
@@ -93,7 +87,7 @@ export async function probeGateway(opts: {
settle({ settle({
ok: false, ok: false,
connectLatencyMs, connectLatencyMs,
error: formatError(err), error: formatErrorMessage(err),
close, close,
health: null, health: null,
status: null, status: null,
+1 -4
View File
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import type { GatewayRequestHandlers } from "./types.js"; import type { GatewayRequestHandlers } from "./types.js";
import { getResolvedLoggerSettings } from "../../logging.js"; import { getResolvedLoggerSettings } from "../../logging.js";
import { clamp } from "../../utils.js";
import { import {
ErrorCodes, ErrorCodes,
errorShape, errorShape,
@@ -15,10 +16,6 @@ const MAX_LIMIT = 5000;
const MAX_BYTES = 1_000_000; const MAX_BYTES = 1_000_000;
const ROLLING_LOG_RE = /^openclaw-\d{4}-\d{2}-\d{2}\.log$/; const ROLLING_LOG_RE = /^openclaw-\d{4}-\d{2}-\d{2}\.log$/;
function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(max, value));
}
function isRollingLogFile(file: string): boolean { function isRollingLogFile(file: string): boolean {
return ROLLING_LOG_RE.test(path.basename(file)); return ROLLING_LOG_RE.test(path.basename(file));
} }
+1 -5
View File
@@ -1,10 +1,6 @@
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { resolveConfigDir } from "../utils.js"; import { escapeRegExp, resolveConfigDir } from "../utils.js";
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function upsertSharedEnvVar(params: { export function upsertSharedEnvVar(params: {
key: string; key: string;
+5 -11
View File
@@ -1,6 +1,7 @@
import type { Llama, LlamaEmbeddingContext, LlamaModel } from "node-llama-cpp"; import type { Llama, LlamaEmbeddingContext, LlamaModel } from "node-llama-cpp";
import fsSync from "node:fs"; import fsSync from "node:fs";
import type { OpenClawConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/config.js";
import { formatErrorMessage } from "../infra/errors.js";
import { resolveUserPath } from "../utils.js"; import { resolveUserPath } from "../utils.js";
import { createGeminiEmbeddingProvider, type GeminiEmbeddingClient } from "./embeddings-gemini.js"; import { createGeminiEmbeddingProvider, type GeminiEmbeddingClient } from "./embeddings-gemini.js";
import { createOpenAiEmbeddingProvider, type OpenAiEmbeddingClient } from "./embeddings-openai.js"; import { createOpenAiEmbeddingProvider, type OpenAiEmbeddingClient } from "./embeddings-openai.js";
@@ -73,7 +74,7 @@ function canAutoSelectLocal(options: EmbeddingProviderOptions): boolean {
} }
function isMissingApiKeyError(err: unknown): boolean { function isMissingApiKeyError(err: unknown): boolean {
const message = formatError(err); const message = formatErrorMessage(err);
return message.includes("No API key found for provider"); return message.includes("No API key found for provider");
} }
@@ -149,7 +150,7 @@ export async function createEmbeddingProvider(
}; };
const formatPrimaryError = (err: unknown, provider: "openai" | "local" | "gemini" | "voyage") => const formatPrimaryError = (err: unknown, provider: "openai" | "local" | "gemini" | "voyage") =>
provider === "local" ? formatLocalSetupError(err) : formatError(err); provider === "local" ? formatLocalSetupError(err) : formatErrorMessage(err);
if (requestedProvider === "auto") { if (requestedProvider === "auto") {
const missingKeyErrors: string[] = []; const missingKeyErrors: string[] = [];
@@ -202,7 +203,7 @@ export async function createEmbeddingProvider(
} catch (fallbackErr) { } catch (fallbackErr) {
// oxlint-disable-next-line preserve-caught-error // oxlint-disable-next-line preserve-caught-error
throw new Error( throw new Error(
`${reason}\n\nFallback to ${fallback} failed: ${formatError(fallbackErr)}`, `${reason}\n\nFallback to ${fallback} failed: ${formatErrorMessage(fallbackErr)}`,
{ cause: fallbackErr }, { cause: fallbackErr },
); );
} }
@@ -211,13 +212,6 @@ export async function createEmbeddingProvider(
} }
} }
function formatError(err: unknown): string {
if (err instanceof Error) {
return err.message;
}
return String(err);
}
function isNodeLlamaCppMissing(err: unknown): boolean { function isNodeLlamaCppMissing(err: unknown): boolean {
if (!(err instanceof Error)) { if (!(err instanceof Error)) {
return false; return false;
@@ -230,7 +224,7 @@ function isNodeLlamaCppMissing(err: unknown): boolean {
} }
function formatLocalSetupError(err: unknown): string { function formatLocalSetupError(err: unknown): string {
const detail = formatError(err); const detail = formatErrorMessage(err);
const missing = isNodeLlamaCppMissing(err); const missing = isNodeLlamaCppMissing(err);
return [ return [
"Local embeddings unavailable.", "Local embeddings unavailable.",
+2 -1
View File
@@ -229,7 +229,8 @@ export {
} from "../agents/tools/common.js"; } from "../agents/tools/common.js";
export { formatDocsLink } from "../terminal/links.js"; export { formatDocsLink } from "../terminal/links.js";
export type { HookEntry } from "../hooks/types.js"; export type { HookEntry } from "../hooks/types.js";
export { normalizeE164 } from "../utils.js"; export { clamp, escapeRegExp, normalizeE164, sleep } from "../utils.js";
export { stripAnsi } from "../terminal/ansi.js";
export { missingTargetError } from "../infra/outbound/target-errors.js"; export { missingTargetError } from "../infra/outbound/target-errors.js";
export { registerLogTransport } from "../logging/logger.js"; export { registerLogTransport } from "../logging/logger.js";
export type { LogTransport, LogTransportRecord } from "../logging/logger.js"; export type { LogTransport, LogTransportRecord } from "../logging/logger.js";
+10
View File
@@ -21,6 +21,16 @@ export function clampInt(value: number, min: number, max: number): number {
return clampNumber(Math.floor(value), min, max); return clampNumber(Math.floor(value), min, max);
} }
/** Alias for clampNumber (shorter, more common name) */
export const clamp = clampNumber;
/**
* Escapes special regex characters in a string so it can be used in a RegExp constructor.
*/
export function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export type WebChannel = "web"; export type WebChannel = "web";
export function assertWebChannel(input: string): asserts input is WebChannel { export function assertWebChannel(input: string): asserts input is WebChannel {
+1 -2
View File
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import type { OpenClawConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/config.js";
import type { BackoffPolicy } from "../infra/backoff.js"; import type { BackoffPolicy } from "../infra/backoff.js";
import { computeBackoff, sleepWithAbort } from "../infra/backoff.js"; import { computeBackoff, sleepWithAbort } from "../infra/backoff.js";
import { clamp } from "../utils.js";
export type ReconnectPolicy = BackoffPolicy & { export type ReconnectPolicy = BackoffPolicy & {
maxAttempts: number; maxAttempts: number;
@@ -16,8 +17,6 @@ export const DEFAULT_RECONNECT_POLICY: ReconnectPolicy = {
maxAttempts: 12, maxAttempts: 12,
}; };
const clamp = (val: number, min: number, max: number) => Math.max(min, Math.min(max, val));
export function resolveHeartbeatSeconds(cfg: OpenClawConfig, overrideSeconds?: number): number { export function resolveHeartbeatSeconds(cfg: OpenClawConfig, overrideSeconds?: number): number {
const candidate = overrideSeconds ?? cfg.web?.heartbeatSeconds; const candidate = overrideSeconds ?? cfg.web?.heartbeatSeconds;
if (typeof candidate === "number" && candidate > 0) { if (typeof candidate === "number" && candidate > 0) {
+1 -2
View File
@@ -8,6 +8,7 @@ import path from "node:path";
import { afterAll, describe, expect, it } from "vitest"; import { afterAll, describe, expect, it } from "vitest";
import { GatewayClient } from "../src/gateway/client.js"; import { GatewayClient } from "../src/gateway/client.js";
import { loadOrCreateDeviceIdentity } from "../src/infra/device-identity.js"; import { loadOrCreateDeviceIdentity } from "../src/infra/device-identity.js";
import { sleep } from "../src/utils.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../src/utils/message-channel.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../src/utils/message-channel.js";
type GatewayInstance = { type GatewayInstance = {
@@ -32,8 +33,6 @@ type HealthPayload = { ok?: boolean };
const GATEWAY_START_TIMEOUT_MS = 45_000; const GATEWAY_START_TIMEOUT_MS = 45_000;
const E2E_TIMEOUT_MS = 120_000; const E2E_TIMEOUT_MS = 120_000;
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const getFreePort = async () => { const getFreePort = async () => {
const srv = net.createServer(); const srv = net.createServer();
await new Promise<void>((resolve) => srv.listen(0, "127.0.0.1", resolve)); await new Promise<void>((resolve) => srv.listen(0, "127.0.0.1", resolve));
+2 -4
View File
@@ -3,6 +3,8 @@ import {
formatZonedTimestamp, formatZonedTimestamp,
} from "../../src/infra/format-time/format-datetime.js"; } from "../../src/infra/format-time/format-datetime.js";
export { escapeRegExp } from "../../src/utils.js";
type EnvelopeTimestampZone = string; type EnvelopeTimestampZone = string;
export function formatEnvelopeTimestamp(date: Date, zone: EnvelopeTimestampZone = "utc"): string { export function formatEnvelopeTimestamp(date: Date, zone: EnvelopeTimestampZone = "utc"): string {
@@ -36,7 +38,3 @@ export function formatEnvelopeTimestamp(date: Date, zone: EnvelopeTimestampZone
export function formatLocalEnvelopeTimestamp(date: Date): string { export function formatLocalEnvelopeTimestamp(date: Date): string {
return formatEnvelopeTimestamp(date, "local"); return formatEnvelopeTimestamp(date, "local");
} }
export function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
+1 -29
View File
@@ -1,32 +1,4 @@
function stripAnsi(input: string): string { import { stripAnsi } from "../../src/terminal/ansi.js";
let out = "";
for (let i = 0; i < input.length; i++) {
const code = input.charCodeAt(i);
if (code !== 27) {
out += input[i];
continue;
}
const next = input[i + 1];
if (next !== "[") {
continue;
}
i += 1;
while (i + 1 < input.length) {
i += 1;
const c = input[i];
if (!c) {
break;
}
const isLetter = (c >= "A" && c <= "Z") || (c >= "a" && c <= "z") || c === "~";
if (isLetter) {
break;
}
}
}
return out;
}
export function normalizeTestText(input: string): string { export function normalizeTestText(input: string): string {
return stripAnsi(input) return stripAnsi(input)
+2 -4
View File
@@ -1,12 +1,10 @@
import { sleep } from "../../src/utils.js";
export type PollOptions = { export type PollOptions = {
timeoutMs?: number; timeoutMs?: number;
intervalMs?: number; intervalMs?: number;
}; };
function sleep(ms: number) {
return new Promise<void>((resolve) => setTimeout(resolve, ms));
}
export async function pollUntil<T>( export async function pollUntil<T>(
fn: () => Promise<T | null | undefined>, fn: () => Promise<T | null | undefined>,
opts: PollOptions = {}, opts: PollOptions = {},