mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
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:
@@ -6,6 +6,7 @@ import {
|
||||
type MSTeamsReplyStyle,
|
||||
type ReplyPayload,
|
||||
SILENT_REPLY_TOKEN,
|
||||
sleep,
|
||||
} from "openclaw/plugin-sdk";
|
||||
import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
|
||||
import type { StoredConversationReference } from "./conversation-store.js";
|
||||
@@ -166,16 +167,6 @@ function clampMs(value: number, maxMs: number): number {
|
||||
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(
|
||||
retry: false | MSTeamsSendRetryOptions | undefined,
|
||||
): Required<MSTeamsSendRetryOptions> & { enabled: boolean } {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Command } from "commander";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { sleep } from "openclaw/plugin-sdk";
|
||||
import type { VoiceCallConfig } from "./config.js";
|
||||
import type { VoiceCallRuntime } from "./runtime.js";
|
||||
import { resolveUserPath } from "./utils.js";
|
||||
@@ -40,10 +41,6 @@ function resolveDefaultStorePath(config: VoiceCallConfig): string {
|
||||
return path.join(base, "calls.jsonl");
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function registerVoiceCallCli(params: {
|
||||
program: Command;
|
||||
config: VoiceCallConfig;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
collectWhatsAppStatusIssues,
|
||||
createActionGate,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
escapeRegExp,
|
||||
formatPairingApproveHint,
|
||||
getChatChannelMeta,
|
||||
isWhatsAppGroupJid,
|
||||
@@ -33,8 +34,6 @@ import { getWhatsAppRuntime } from "./runtime.js";
|
||||
|
||||
const meta = getChatChannelMeta("whatsapp");
|
||||
|
||||
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
||||
export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
|
||||
id: "whatsapp",
|
||||
meta: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { spawn, type SpawnOptions } from "node:child_process";
|
||||
import { stripAnsi } from "openclaw/plugin-sdk";
|
||||
import type { ZcaResult, ZcaRunOptions } from "./types.js";
|
||||
|
||||
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 {
|
||||
try {
|
||||
return JSON.parse(stdout) as T;
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
buildDockerExecArgs,
|
||||
buildSandboxEnv,
|
||||
chunkString,
|
||||
clampNumber,
|
||||
clampWithDefault,
|
||||
coerceEnv,
|
||||
killSession,
|
||||
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"),
|
||||
200_000,
|
||||
1_000,
|
||||
200_000,
|
||||
);
|
||||
const DEFAULT_PENDING_MAX_OUTPUT = clampNumber(
|
||||
const DEFAULT_PENDING_MAX_OUTPUT = clampWithDefault(
|
||||
readEnvInt("OPENCLAW_BASH_PENDING_MAX_OUTPUT_CHARS"),
|
||||
200_000,
|
||||
1_000,
|
||||
@@ -801,7 +801,7 @@ export function createExecTool(
|
||||
defaults?: ExecToolDefaults,
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
): AgentTool<any, ExecToolDetails> {
|
||||
const defaultBackgroundMs = clampNumber(
|
||||
const defaultBackgroundMs = clampWithDefault(
|
||||
defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"),
|
||||
10_000,
|
||||
10,
|
||||
@@ -860,7 +860,12 @@ export function createExecTool(
|
||||
const yieldWindow = allowBackground
|
||||
? backgroundRequested
|
||||
? 0
|
||||
: clampNumber(params.yieldMs ?? defaultBackgroundMs, defaultBackgroundMs, 10, 120_000)
|
||||
: clampWithDefault(
|
||||
params.yieldMs ?? defaultBackgroundMs,
|
||||
defaultBackgroundMs,
|
||||
10,
|
||||
120_000,
|
||||
)
|
||||
: null;
|
||||
const elevatedDefaults = defaults?.elevated;
|
||||
const elevatedAllowed = Boolean(elevatedDefaults?.enabled && elevatedDefaults.allowed);
|
||||
|
||||
@@ -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,
|
||||
defaultValue: number,
|
||||
min: number,
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { CliBackendConfig } from "../../config/types.js";
|
||||
import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
|
||||
import { runExec } from "../../process/exec.js";
|
||||
import { buildTtsSystemPromptHint } from "../../tts/tts.js";
|
||||
import { escapeRegExp } from "../../utils.js";
|
||||
import { resolveDefaultModelForAgent } from "../model-selection.js";
|
||||
import { detectRuntimeShell } from "../shell-utils.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>>();
|
||||
|
||||
function escapeRegex(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
export async function cleanupResumeProcesses(
|
||||
backend: CliBackendConfig,
|
||||
sessionId: string,
|
||||
@@ -43,7 +40,7 @@ export async function cleanupResumeProcesses(
|
||||
const resumeTokens = resumeArgs.map((arg) => arg.replaceAll("{sessionId}", sessionId));
|
||||
const pattern = [commandToken, ...resumeTokens]
|
||||
.filter(Boolean)
|
||||
.map((token) => escapeRegex(token))
|
||||
.map((token) => escapeRegExp(token))
|
||||
.join(".*");
|
||||
if (!pattern) {
|
||||
return;
|
||||
@@ -95,9 +92,9 @@ function buildSessionMatchers(backend: CliBackendConfig): RegExp[] {
|
||||
|
||||
function tokenToRegex(token: string): string {
|
||||
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+");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { escapeRegExp } from "../utils.js";
|
||||
|
||||
const ESC = "\x1b";
|
||||
const CR = "\r";
|
||||
const TAB = "\t";
|
||||
@@ -12,10 +14,6 @@ type Modifiers = {
|
||||
shift: boolean;
|
||||
};
|
||||
|
||||
function escapeRegExp(value: string) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
const namedKeyMap = new Map<string, string>([
|
||||
["enter", CR],
|
||||
["return", CR],
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
} from "./commands-registry.types.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
||||
import { escapeRegExp } from "../utils.js";
|
||||
import { getChatCommands, getNativeCommandSurfaces } from "./commands-registry.data.js";
|
||||
|
||||
export type {
|
||||
@@ -68,10 +69,6 @@ function getTextAliasMap(): Map<string, TextAliasSpec> {
|
||||
return map;
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function buildSkillCommandDefinitions(skillCommands?: SkillCommandSpec[]): ChatCommandDefinition[] {
|
||||
if (!skillCommands || skillCommands.length === 0) {
|
||||
return [];
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
function escapeRegExp(value: string) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
import { escapeRegExp } from "../utils.js";
|
||||
|
||||
export function extractModelDirective(
|
||||
body?: string,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { NoticeLevel, ReasoningLevel } from "../thinking.js";
|
||||
import { escapeRegExp } from "../../utils.js";
|
||||
import {
|
||||
type ElevatedLevel,
|
||||
normalizeElevatedLevel,
|
||||
@@ -17,8 +18,6 @@ type ExtractedLevel<T> = {
|
||||
hasDirective: boolean;
|
||||
};
|
||||
|
||||
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
||||
const matchLevelDirective = (
|
||||
body: string,
|
||||
names: string[],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import { normalizeChatType } from "../../channels/chat-type.js";
|
||||
import { listSenderLabelCandidates, resolveSenderLabel } from "../../channels/sender-label.js";
|
||||
import { escapeRegExp } from "../../utils.js";
|
||||
|
||||
export function formatInboundBodyWithSenderMeta(params: { body: string; ctx: MsgContext }): string {
|
||||
const body = params.body;
|
||||
@@ -51,7 +52,3 @@ function hasSenderMetaLine(body: string, ctx: MsgContext): boolean {
|
||||
return pattern.test(body);
|
||||
});
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ import type { MsgContext } from "../templating.js";
|
||||
import { resolveAgentConfig } from "../../agents/agent-scope.js";
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
|
||||
function escapeRegExp(text: string): string {
|
||||
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
import { escapeRegExp } from "../../utils.js";
|
||||
|
||||
function deriveMentionPatterns(identity?: { name?: string; emoji?: string }) {
|
||||
const patterns: string[] = [];
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { escapeRegExp } from "../utils.js";
|
||||
|
||||
export const HEARTBEAT_TOKEN = "HEARTBEAT_OK";
|
||||
export const SILENT_REPLY_TOKEN = "NO_REPLY";
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
export function isSilentReplyText(
|
||||
text: string | undefined,
|
||||
token: string = SILENT_REPLY_TOKEN,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { BrowserRouteContext } from "../server-context.js";
|
||||
import type { BrowserRequest, BrowserResponse, BrowserRouteRegistrar } from "./types.js";
|
||||
import { escapeRegExp } from "../../utils.js";
|
||||
import { registerBrowserRoutes } from "./index.js";
|
||||
|
||||
type BrowserDispatchRequest = {
|
||||
@@ -22,10 +23,6 @@ type RouteEntry = {
|
||||
handler: (req: BrowserRequest, res: BrowserResponse) => void | Promise<void>;
|
||||
};
|
||||
|
||||
function escapeRegex(value: string) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function compileRoute(path: string): { regex: RegExp; paramNames: string[] } {
|
||||
const paramNames: string[] = [];
|
||||
const parts = path.split("/").map((part) => {
|
||||
@@ -34,7 +31,7 @@ function compileRoute(path: string): { regex: RegExp; paramNames: string[] } {
|
||||
paramNames.push(name);
|
||||
return "([^/]+)";
|
||||
}
|
||||
return escapeRegex(part);
|
||||
return escapeRegExp(part);
|
||||
});
|
||||
return { regex: new RegExp(`^${parts.join("/")}$`), paramNames };
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { resolveSignalAccount } from "../signal/accounts.js";
|
||||
import { resolveSlackAccount, resolveSlackReplyToMode } from "../slack/accounts.js";
|
||||
import { buildSlackThreadingToolContext } from "../slack/threading-tool-context.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 { normalizeWhatsAppTarget } from "../whatsapp/normalize.js";
|
||||
import {
|
||||
@@ -76,8 +76,6 @@ const formatLower = (allowFrom: Array<string | number>) =>
|
||||
.filter(Boolean)
|
||||
.map((entry) => entry.toLowerCase());
|
||||
|
||||
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
||||
// Channel docks: lightweight channel metadata/behavior for shared code paths.
|
||||
//
|
||||
// Rules:
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { Command } from "commander";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
|
||||
export { formatErrorMessage };
|
||||
|
||||
export type ManagerLookupResult<T> = {
|
||||
manager: T | null;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export function formatErrorMessage(err: unknown): string {
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
|
||||
export async function withManager<T>(params: {
|
||||
getManager: () => Promise<ManagerLookupResult<T>>;
|
||||
onMissing: (error?: string) => void;
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { stripAnsi } from "../terminal/ansi.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", () => {
|
||||
it("keeps non-rich output stable", () => {
|
||||
const err = new Error("gateway closed (1006 abnormal closure): no close reason");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
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 { GatewayClient } from "./client.js";
|
||||
|
||||
@@ -26,13 +27,6 @@ export type GatewayProbeResult = {
|
||||
configSnapshot: unknown;
|
||||
};
|
||||
|
||||
function formatError(err: unknown): string {
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
}
|
||||
return String(err);
|
||||
}
|
||||
|
||||
export async function probeGateway(opts: {
|
||||
url: string;
|
||||
auth?: GatewayProbeAuth;
|
||||
@@ -65,7 +59,7 @@ export async function probeGateway(opts: {
|
||||
mode: GATEWAY_CLIENT_MODES.PROBE,
|
||||
instanceId,
|
||||
onConnectError: (err) => {
|
||||
connectError = formatError(err);
|
||||
connectError = formatErrorMessage(err);
|
||||
},
|
||||
onClose: (code, reason) => {
|
||||
close = { code, reason };
|
||||
@@ -93,7 +87,7 @@ export async function probeGateway(opts: {
|
||||
settle({
|
||||
ok: false,
|
||||
connectLatencyMs,
|
||||
error: formatError(err),
|
||||
error: formatErrorMessage(err),
|
||||
close,
|
||||
health: null,
|
||||
status: null,
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { GatewayRequestHandlers } from "./types.js";
|
||||
import { getResolvedLoggerSettings } from "../../logging.js";
|
||||
import { clamp } from "../../utils.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
@@ -15,10 +16,6 @@ const MAX_LIMIT = 5000;
|
||||
const MAX_BYTES = 1_000_000;
|
||||
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 {
|
||||
return ROLLING_LOG_RE.test(path.basename(file));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolveConfigDir } from "../utils.js";
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
import { escapeRegExp, resolveConfigDir } from "../utils.js";
|
||||
|
||||
export function upsertSharedEnvVar(params: {
|
||||
key: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Llama, LlamaEmbeddingContext, LlamaModel } from "node-llama-cpp";
|
||||
import fsSync from "node:fs";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { createGeminiEmbeddingProvider, type GeminiEmbeddingClient } from "./embeddings-gemini.js";
|
||||
import { createOpenAiEmbeddingProvider, type OpenAiEmbeddingClient } from "./embeddings-openai.js";
|
||||
@@ -73,7 +74,7 @@ function canAutoSelectLocal(options: EmbeddingProviderOptions): boolean {
|
||||
}
|
||||
|
||||
function isMissingApiKeyError(err: unknown): boolean {
|
||||
const message = formatError(err);
|
||||
const message = formatErrorMessage(err);
|
||||
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") =>
|
||||
provider === "local" ? formatLocalSetupError(err) : formatError(err);
|
||||
provider === "local" ? formatLocalSetupError(err) : formatErrorMessage(err);
|
||||
|
||||
if (requestedProvider === "auto") {
|
||||
const missingKeyErrors: string[] = [];
|
||||
@@ -202,7 +203,7 @@ export async function createEmbeddingProvider(
|
||||
} catch (fallbackErr) {
|
||||
// oxlint-disable-next-line preserve-caught-error
|
||||
throw new Error(
|
||||
`${reason}\n\nFallback to ${fallback} failed: ${formatError(fallbackErr)}`,
|
||||
`${reason}\n\nFallback to ${fallback} failed: ${formatErrorMessage(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 {
|
||||
if (!(err instanceof Error)) {
|
||||
return false;
|
||||
@@ -230,7 +224,7 @@ function isNodeLlamaCppMissing(err: unknown): boolean {
|
||||
}
|
||||
|
||||
function formatLocalSetupError(err: unknown): string {
|
||||
const detail = formatError(err);
|
||||
const detail = formatErrorMessage(err);
|
||||
const missing = isNodeLlamaCppMissing(err);
|
||||
return [
|
||||
"Local embeddings unavailable.",
|
||||
|
||||
@@ -229,7 +229,8 @@ export {
|
||||
} from "../agents/tools/common.js";
|
||||
export { formatDocsLink } from "../terminal/links.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 { registerLogTransport } from "../logging/logger.js";
|
||||
export type { LogTransport, LogTransportRecord } from "../logging/logger.js";
|
||||
|
||||
10
src/utils.ts
10
src/utils.ts
@@ -21,6 +21,16 @@ export function clampInt(value: number, min: number, max: number): number {
|
||||
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 function assertWebChannel(input: string): asserts input is WebChannel {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { BackoffPolicy } from "../infra/backoff.js";
|
||||
import { computeBackoff, sleepWithAbort } from "../infra/backoff.js";
|
||||
import { clamp } from "../utils.js";
|
||||
|
||||
export type ReconnectPolicy = BackoffPolicy & {
|
||||
maxAttempts: number;
|
||||
@@ -16,8 +17,6 @@ export const DEFAULT_RECONNECT_POLICY: ReconnectPolicy = {
|
||||
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 {
|
||||
const candidate = overrideSeconds ?? cfg.web?.heartbeatSeconds;
|
||||
if (typeof candidate === "number" && candidate > 0) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import path from "node:path";
|
||||
import { afterAll, describe, expect, it } from "vitest";
|
||||
import { GatewayClient } from "../src/gateway/client.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";
|
||||
|
||||
type GatewayInstance = {
|
||||
@@ -32,8 +33,6 @@ type HealthPayload = { ok?: boolean };
|
||||
const GATEWAY_START_TIMEOUT_MS = 45_000;
|
||||
const E2E_TIMEOUT_MS = 120_000;
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const getFreePort = async () => {
|
||||
const srv = net.createServer();
|
||||
await new Promise<void>((resolve) => srv.listen(0, "127.0.0.1", resolve));
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
formatZonedTimestamp,
|
||||
} from "../../src/infra/format-time/format-datetime.js";
|
||||
|
||||
export { escapeRegExp } from "../../src/utils.js";
|
||||
|
||||
type EnvelopeTimestampZone = 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 {
|
||||
return formatEnvelopeTimestamp(date, "local");
|
||||
}
|
||||
|
||||
export function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
@@ -1,32 +1,4 @@
|
||||
function stripAnsi(input: string): string {
|
||||
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;
|
||||
}
|
||||
import { stripAnsi } from "../../src/terminal/ansi.js";
|
||||
|
||||
export function normalizeTestText(input: string): string {
|
||||
return stripAnsi(input)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { sleep } from "../../src/utils.js";
|
||||
|
||||
export type PollOptions = {
|
||||
timeoutMs?: number;
|
||||
intervalMs?: number;
|
||||
};
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export async function pollUntil<T>(
|
||||
fn: () => Promise<T | null | undefined>,
|
||||
opts: PollOptions = {},
|
||||
|
||||
Reference in New Issue
Block a user