mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 19:01:47 +03: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 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 } {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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+");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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,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,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,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, "\\$&");
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,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");
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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,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)
|
||||||
|
|||||||
@@ -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 = {},
|
||||||
|
|||||||
Reference in New Issue
Block a user