diff --git a/extensions/copilot/src/permission-bridge.ts b/extensions/copilot/src/permission-bridge.ts index 514f0f916781..4381a7d68386 100755 --- a/extensions/copilot/src/permission-bridge.ts +++ b/extensions/copilot/src/permission-bridge.ts @@ -30,6 +30,7 @@ import type { PermissionRequest as SdkPermissionRequest, PermissionRequestResult as SdkPermissionRequestResult, } from "@github/copilot-sdk"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; /** Request shape forwarded to host-implemented policies. */ interface CopilotPermissionContext { @@ -83,20 +84,9 @@ export function createPermissionBridge( } catch (error) { return { kind: "reject", - feedback: `copilot permission policy threw: ${formatError(error)}`, + feedback: `copilot permission policy threw: ${formatErrorMessage(error)}`, }; } return { kind: "reject", feedback: REJECT_ALL_FEEDBACK }; }; } - -function formatError(error: unknown): string { - if (error instanceof Error) { - return error.message; - } - try { - return JSON.stringify(error); - } catch { - return String(error); - } -} diff --git a/extensions/discord/src/monitor/ingress.ts b/extensions/discord/src/monitor/ingress.ts index d1caeeb4ebf1..c5052bf539bc 100644 --- a/extensions/discord/src/monitor/ingress.ts +++ b/extensions/discord/src/monitor/ingress.ts @@ -7,6 +7,7 @@ import { type ChannelIngressMonitorDeliveryResult, type ChannelIngressMonitorLifecycle, } from "openclaw/plugin-sdk/channel-outbound"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { danger, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { normalizeNullableString as nonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { Client } from "../internal/discord.js"; @@ -82,10 +83,6 @@ function decodeDiscordIngressPayload( }; } -function errorText(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function isDiscordAuthenticationFailure(error: unknown): boolean { let current: unknown = error; const seen = new Set(); @@ -155,14 +152,14 @@ export function createDiscordIngressMonitor(params: { return { reason: "invalid-event", message: error.message }; } if (isDiscordAuthenticationFailure(error)) { - return { reason: "authentication-failed", message: errorText(error) }; + return { reason: "authentication-failed", message: formatErrorMessage(error) }; } return null; }, onLog: (message) => params.runtime.error?.(danger(`discord ingress: ${message}`)), }, onError: (error) => - params.runtime.error?.(danger(`discord ingress drain failed: ${errorText(error)}`)), + params.runtime.error?.(danger(`discord ingress drain failed: ${formatErrorMessage(error)}`)), }); return { diff --git a/extensions/discord/src/send.messages.ts b/extensions/discord/src/send.messages.ts index 4d0aad8b0080..9148c7087ccb 100644 --- a/extensions/discord/src/send.messages.ts +++ b/extensions/discord/src/send.messages.ts @@ -1,6 +1,7 @@ // Discord plugin module implements send.messages behavior. import type { APIChannel, APIMessage } from "discord-api-types/v10"; import { ChannelType } from "discord-api-types/v10"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { createChannelMessage, createThread, @@ -26,10 +27,6 @@ import type { DiscordThreadList, } from "./send.types.js"; -function formatDiscordThreadInitialMessageError(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function assertDiscordResponseArray(value: unknown, label: string): T[] { if (!Array.isArray(value)) { throw new Error(`Unexpected Discord response for ${label}: expected array.`); @@ -56,7 +53,7 @@ export class DiscordThreadInitialMessageError extends Error { readonly thread: APIChannel; constructor(thread: APIChannel, error: unknown) { - const initialMessageError = formatDiscordThreadInitialMessageError(error); + const initialMessageError = formatErrorMessage(error); super( `Discord thread was created, but sending the initial message failed: ${initialMessageError}`, ); diff --git a/extensions/google-meet/src/meet-api.ts b/extensions/google-meet/src/meet-api.ts index 9eb797663356..93d76b1058d2 100644 --- a/extensions/google-meet/src/meet-api.ts +++ b/extensions/google-meet/src/meet-api.ts @@ -259,10 +259,6 @@ function assertResourceArray( return resources; } -export function getErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - async function requestGoogleMeetApi(params: { accessToken: string; path: string; diff --git a/extensions/google-meet/src/meet.ts b/extensions/google-meet/src/meet.ts index 9dff0b352954..501b6ac4d027 100644 --- a/extensions/google-meet/src/meet.ts +++ b/extensions/google-meet/src/meet.ts @@ -1,3 +1,4 @@ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime"; import { exportGoogleDriveDocumentText, extractGoogleDriveDocumentId } from "./drive.js"; import { @@ -5,7 +6,6 @@ import { endGoogleMeetActiveConference, fetchGoogleMeetSpace, fetchLatestGoogleMeetConferenceRecord, - getErrorMessage, listGoogleMeetParticipants, listGoogleMeetParticipantSessions, listGoogleMeetRecordings, @@ -82,7 +82,7 @@ async function attachDocumentText((smartNotes) => ({ smartNotes })) .catch((error: unknown) => ({ smartNotes: [], - smartNotesError: getErrorMessage(error), + smartNotesError: formatErrorMessage(error), })), ]); const transcriptEntries = @@ -299,7 +299,7 @@ export async function fetchGoogleMeetArtifacts(params: { return { transcript: transcript.name, entries: [], - entriesError: getErrorMessage(error), + entriesError: formatErrorMessage(error), }; } }), diff --git a/extensions/line/src/webhook-spool.ts b/extensions/line/src/webhook-spool.ts index b4cfd131ce06..a8d67f8c2a56 100644 --- a/extensions/line/src/webhook-spool.ts +++ b/extensions/line/src/webhook-spool.ts @@ -8,6 +8,7 @@ import { DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, type ChannelIngressQueue, } from "openclaw/plugin-sdk/channel-outbound"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { danger, type RuntimeEnv, warn } from "openclaw/plugin-sdk/runtime-env"; import { normalizeNullableString as nonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards"; @@ -126,10 +127,6 @@ function parseStoredEvent(rawEvent: string): webhook.Event { return event as webhook.Event; } -function errorText(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function isLineAuthenticationFailure(error: unknown): boolean { if (!error || typeof error !== "object") { return false; @@ -226,7 +223,9 @@ export function createLineWebhookSpool(options: LineWebhookSpoolOptions): LineWe .then(() => boundLifecycle.onAbandoned()) .catch((error: unknown) => { options.runtime.error?.( - danger(`line: failed to abandon a late webhook delivery: ${errorText(error)}`), + danger( + `line: failed to abandon a late webhook delivery: ${formatErrorMessage(error)}`, + ), ); }); return; @@ -285,7 +284,7 @@ export function createLineWebhookSpool(options: LineWebhookSpoolOptions): LineWe return { reason: error.reason, message: error.message }; } if (isLineAuthenticationFailure(error)) { - return { reason: "authentication-failed", message: errorText(error) }; + return { reason: "authentication-failed", message: formatErrorMessage(error) }; } return null; }, @@ -293,7 +292,9 @@ export function createLineWebhookSpool(options: LineWebhookSpoolOptions): LineWe }, createStoppedError: () => new Error("LINE webhook spool is stopped."), onError: (error) => - options.runtime.error?.(danger(`line: webhook spool drain failed: ${errorText(error)}`)), + options.runtime.error?.( + danger(`line: webhook spool drain failed: ${formatErrorMessage(error)}`), + ), }); let stopTask: Promise | undefined; diff --git a/extensions/llama-cpp/src/node-llama.runtime.ts b/extensions/llama-cpp/src/node-llama.runtime.ts index 0a4bef675804..43953d157d64 100644 --- a/extensions/llama-cpp/src/node-llama.runtime.ts +++ b/extensions/llama-cpp/src/node-llama.runtime.ts @@ -1,5 +1,6 @@ import { createRequire } from "node:module"; import { pathToFileURL } from "node:url"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; export type NodeLlamaCppModule = typeof import("node-llama-cpp"); @@ -11,10 +12,6 @@ function isNodeLlamaCppMissing(error: unknown): boolean { return code === "ERR_MODULE_NOT_FOUND" && error.message.includes("node-llama-cpp"); } -function formatErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - export function formatLlamaCppSetupError(error: unknown): string { const detail = formatErrorMessage(error); const missing = isNodeLlamaCppMissing(error); diff --git a/extensions/matrix/src/channel-account-paths.ts b/extensions/matrix/src/channel-account-paths.ts index dc086de73e9a..8a9965ab31bf 100644 --- a/extensions/matrix/src/channel-account-paths.ts +++ b/extensions/matrix/src/channel-account-paths.ts @@ -1,8 +1,8 @@ // Matrix plugin module implements channel account paths behavior. import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing"; import { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/channel-status"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import type { PinnedDispatcherPolicy, SsrFPolicy } from "openclaw/plugin-sdk/ssrf-dispatcher"; -import { formatMatrixErrorMessage } from "./matrix/errors.js"; import type { MatrixProbe } from "./matrix/probe.js"; import type { CoreConfig } from "./types.js"; @@ -66,7 +66,7 @@ export function createMatrixProbeAccount(params: { } catch (err) { return { ok: false, - error: formatMatrixErrorMessage(err), + error: formatErrorMessage(err), elapsedMs: 0, }; } diff --git a/extensions/matrix/src/cli-account.ts b/extensions/matrix/src/cli-account.ts index 3c4d29e92f44..ba954f91748b 100644 --- a/extensions/matrix/src/cli-account.ts +++ b/extensions/matrix/src/cli-account.ts @@ -1,5 +1,6 @@ import type { Command } from "commander"; import { normalizeAccountId } from "openclaw/plugin-sdk/account-id"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import * as cli from "./cli-shared.js"; import { resolveMatrixAccountConfig } from "./matrix/accounts.js"; import { listMatrixOwnDevices } from "./matrix/actions/devices.js"; @@ -162,7 +163,7 @@ async function addMatrixAccount(params: { avatarUpdated: false, resolvedAvatarUrl: null, convertedAvatarFromHttp: false, - error: cli.formatMatrixErrorMessage(err), + error: formatErrorMessage(err), }; } } @@ -180,7 +181,7 @@ async function addMatrixAccount(params: { deviceHealth = { currentDeviceId: null, staleOpenClawDeviceIds: [], - error: cli.formatMatrixErrorMessage(err), + error: formatErrorMessage(err), }; } diff --git a/extensions/matrix/src/cli-shared.ts b/extensions/matrix/src/cli-shared.ts index 56ba73a2aa13..9afb5a044dda 100644 --- a/extensions/matrix/src/cli-shared.ts +++ b/extensions/matrix/src/cli-shared.ts @@ -1,18 +1,16 @@ import { normalizeAccountId } from "openclaw/plugin-sdk/account-id"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { parseStrictInteger } from "openclaw/plugin-sdk/number-runtime"; import { readByteStreamWithLimit } from "openclaw/plugin-sdk/response-limit-runtime"; import { resolveMatrixRoomKeyBackupIssue } from "./matrix/backup-health.js"; import { resolveMatrixAuthContext } from "./matrix/client.js"; import { setMatrixSdkConsoleLogging, setMatrixSdkLogMode } from "./matrix/client/logging.js"; -import { formatMatrixErrorMessage } from "./matrix/errors.js"; import type { MatrixOwnDeviceVerificationStatus, MatrixRoomKeyBackupStatus } from "./matrix/sdk.js"; import type { MatrixVerificationSummary } from "./matrix/sdk/verification-manager.js"; import { formatZonedTimestamp } from "./runtime-api.js"; import { getMatrixRuntime } from "./runtime.js"; import type { CoreConfig } from "./types.js"; -export { formatMatrixErrorMessage }; - let matrixCliExitScheduled = false; const MATRIX_CLI_RECOVERY_KEY_STDIN_MAX_BYTES = 1024 * 1024; @@ -202,7 +200,7 @@ export async function runMatrixCliCommand( markCliFailure(); } } catch (err) { - const message = formatMatrixErrorMessage(err); + const message = formatErrorMessage(err); if (config.json) { printJson(config.onJsonError ? config.onJsonError(message) : { error: message }); } else { diff --git a/extensions/matrix/src/matrix/errors.ts b/extensions/matrix/src/matrix/errors.ts index 87b31854fee2..e726f530d4c6 100644 --- a/extensions/matrix/src/matrix/errors.ts +++ b/extensions/matrix/src/matrix/errors.ts @@ -2,12 +2,8 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime"; -export function formatMatrixErrorMessage(err: unknown): string { - return formatErrorMessage(err); -} - export function formatMatrixErrorReason(err: unknown): string { - return normalizeLowercaseStringOrEmpty(formatMatrixErrorMessage(err)); + return normalizeLowercaseStringOrEmpty(formatErrorMessage(err)); } export function isMatrixNotFoundError(err: unknown): boolean { diff --git a/extensions/matrix/src/matrix/monitor/handler-ingress-content.ts b/extensions/matrix/src/matrix/monitor/handler-ingress-content.ts index 97aa1f166dd8..d695921af7aa 100644 --- a/extensions/matrix/src/matrix/monitor/handler-ingress-content.ts +++ b/extensions/matrix/src/matrix/monitor/handler-ingress-content.ts @@ -1,6 +1,6 @@ import { resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-inbound"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { buildInboundHistoryFromEntries } from "openclaw/plugin-sdk/reply-history"; -import { formatMatrixErrorMessage } from "../errors.js"; import { isMatrixMediaSizeLimitError } from "../media-errors.js"; import { isLikelyBareFilename } from "../media-text.js"; import { fetchMatrixPollSnapshot, type MatrixPollSnapshot } from "../poll-summary.js"; @@ -218,7 +218,7 @@ export async function resolveMatrixIngressContent(config: { if (isMatrixMediaSizeLimitError(err)) { preflightMediaSizeLimitExceeded = true; } - const errorText = formatMatrixErrorMessage(err); + const errorText = formatErrorMessage(err); logVerboseMessage( `matrix: media download failed room=${roomId} id=${event.event_id ?? "unknown"} type=${content.msgtype} error=${errorText}`, ); @@ -413,7 +413,7 @@ export async function resolveMatrixIngressContent(config: { if (isMatrixMediaSizeLimitError(err)) { mediaSizeLimitExceeded = true; } - const errorText = formatMatrixErrorMessage(err); + const errorText = formatErrorMessage(err); logVerboseMessage( `matrix: media download failed room=${roomId} id=${event.event_id ?? "unknown"} type=${content.msgtype} error=${errorText}`, ); diff --git a/extensions/matrix/src/matrix/monitor/startup-verification.ts b/extensions/matrix/src/matrix/monitor/startup-verification.ts index 85498ca69431..94426c4baea9 100644 --- a/extensions/matrix/src/matrix/monitor/startup-verification.ts +++ b/extensions/matrix/src/matrix/monitor/startup-verification.ts @@ -2,13 +2,13 @@ import { createHash } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store"; import { timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime"; import { getMatrixRuntime } from "../../runtime.js"; import type { MatrixConfig } from "../../types.js"; import { recordCurrentStorageMetaDeviceId, resolveMatrixStoragePaths } from "../client/storage.js"; import type { MatrixAuth } from "../client/types.js"; -import { formatMatrixErrorMessage } from "../errors.js"; import type { MatrixClient, MatrixOwnDeviceVerificationStatus } from "../sdk.js"; import { resolveMatrixSqliteStateEnv } from "../sqlite-state.js"; @@ -388,7 +388,7 @@ export async function ensureMatrixStartupVerification(params: { transactionId: request.transactionId ?? undefined, }; } catch (err) { - const error = formatMatrixErrorMessage(err); + const error = formatErrorMessage(err); await writeStartupVerificationState({ auth: params.auth, env: params.env, diff --git a/extensions/matrix/src/matrix/monitor/status.ts b/extensions/matrix/src/matrix/monitor/status.ts index 70b364113d46..2b1e842ff8b2 100644 --- a/extensions/matrix/src/matrix/monitor/status.ts +++ b/extensions/matrix/src/matrix/monitor/status.ts @@ -1,10 +1,10 @@ // Matrix plugin module implements status behavior. import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/channel-contract"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { createConnectedChannelStatusPatch, createTransportActivityStatusPatch, } from "openclaw/plugin-sdk/gateway-runtime"; -import { formatMatrixErrorMessage } from "../errors.js"; import { isMatrixDisconnectedSyncState, isMatrixReadySyncState, @@ -29,7 +29,7 @@ function formatSyncError(error: unknown): string | null { if (error instanceof Error) { return error.message || error.name || "unknown"; } - return formatMatrixErrorMessage(error); + return formatErrorMessage(error); } export type MatrixMonitorStatusController = ReturnType; diff --git a/extensions/matrix/src/matrix/sdk.ts b/extensions/matrix/src/matrix/sdk.ts index 2751dcbe55df..e5da81daf9fb 100644 --- a/extensions/matrix/src/matrix/sdk.ts +++ b/extensions/matrix/src/matrix/sdk.ts @@ -1,8 +1,9 @@ // Matrix plugin module implements sdk behavior. import type { Room } from "matrix-js-sdk/lib/models/room.js"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { normalizeStringEntries, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime"; import { resolveMatrixRoomKeyBackupReadinessError } from "./backup-health.js"; -import { formatMatrixErrorMessage, isMatrixNotFoundError } from "./errors.js"; +import { isMatrixNotFoundError } from "./errors.js"; import { listMatrixOwnDevices, resolveMatrixCrossSigningPublicationStatus, @@ -104,7 +105,7 @@ export class MatrixClient extends MatrixClientVerification { keyId: stagedKeyId, }); } catch (err) { - return await fail(formatMatrixErrorMessage(err)); + return await fail(formatErrorMessage(err)); } const storedRecoveryKeyMatches = @@ -260,7 +261,7 @@ export class MatrixClient extends MatrixClientVerification { }; } catch (err) { this.recoveryKeyStore.discardStagedRecoveryKey(); - return await fail(formatMatrixErrorMessage(err)); + return await fail(formatErrorMessage(err)); } } @@ -335,7 +336,7 @@ export class MatrixClient extends MatrixClientVerification { }; } catch (err) { this.recoveryKeyStore.discardStagedRecoveryKey(); - return await fail(formatMatrixErrorMessage(err)); + return await fail(formatErrorMessage(err)); } } @@ -437,7 +438,7 @@ export class MatrixClient extends MatrixClientVerification { backup, }; } catch (err) { - return await fail(formatMatrixErrorMessage(err)); + return await fail(formatErrorMessage(err)); } } @@ -510,7 +511,7 @@ export class MatrixClient extends MatrixClientVerification { await this.ensureRoomKeyBackupEnabled(crypto); } catch (err) { this.recoveryKeyStore.discardStagedRecoveryKey(); - bootstrapError = formatMatrixErrorMessage(err); + bootstrapError = formatErrorMessage(err); } const verification = await this.getOwnDeviceVerificationStatus(); diff --git a/extensions/matrix/src/matrix/sdk/client-verification.ts b/extensions/matrix/src/matrix/sdk/client-verification.ts index e7a85153362b..8853244bcbdc 100644 --- a/extensions/matrix/src/matrix/sdk/client-verification.ts +++ b/extensions/matrix/src/matrix/sdk/client-verification.ts @@ -1,5 +1,5 @@ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { normalizeNullableString } from "openclaw/plugin-sdk/string-coerce-runtime"; -import { formatMatrixErrorMessage } from "../errors.js"; import { loadMatrixCryptoRuntime } from "./client-base.js"; import { MatrixClientCore } from "./client-core.js"; import { @@ -65,7 +65,7 @@ export abstract class MatrixClientVerification extends MatrixClientCore { try { await crypto.loadSessionBackupPrivateKeyFromSecretStorage(); // pragma: allowlist secret } catch (err) { - keyLoadError = formatMatrixErrorMessage(err); + keyLoadError = formatErrorMessage(err); } } else { keyLoadError = diff --git a/extensions/matrix/src/matrix/sdk/recovery-key-store.ts b/extensions/matrix/src/matrix/sdk/recovery-key-store.ts index 28b769fc9e59..c7176e388a27 100644 --- a/extensions/matrix/src/matrix/sdk/recovery-key-store.ts +++ b/extensions/matrix/src/matrix/sdk/recovery-key-store.ts @@ -1,13 +1,14 @@ import path from "node:path"; // Matrix plugin module implements recovery key store behavior. import { decodeRecoveryKey } from "matrix-js-sdk/lib/crypto-api/recovery-key.js"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { migrateLegacyMatrixRecoveryKeyFilePathToStore, readLegacyMatrixRecoveryKeyFile, readMatrixRecoveryKeyStateForPath, writeMatrixRecoveryKeyStateForPath, } from "../crypto-state-store.js"; -import { formatMatrixErrorMessage, formatMatrixErrorReason } from "../errors.js"; +import { formatMatrixErrorReason } from "../errors.js"; import { LogService } from "./logger.js"; import type { MatrixCryptoBootstrapApi, @@ -174,7 +175,7 @@ export class MatrixRecoveryKeyStore { try { privateKey = decodeRecoveryKey(encodedPrivateKey); } catch (err) { - throw new Error(`Invalid Matrix recovery key: ${formatMatrixErrorMessage(err)}`, { + throw new Error(`Invalid Matrix recovery key: ${formatErrorMessage(err)}`, { cause: err, }); } diff --git a/extensions/matrix/src/matrix/sdk/verification-manager.ts b/extensions/matrix/src/matrix/sdk/verification-manager.ts index e9692b353193..de4d49f509b7 100644 --- a/extensions/matrix/src/matrix/sdk/verification-manager.ts +++ b/extensions/matrix/src/matrix/sdk/verification-manager.ts @@ -4,13 +4,13 @@ import { VerifierEvent, } from "matrix-js-sdk/lib/crypto-api/verification.js"; import { VerificationMethod } from "matrix-js-sdk/lib/types.js"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; // Matrix plugin module implements verification manager behavior. import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { resolveDateTimestampMs, resolveTimestampMsToIsoString, } from "openclaw/plugin-sdk/number-runtime"; -import { formatMatrixErrorMessage } from "../errors.js"; export type MatrixVerificationMethod = "sas" | "show-qr" | "scan-qr"; type MatrixVerificationPhase = VerificationPhase | -1; @@ -398,7 +398,7 @@ export class MatrixVerificationManager { }) .catch((err: unknown) => { session.acceptRequested = false; - session.error = formatMatrixErrorMessage(err); + session.error = formatErrorMessage(err); this.touchVerificationSession(session); }); } @@ -483,7 +483,7 @@ export class MatrixVerificationManager { }); verifier.on(VerifierEvent.Cancel, (err) => { this.clearSasAutoConfirmTimer(session); - session.error = formatMatrixErrorMessage(err); + session.error = formatErrorMessage(err); this.touchVerificationSession(session); }); this.ensureVerificationStarted(session); @@ -519,7 +519,7 @@ export class MatrixVerificationManager { this.touchVerificationSession(session); }) .catch((err: unknown) => { - session.error = formatMatrixErrorMessage(err); + session.error = formatErrorMessage(err); this.touchVerificationSession(session); }); }, SAS_AUTO_CONFIRM_DELAY_MS); @@ -548,7 +548,7 @@ export class MatrixVerificationManager { this.touchVerificationSession(session); }) .catch((err: unknown) => { - session.error = formatMatrixErrorMessage(err); + session.error = formatErrorMessage(err); this.touchVerificationSession(session); }); } diff --git a/extensions/matrix/src/plugin-entry.runtime.ts b/extensions/matrix/src/plugin-entry.runtime.ts index 1051fddf9978..f5f2fa584bd3 100644 --- a/extensions/matrix/src/plugin-entry.runtime.ts +++ b/extensions/matrix/src/plugin-entry.runtime.ts @@ -1,15 +1,15 @@ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; // Matrix plugin module implements plugin entry behavior. import type { GatewayRequestHandlerOptions } from "openclaw/plugin-sdk/gateway-runtime"; import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; -import { formatMatrixErrorMessage } from "./matrix/errors.js"; const loadMatrixVerificationRuntime = createLazyRuntimeModule( () => import("./matrix/actions/verification.js"), ); function sendError(respond: (ok: boolean, payload?: unknown) => void, err: unknown) { - respond(false, { error: formatMatrixErrorMessage(err) }); + respond(false, { error: formatErrorMessage(err) }); } export async function handleVerifyRecoveryKey({ diff --git a/extensions/matrix/src/setup-bootstrap.ts b/extensions/matrix/src/setup-bootstrap.ts index 6de67d30f5cd..15f27903b8b5 100644 --- a/extensions/matrix/src/setup-bootstrap.ts +++ b/extensions/matrix/src/setup-bootstrap.ts @@ -1,8 +1,8 @@ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; // Matrix plugin module implements setup bootstrap behavior. import { hasExplicitMatrixAccountConfig } from "./matrix/account-config.js"; import { resolveMatrixAccountConfig } from "./matrix/accounts.js"; import { bootstrapMatrixVerification } from "./matrix/actions/verification.js"; -import { formatMatrixErrorMessage } from "./matrix/errors.js"; import type { RuntimeEnv } from "./runtime-api.js"; import type { CoreConfig } from "./types.js"; @@ -61,7 +61,7 @@ export async function maybeBootstrapNewEncryptedMatrixAccount(params: { success: false, recoveryKeyCreatedAt: null, backupVersion: null, - error: formatMatrixErrorMessage(err), + error: formatErrorMessage(err), }; } } diff --git a/extensions/memory-lancedb/embeddings.ts b/extensions/memory-lancedb/embeddings.ts index de0513139a01..b9b5be6f5c4f 100644 --- a/extensions/memory-lancedb/embeddings.ts +++ b/extensions/memory-lancedb/embeddings.ts @@ -1,7 +1,7 @@ import { Buffer } from "node:buffer"; import type { AgentToolResult } from "openclaw/plugin-sdk/agent-core"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -import { toErrorObject } from "openclaw/plugin-sdk/error-runtime"; +import { formatErrorMessage, toErrorObject } from "openclaw/plugin-sdk/error-runtime"; import { resolveGlobalSingleton } from "openclaw/plugin-sdk/global-singleton"; import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; import { canonicalizeBase64 } from "openclaw/plugin-sdk/media-runtime"; @@ -383,10 +383,6 @@ export async function runWithTimeout(params: { } } -export function formatMemoryRecallError(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - export function isMemoryRecallTimeoutError(error: unknown): boolean { let current: unknown = error; for (let depth = 0; depth < 3 && current !== undefined; depth += 1) { @@ -433,7 +429,7 @@ export function buildMemoryRecallUnavailableResult(error: string): AgentToolResu export class MemoryRecallEmbeddingError extends Error { constructor(readonly originalError: unknown) { - super(formatMemoryRecallError(originalError)); + super(formatErrorMessage(originalError)); this.name = "MemoryRecallEmbeddingError"; } } diff --git a/extensions/memory-lancedb/index.ts b/extensions/memory-lancedb/index.ts index 6538d02f3c9f..ec4d62f7d6df 100644 --- a/extensions/memory-lancedb/index.ts +++ b/extensions/memory-lancedb/index.ts @@ -7,6 +7,7 @@ import { optionalPositiveIntegerSchema, } from "openclaw/plugin-sdk/channel-actions"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; import { readFiniteNumberParam, readPositiveIntegerParam } from "openclaw/plugin-sdk/param-readers"; import { resolveLivePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime"; @@ -24,7 +25,6 @@ import { import { buildMemoryRecallUnavailableResult, createEmbeddings, - formatMemoryRecallError, isMemoryRecallTimeoutError, MemoryRecallEmbeddingError, runWithTimeout, @@ -250,7 +250,7 @@ export default definePluginEntry({ if (!(error instanceof MemoryRecallEmbeddingError)) { throw error; } - const message = formatMemoryRecallError(error.originalError); + const message = formatErrorMessage(error.originalError); if (isMemoryRecallTimeoutError(error.originalError)) { recordMemoryRecallCooldown(agentId, message); } @@ -587,7 +587,7 @@ export default definePluginEntry({ err instanceof MemoryRecallEmbeddingError && isMemoryRecallTimeoutError(err.originalError) ) { - recordMemoryRecallCooldown(agentId, formatMemoryRecallError(err.originalError)); + recordMemoryRecallCooldown(agentId, formatErrorMessage(err.originalError)); } api.logger.warn(`memory-lancedb: recall failed: ${String(err)}`); } diff --git a/extensions/msteams/src/msteams-ingress.ts b/extensions/msteams/src/msteams-ingress.ts index 875fc403168c..b44777bcc608 100644 --- a/extensions/msteams/src/msteams-ingress.ts +++ b/extensions/msteams/src/msteams-ingress.ts @@ -6,6 +6,7 @@ import { type ChannelIngressMonitorDeliveryResult, type ChannelIngressMonitorLifecycle, } from "openclaw/plugin-sdk/channel-outbound"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { normalizeNullableString as nonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { classifyMSTeamsSendError } from "./errors.js"; @@ -132,10 +133,6 @@ function parseClaimedActivity( return parsed; } -function errorText(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - export function createMSTeamsIngress(options: MSTeamsIngressOptions): MSTeamsIngress { const queue = options.queue ?? @@ -193,14 +190,14 @@ export function createMSTeamsIngress(options: MSTeamsIngressOptions): MSTeamsIng } const classification = classifyMSTeamsSendError(error); return classification.kind === "auth" - ? { reason: "authentication-failed", message: errorText(error) } + ? { reason: "authentication-failed", message: formatErrorMessage(error) } : null; }, onLog: (message) => options.runtime.error?.(`msteams: ${message}`), }, createStoppedError: () => new Error("Microsoft Teams ingress stopped."), onError: (error) => - options.runtime.error?.(`msteams ingress drain failed: ${errorText(error)}`), + options.runtime.error?.(`msteams ingress drain failed: ${formatErrorMessage(error)}`), }); let stopTask: Promise | undefined; diff --git a/extensions/mxc/src/sandbox-policy-loader.ts b/extensions/mxc/src/sandbox-policy-loader.ts index 36cda6a40514..9f03e69136e2 100644 --- a/extensions/mxc/src/sandbox-policy-loader.ts +++ b/extensions/mxc/src/sandbox-policy-loader.ts @@ -1,5 +1,6 @@ import { readFileSync, statSync } from "node:fs"; import { win32 } from "node:path"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { z } from "zod"; import { DEFAULT_SANDBOX_BASELINE, @@ -278,7 +279,7 @@ function assertConfiguredPathExists(pathValue: string, source: string): void { ); } throw new Error( - `Sandbox policy path ${pathValue} configured by ${source} is not accessible on the host: ${formatError(err)}`, + `Sandbox policy path ${pathValue} configured by ${source} is not accessible on the host: ${formatErrorMessage(err)}`, { cause: err }, ); } @@ -306,9 +307,12 @@ function policyFileError(policyPath: string, err: unknown): Error { { cause: err }, ); } - return new Error(`Failed to load sandbox policy file at ${policyPath}: ${formatError(err)}`, { - cause: err instanceof Error ? err : undefined, - }); + return new Error( + `Failed to load sandbox policy file at ${policyPath}: ${formatErrorMessage(err)}`, + { + cause: err instanceof Error ? err : undefined, + }, + ); } function formatSandboxPolicyIssue(sourceLabel: string, issue: z.ZodIssue | undefined): string { @@ -347,13 +351,6 @@ function formatIssuePath(pathSegments: readonly PropertyKey[]): string { return label; } -function formatError(err: unknown): string { - if (err instanceof Error && err.message) { - return err.message; - } - return String(err); -} - function isNodeError(err: unknown): err is NodeJS.ErrnoException { return err instanceof Error && "code" in err; } diff --git a/extensions/nextcloud-talk/src/monitor.ts b/extensions/nextcloud-talk/src/monitor.ts index 0893a04938fa..0e7abc7193e5 100644 --- a/extensions/nextcloud-talk/src/monitor.ts +++ b/extensions/nextcloud-talk/src/monitor.ts @@ -1,5 +1,6 @@ // Nextcloud Talk plugin module implements monitor behavior. import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { WEBHOOK_RATE_LIMIT_DEFAULTS, createAuthRateLimiter, @@ -27,13 +28,6 @@ const WEBHOOK_ERRORS = { internalServerError: "Internal server error", } as const; -function formatError(err: unknown): string { - if (err instanceof Error) { - return err.message; - } - return typeof err === "string" ? err : JSON.stringify(err); -} - function writeJsonResponse( res: ServerResponse, status: number, @@ -206,7 +200,7 @@ export function createNextcloudTalkWebhookServer(opts: NextcloudTalkWebhookServe writeWebhookError(res, 400, WEBHOOK_ERRORS.invalidPayloadFormat); return; } - const error = err instanceof Error ? err : new Error(formatError(err)); + const error = err instanceof Error ? err : new Error(formatErrorMessage(err)); onError?.(error); writeWebhookError(res, 500, WEBHOOK_ERRORS.internalServerError); } diff --git a/extensions/qa-lab/web/src/app.ts b/extensions/qa-lab/web/src/app.ts index 0ff300627144..56cc8c7ac539 100644 --- a/extensions/qa-lab/web/src/app.ts +++ b/extensions/qa-lab/web/src/app.ts @@ -1,7 +1,7 @@ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; // Qa Lab plugin module implements app behavior. import { defaultQaModelForMode, isQaFastModeEnabled } from "../../model-selection.js"; import { normalizeCaptureSavedView, normalizeCaptureSavedViews } from "./capture-saved-view.js"; -import { formatErrorMessage } from "./errors.js"; import { getJson, getJsonNoStore, postJson, QaLabHttpError } from "./http.js"; import { conversationSelectionKey, findConversationBySelectionKey } from "./ui-conversation-key.js"; import { diff --git a/extensions/qa-lab/web/src/errors.ts b/extensions/qa-lab/web/src/errors.ts deleted file mode 100644 index 448c46f811f5..000000000000 --- a/extensions/qa-lab/web/src/errors.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Qa Lab plugin module implements errors behavior. -export function formatErrorMessage(err: unknown): string { - if (err instanceof Error) { - let formatted = err.message || err.name || "Error"; - let cause: unknown = err.cause; - const seen = new Set([err]); - while (cause && !seen.has(cause)) { - seen.add(cause); - if (cause instanceof Error) { - if (cause.message) { - formatted += ` | ${cause.message}`; - } - cause = cause.cause; - continue; - } - if (typeof cause === "string") { - formatted += ` | ${cause}`; - } - break; - } - return formatted; - } - if (typeof err === "string") { - return err; - } - if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") { - return String(err); - } - try { - return JSON.stringify(err); - } catch { - return Object.prototype.toString.call(err); - } -} diff --git a/extensions/qqbot/src/engine/api/api-client.ts b/extensions/qqbot/src/engine/api/api-client.ts index 45af7ea5d294..2b363db36113 100644 --- a/extensions/qqbot/src/engine/api/api-client.ts +++ b/extensions/qqbot/src/engine/api/api-client.ts @@ -9,6 +9,7 @@ * - `redactBodyKeys` replaces the hardcoded `file_data` redaction. */ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { readProviderTextResponse, readResponseTextLimited, @@ -16,7 +17,6 @@ import { import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { ApiError, type ApiClientConfig, type EngineLogger } from "../types.js"; -import { formatErrorMessage } from "../utils/format.js"; const DEFAULT_BASE_URL = "https://api.sgroup.qq.com"; const DEFAULT_TIMEOUT_MS = 30_000; diff --git a/extensions/qqbot/src/engine/api/messages.ts b/extensions/qqbot/src/engine/api/messages.ts index 412b7f0e9111..d481b054b38d 100644 --- a/extensions/qqbot/src/engine/api/messages.ts +++ b/extensions/qqbot/src/engine/api/messages.ts @@ -7,6 +7,7 @@ * - Markdown support flag is per-instance, not a global Map. */ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import type { ChatScope, MessageResponse, @@ -15,7 +16,6 @@ import type { InlineKeyboard, StreamMessageRequest, } from "../types.js"; -import { formatErrorMessage } from "../utils/format.js"; import { ApiClient } from "./api-client.js"; import { messagePath, diff --git a/extensions/qqbot/src/engine/api/retry.ts b/extensions/qqbot/src/engine/api/retry.ts index 39c419b86123..01d6f2feb7ba 100644 --- a/extensions/qqbot/src/engine/api/retry.ts +++ b/extensions/qqbot/src/engine/api/retry.ts @@ -10,11 +10,11 @@ * parameterized by `RetryPolicy` and optional `PersistentRetryPolicy`. */ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { createChannelApiRetryRunner, resolveRetryConfig } from "openclaw/plugin-sdk/retry-runtime"; import { sleep } from "openclaw/plugin-sdk/runtime-env"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import type { EngineLogger } from "../types.js"; -import { formatErrorMessage } from "../utils/format.js"; /** Standard retry policy with exponential or fixed backoff. */ interface RetryPolicy { diff --git a/extensions/qqbot/src/engine/api/token.ts b/extensions/qqbot/src/engine/api/token.ts index 8fd23d765cd1..79c7e25dd406 100644 --- a/extensions/qqbot/src/engine/api/token.ts +++ b/extensions/qqbot/src/engine/api/token.ts @@ -6,6 +6,7 @@ * globals, fully supporting multi-account concurrent operation. */ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { asDateTimestampMs, parseStrictPositiveInteger, @@ -16,7 +17,6 @@ import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http"; import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env"; import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime"; import type { EngineLogger } from "../types.js"; -import { formatErrorMessage } from "../utils/format.js"; const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken"; const DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 7200; diff --git a/extensions/qqbot/src/engine/gateway/message-queue.ts b/extensions/qqbot/src/engine/gateway/message-queue.ts index c4c5df15f3c6..adaa26502c27 100644 --- a/extensions/qqbot/src/engine/gateway/message-queue.ts +++ b/extensions/qqbot/src/engine/gateway/message-queue.ts @@ -1,7 +1,7 @@ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; // Qqbot plugin module implements message queue behavior. import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; -import { formatErrorMessage } from "../utils/format.js"; import { isQQBotAuthenticationFailure } from "./ingress-errors.js"; import { buildQQBotMergedIngressLifecycle } from "./message-queue-ingress.js"; import type { QQBotIngressLifecycle } from "./types.js"; diff --git a/extensions/qqbot/src/engine/gateway/typing-keepalive.ts b/extensions/qqbot/src/engine/gateway/typing-keepalive.ts index 112bd0aefdec..919becd6fb96 100644 --- a/extensions/qqbot/src/engine/gateway/typing-keepalive.ts +++ b/extensions/qqbot/src/engine/gateway/typing-keepalive.ts @@ -6,9 +6,9 @@ */ import { createTypingKeepaliveLoop } from "openclaw/plugin-sdk/channel-outbound"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { claimMessageReply } from "../messaging/outbound-reply.js"; import type { ReplyLimitResult } from "../messaging/reply-limiter.js"; -import { formatErrorMessage } from "../utils/format.js"; /** Function that sends a typing indicator to one user. */ type SendInputNotifyFn = ( diff --git a/extensions/qqbot/src/engine/messaging/outbound-deliver.ts b/extensions/qqbot/src/engine/messaging/outbound-deliver.ts index affdcb83f6ed..2f861932495d 100644 --- a/extensions/qqbot/src/engine/messaging/outbound-deliver.ts +++ b/extensions/qqbot/src/engine/messaging/outbound-deliver.ts @@ -6,13 +6,13 @@ * `DeliverDeps.mediaSender`. */ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { sendPayloadMediaSequence, sendPayloadTextChunkSequence, } from "openclaw/plugin-sdk/reply-payload"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import type { GatewayAccount } from "../types.js"; -import { formatErrorMessage } from "../utils/format.js"; import { getImageSize, formatQQBotMarkdownImage, hasQQBotImageSize } from "../utils/image-size.js"; import { normalizeMediaTags } from "../utils/media-tags.js"; import { isLocalPath as isLocalFilePath } from "../utils/platform.js"; diff --git a/extensions/qqbot/src/engine/messaging/outbound-media-send.ts b/extensions/qqbot/src/engine/messaging/outbound-media-send.ts index d1b673fdc9f8..658910a79ca0 100644 --- a/extensions/qqbot/src/engine/messaging/outbound-media-send.ts +++ b/extensions/qqbot/src/engine/messaging/outbound-media-send.ts @@ -5,6 +5,7 @@ import { randomUUID } from "node:crypto"; import { mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { extensionForMime, type MediaKind } from "openclaw/plugin-sdk/media-mime"; import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media"; import { @@ -23,7 +24,6 @@ import { getMaxUploadSize, readFileAsync, } from "../utils/file-utils.js"; -import { formatErrorMessage } from "../utils/format.js"; import { debugError, debugLog, debugWarn } from "../utils/log.js"; import { getQQBotDataDir, diff --git a/extensions/qqbot/src/engine/messaging/outbound.ts b/extensions/qqbot/src/engine/messaging/outbound.ts index 352c2e44761b..5ac2a16a76f1 100644 --- a/extensions/qqbot/src/engine/messaging/outbound.ts +++ b/extensions/qqbot/src/engine/messaging/outbound.ts @@ -33,10 +33,10 @@ export { sendVoice, } from "./outbound-media-send.js"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import type { GatewayAccount } from "../types.js"; import type { EngineLogger } from "../types.js"; -import { formatErrorMessage } from "../utils/format.js"; import { debugError, debugLog, debugWarn } from "../utils/log.js"; import { normalizeMediaTags } from "../utils/media-tags.js"; import { decodeCronPayload } from "../utils/payload.js"; diff --git a/extensions/qqbot/src/engine/messaging/reply-dispatcher.ts b/extensions/qqbot/src/engine/messaging/reply-dispatcher.ts index 70e5a5ce940d..fe6bd54953d9 100644 --- a/extensions/qqbot/src/engine/messaging/reply-dispatcher.ts +++ b/extensions/qqbot/src/engine/messaging/reply-dispatcher.ts @@ -7,11 +7,11 @@ import crypto from "node:crypto"; import path from "node:path"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { resolveLocalPathFromRootsSync } from "openclaw/plugin-sdk/security-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { MediaFileType, type GatewayAccount } from "../types.js"; import { formatFileSize, getImageMimeType, getMaxUploadSize } from "../utils/file-utils.js"; -import { formatErrorMessage } from "../utils/format.js"; import { parseQQBotPayload, encodePayloadForCron, diff --git a/extensions/qqbot/src/engine/messaging/sender.ts b/extensions/qqbot/src/engine/messaging/sender.ts index 6daa1af6dbfb..f5159921aa1e 100644 --- a/extensions/qqbot/src/engine/messaging/sender.ts +++ b/extensions/qqbot/src/engine/messaging/sender.ts @@ -25,6 +25,7 @@ */ import os from "node:os"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { ApiClient } from "../api/api-client.js"; import { ChunkedMediaApi as ChunkedMediaApiClass } from "../api/media-chunked.js"; @@ -43,7 +44,6 @@ import { type UploadMediaResponse, } from "../types.js"; import { getMaxUploadSize, LARGE_FILE_THRESHOLD } from "../utils/file-utils.js"; -import { formatErrorMessage } from "../utils/format.js"; import { debugLog, debugError, debugWarn } from "../utils/log.js"; import { sanitizeFileName } from "../utils/string-normalize.js"; import { computeFileHash, getCachedFileInfo, setCachedFileInfo } from "../utils/upload-cache.js"; diff --git a/extensions/qqbot/src/engine/messaging/streaming-c2c.ts b/extensions/qqbot/src/engine/messaging/streaming-c2c.ts index 06fdc0f4cf08..7260cc6f9fcc 100644 --- a/extensions/qqbot/src/engine/messaging/streaming-c2c.ts +++ b/extensions/qqbot/src/engine/messaging/streaming-c2c.ts @@ -15,6 +15,7 @@ * it is treated as a new message. */ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { getNextMsgSeq } from "../api/routes.js"; import type { GatewayAccount } from "../types.js"; @@ -37,10 +38,6 @@ import { type MediaSendContext, } from "./streaming-media-send.js"; -function formatStreamErr(e: unknown): string { - return e instanceof Error ? e.message : String(e); -} - // ============ 常量 ============ /** 流式消息节流常量(毫秒) */ @@ -485,7 +482,7 @@ export class StreamingController { () => this.handlePartialReply(payload), (err: unknown) => { // 上一次如果异常,不阻塞后续调用 - this.logError(`onPartialReply chain error: ${formatStreamErr(err)}`); + this.logError(`onPartialReply chain error: ${formatErrorMessage(err)}`); return this.handlePartialReply(payload); }, ); @@ -590,7 +587,7 @@ export class StreamingController { this.callbackChain = this.callbackChain.then( () => this.handleIdle(payload), (err: unknown) => { - this.logError(`onIdle chain error: ${formatStreamErr(err)}`); + this.logError(`onIdle chain error: ${formatErrorMessage(err)}`); return this.handleIdle(payload); }, ); @@ -674,7 +671,7 @@ export class StreamingController { await this.sendStreamChunk(safeText, StreamInputState.DONE, "onIdle"); this.logInfo(`streaming completed, final text length: ${safeText.length}`); } catch (err) { - this.logError(`failed to send final stream chunk: ${formatStreamErr(err)}`); + this.logError(`failed to send final stream chunk: ${formatErrorMessage(err)}`); } } else if (this.sentStreamChunkCount > 0) { // 没有活跃流式会话,但之前发过流式分片或媒体 → 正常完成 @@ -693,7 +690,7 @@ export class StreamingController { * 处理错误 */ async onError(err: unknown): Promise { - this.logError(`reply error: ${formatStreamErr(err)}`); + this.logError(`reply error: ${formatErrorMessage(err)}`); if (this.isTerminalPhase) { return; @@ -726,7 +723,7 @@ export class StreamingController { : "**Error**: 生成响应时发生错误。"; await this.sendStreamChunk(errorText, StreamInputState.DONE, "onError"); } catch (sendErr) { - this.logError(`failed to send error stream chunk: ${formatStreamErr(sendErr)}`); + this.logError(`failed to send error stream chunk: ${formatErrorMessage(sendErr)}`); } } @@ -759,7 +756,7 @@ export class StreamingController { await this.sendStreamChunk(abortText, StreamInputState.DONE, "abortStreaming"); this.logInfo(`streaming aborted, sent final chunk`); } catch (err) { - this.logError(`abort send failed: ${formatStreamErr(err)}`); + this.logError(`abort send failed: ${formatErrorMessage(err)}`); } } } @@ -871,7 +868,7 @@ export class StreamingController { } await this.flush.throttledUpdate(this.throttleMs); } catch (err) { - this.logError(`processMediaTags failed: ${formatStreamErr(err)}`); + this.logError(`processMediaTags failed: ${formatErrorMessage(err)}`); } } @@ -907,7 +904,7 @@ export class StreamingController { await this.sendStreamChunk(safeText, StreamInputState.DONE, caller); this.logDebug(`${caller}: current stream session ended`); } catch (err) { - this.logError(`${caller}: failed to end stream: ${formatStreamErr(err)}`); + this.logError(`${caller}: failed to end stream: ${formatErrorMessage(err)}`); } } else if (safeText && safeText.trim()) { // 没有活跃流式会话,但有非空白文本未发送 → 启动流式 → 立即终结 @@ -926,7 +923,7 @@ export class StreamingController { await this.sendStreamChunk(safeText, StreamInputState.DONE, caller); this.logDebug(`${caller}: started and ended stream for pre-tag text`); } catch (err) { - this.logError(`${caller}: failed to send pre-tag text: ${formatStreamErr(err)}`); + this.logError(`${caller}: failed to send pre-tag text: ${formatErrorMessage(err)}`); } } } @@ -1023,7 +1020,7 @@ export class StreamingController { this.flush.setReady(true); this.logInfo(`stream started, stream_msg_id=${resp.id}`); } catch (err) { - this.logError(`failed to start streaming: ${formatStreamErr(err)}`); + this.logError(`failed to start streaming: ${formatErrorMessage(err)}`); this.transition("idle", "doStartStreaming", "start_failed_will_retry"); } } diff --git a/extensions/qqbot/src/engine/messaging/streaming-media-send.ts b/extensions/qqbot/src/engine/messaging/streaming-media-send.ts index 10a8707b6534..05808fea369f 100644 --- a/extensions/qqbot/src/engine/messaging/streaming-media-send.ts +++ b/extensions/qqbot/src/engine/messaging/streaming-media-send.ts @@ -5,6 +5,7 @@ * 拆分、路径编码修复,以及统一的发送队列执行器。 */ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import type { GatewayAccount } from "../types.js"; import { normalizePath } from "../utils/platform.js"; @@ -21,10 +22,6 @@ import { } from "./outbound.js"; import { raceWithTimeout } from "./race-with-timeout.js"; -function formatStreamSendErr(e: unknown): string { - return e instanceof Error ? e.message : String(e); -} - // ============ 类型定义 ============ /** 发送队列项 */ @@ -119,7 +116,7 @@ function fixPathEncoding( } } } catch (decodeErr) { - log?.error?.(`Path decode error: ${formatStreamSendErr(decodeErr)}`); + log?.error?.(`Path decode error: ${formatErrorMessage(decodeErr)}`); } return result; @@ -282,7 +279,7 @@ export async function executeSendQueue( await options.onSendText(errorMsg); } catch (fallbackErr) { log?.error( - `${prefix} executeSendQueue: fallback text send failed: ${formatStreamSendErr(fallbackErr)}`, + `${prefix} executeSendQueue: fallback text send failed: ${formatErrorMessage(fallbackErr)}`, ); } }; @@ -329,7 +326,7 @@ export async function executeSendQueue( await sendFallbackText(resolveUserFacingMediaError(result)); } } catch (err) { - log?.error(`${prefix} sendVoice unexpected error: ${formatStreamSendErr(err)}`); + log?.error(`${prefix} sendVoice unexpected error: ${formatErrorMessage(err)}`); await sendFallbackText(DEFAULT_MEDIA_SEND_ERROR); } } else if (item.type === "video") { @@ -363,7 +360,7 @@ export async function executeSendQueue( } } catch (err) { log?.error( - `${prefix} executeSendQueue: failed to send ${item.type}: ${formatStreamSendErr(err)}`, + `${prefix} executeSendQueue: failed to send ${item.type}: ${formatErrorMessage(err)}`, ); await sendFallbackText(DEFAULT_MEDIA_SEND_ERROR); } diff --git a/extensions/qqbot/src/engine/ref/store.ts b/extensions/qqbot/src/engine/ref/store.ts index 5052f0316727..473b7dde960b 100644 --- a/extensions/qqbot/src/engine/ref/store.ts +++ b/extensions/qqbot/src/engine/ref/store.ts @@ -2,7 +2,7 @@ * Ref-index store — SQLite KV-backed store for message reference index. */ -import { formatErrorMessage } from "../utils/format.js"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { debugError } from "../utils/log.js"; import { buildQQBotStateKey, openQQBotSyncKeyedStore } from "../utils/sqlite-state.js"; import type { RefAttachmentSummary, RefIndexEntry } from "./types.js"; diff --git a/extensions/qqbot/src/engine/session/known-users.ts b/extensions/qqbot/src/engine/session/known-users.ts index 49cac62d3faa..063af907df9d 100644 --- a/extensions/qqbot/src/engine/session/known-users.ts +++ b/extensions/qqbot/src/engine/session/known-users.ts @@ -3,8 +3,8 @@ */ import crypto from "node:crypto"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import type { ChatScope } from "../types.js"; -import { formatErrorMessage } from "../utils/format.js"; import { debugLog, debugError } from "../utils/log.js"; import { openQQBotSyncKeyedStore } from "../utils/sqlite-state.js"; diff --git a/extensions/qqbot/src/engine/session/session-store.ts b/extensions/qqbot/src/engine/session/session-store.ts index e87607532fe4..13c7e6650f59 100644 --- a/extensions/qqbot/src/engine/session/session-store.ts +++ b/extensions/qqbot/src/engine/session/session-store.ts @@ -2,7 +2,7 @@ * Gateway session persistence — SQLite KV-backed store. */ -import { formatErrorMessage } from "../utils/format.js"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { debugLog, debugError } from "../utils/log.js"; import { buildQQBotStateKey, openQQBotSyncKeyedStore } from "../utils/sqlite-state.js"; diff --git a/extensions/qqbot/src/engine/tools/channel-api.ts b/extensions/qqbot/src/engine/tools/channel-api.ts index 9d6683ef6938..2a535a081146 100644 --- a/extensions/qqbot/src/engine/tools/channel-api.ts +++ b/extensions/qqbot/src/engine/tools/channel-api.ts @@ -10,13 +10,13 @@ import { resolveChannelGroupPolicy } from "openclaw/plugin-sdk/channel-policy"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { readProviderTextResponse, readResponseTextLimited, } from "openclaw/plugin-sdk/provider-http"; import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime"; import { jsonResult as json } from "openclaw/plugin-sdk/tool-results"; -import { formatErrorMessage } from "../utils/format.js"; import { debugLog, debugError } from "../utils/log.js"; const API_BASE = "https://api.sgroup.qq.com"; diff --git a/extensions/qqbot/src/engine/tools/remind-logic.ts b/extensions/qqbot/src/engine/tools/remind-logic.ts index 827696164c76..47e75af53319 100644 --- a/extensions/qqbot/src/engine/tools/remind-logic.ts +++ b/extensions/qqbot/src/engine/tools/remind-logic.ts @@ -1,3 +1,4 @@ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; // Qqbot plugin module implements remind logic behavior. import { resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; @@ -270,10 +271,6 @@ function formatDelay(ms: number): string { return `${hours}h${minutes}m`; } -function formatSchedulerError(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function prepareRemindCronAction( params: RemindParams, ctx: RemindExecuteContext = {}, @@ -363,7 +360,7 @@ export async function executeScheduledRemind( }); } catch (error) { return json({ - error: `Failed to run Gateway cron action: ${formatSchedulerError(error)}`, + error: `Failed to run Gateway cron action: ${formatErrorMessage(error)}`, action: plan.action, }); } diff --git a/extensions/qqbot/src/engine/utils/audio.ts b/extensions/qqbot/src/engine/utils/audio.ts index 14465ea3a2ce..d627cfbfb6e1 100644 --- a/extensions/qqbot/src/engine/utils/audio.ts +++ b/extensions/qqbot/src/engine/utils/audio.ts @@ -11,9 +11,9 @@ import * as fs from "node:fs"; import * as path from "node:path"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { readRegularFileSync } from "openclaw/plugin-sdk/security-runtime"; -import { formatErrorMessage } from "./format.js"; import { debugLog, debugError, debugWarn } from "./log.js"; import { normalizeLowercaseStringOrEmpty as normalizeLowercase } from "./string-normalize.js"; diff --git a/extensions/qqbot/src/engine/utils/file-utils.ts b/extensions/qqbot/src/engine/utils/file-utils.ts index 2aacca30a79a..911ccb61264b 100644 --- a/extensions/qqbot/src/engine/utils/file-utils.ts +++ b/extensions/qqbot/src/engine/utils/file-utils.ts @@ -2,6 +2,7 @@ import crypto from "node:crypto"; import * as fs from "node:fs"; import * as path from "node:path"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime"; import { formatByteSize } from "openclaw/plugin-sdk/number-runtime"; import { @@ -12,7 +13,6 @@ import { import { getPlatformAdapter } from "../adapter/index.js"; import type { SsrfPolicyConfig } from "../adapter/types.js"; import { MediaFileType } from "../types.js"; -import { formatErrorMessage } from "./format.js"; import { normalizeOptionalString } from "./string-normalize.js"; /** Maximum file size accepted by the QQ Bot one-shot upload API (base64 direct). */ diff --git a/extensions/qqbot/src/engine/utils/format.test.ts b/extensions/qqbot/src/engine/utils/format.test.ts index df221ce2a270..e78e15694b2b 100644 --- a/extensions/qqbot/src/engine/utils/format.test.ts +++ b/extensions/qqbot/src/engine/utils/format.test.ts @@ -1,6 +1,7 @@ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; // Qqbot tests cover format plugin behavior. import { describe, expect, it } from "vitest"; -import { formatErrorMessage, formatDuration } from "./format.js"; +import { formatDuration } from "./format.js"; describe("engine/utils/format", () => { describe("formatErrorMessage", () => { @@ -36,7 +37,7 @@ describe("engine/utils/format", () => { }); it("JSON-stringifies plain objects", () => { - expect(formatErrorMessage({ code: 500 })).toBe('{"code":500}'); + expect(formatErrorMessage({ code: 500 })).toBe("status=unknown code=500"); }); }); diff --git a/extensions/qqbot/src/engine/utils/format.ts b/extensions/qqbot/src/engine/utils/format.ts index 500e559b7265..0abc63f63dad 100644 --- a/extensions/qqbot/src/engine/utils/format.ts +++ b/extensions/qqbot/src/engine/utils/format.ts @@ -3,60 +3,9 @@ * 通用格式化与字符串工具。 * * Pure utility functions, with duration presentation from the plugin-local dependency. - * - * NOTE: The framework `formatErrorMessage` also applies `redactSensitiveText()` - * for token masking. We intentionally omit that here — the framework's log - * pipeline handles redaction at a higher level. */ import prettyMilliseconds from "pretty-ms"; -/** - * Format any error object into a readable string. - * 将任意错误对象格式化为可读字符串。 - * - * Traverses the `.cause` chain for nested Error objects to include - * the full error context (e.g. network errors wrapped inside HTTP errors). - */ -export function formatErrorMessage(err: unknown): string { - if (err instanceof Error) { - let formatted = err.message || err.name || "Error"; - let cause: unknown = err.cause; - const seen = new Set([err]); - while (cause && !seen.has(cause)) { - seen.add(cause); - if (cause instanceof Error) { - if (cause.message) { - formatted += ` | ${cause.message}`; - } - cause = cause.cause; - } else if (typeof cause === "string") { - formatted += ` | ${cause}`; - break; - } else { - break; - } - } - return formatted; - } - if (typeof err === "string") { - return err; - } - if ( - err === null || - err === undefined || - typeof err === "number" || - typeof err === "boolean" || - typeof err === "bigint" - ) { - return String(err); - } - try { - return JSON.stringify(err); - } catch { - return Object.prototype.toString.call(err); - } -} - /** Format a millisecond duration into a human-readable string (e.g. "5m 30s"). */ export function formatDuration(durationMs: number): string { if (durationMs <= 0) { diff --git a/extensions/qqbot/src/engine/utils/image-size.ts b/extensions/qqbot/src/engine/utils/image-size.ts index af0f3de7392d..73f652f7be9b 100644 --- a/extensions/qqbot/src/engine/utils/image-size.ts +++ b/extensions/qqbot/src/engine/utils/image-size.ts @@ -5,10 +5,10 @@ */ import { Buffer } from "node:buffer"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { getPlatformAdapter } from "../adapter/index.js"; import type { SsrfPolicyConfig } from "../adapter/types.js"; -import { formatErrorMessage } from "./format.js"; import { debugLog } from "./log.js"; interface ImageSize { diff --git a/extensions/qqbot/src/engine/utils/payload.ts b/extensions/qqbot/src/engine/utils/payload.ts index 334ec181a400..0b8aadbc726e 100644 --- a/extensions/qqbot/src/engine/utils/payload.ts +++ b/extensions/qqbot/src/engine/utils/payload.ts @@ -7,6 +7,7 @@ * Zero external dependencies. */ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import type { ChatScope } from "../types.js"; /** Structured reminder payload emitted by the model. */ @@ -40,10 +41,6 @@ interface ParseResult { const PAYLOAD_PREFIX = "QQBOT_PAYLOAD:"; const CRON_PREFIX = "QQBOT_CRON:"; -function formatErr(e: unknown): string { - return e instanceof Error ? e.message : String(e); -} - function normalizeBase64ForCompare(value: string): string { return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/"); } @@ -96,7 +93,7 @@ export function parseQQBotPayload(text: string): ParseResult { return { isPayload: true, payload }; } catch (e) { - return { isPayload: true, error: `Failed to parse JSON: ${formatErr(e)}` }; + return { isPayload: true, error: `Failed to parse JSON: ${formatErrorMessage(e)}` }; } } @@ -142,7 +139,10 @@ export function decodeCronPayload(message: string): { return { isCronPayload: true, payload }; } catch (e) { - return { isCronPayload: true, error: `Failed to decode cron payload: ${formatErr(e)}` }; + return { + isCronPayload: true, + error: `Failed to decode cron payload: ${formatErrorMessage(e)}`, + }; } } diff --git a/extensions/qqbot/src/engine/utils/platform.ts b/extensions/qqbot/src/engine/utils/platform.ts index ae2b45082363..dc85d7676ce8 100644 --- a/extensions/qqbot/src/engine/utils/platform.ts +++ b/extensions/qqbot/src/engine/utils/platform.ts @@ -9,8 +9,8 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { getPlatformAdapter } from "../adapter/index.js"; -import { formatErrorMessage } from "./format.js"; import { debugLog, debugWarn } from "./log.js"; /** diff --git a/extensions/signal/src/client-adapter.ts b/extensions/signal/src/client-adapter.ts index 04019417460e..05c3b9b7be0a 100644 --- a/extensions/signal/src/client-adapter.ts +++ b/extensions/signal/src/client-adapter.ts @@ -6,6 +6,7 @@ * only need to change their import path. */ +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import type { SignalTransportConfig } from "./account-types.js"; import { containerCheck, containerRpcRequest, streamContainerEvents } from "./client-container.js"; import type { SignalRpcOptions } from "./client.js"; @@ -24,10 +25,6 @@ export type SignalSseEvent = { export type SignalTransportKind = SignalTransportConfig["kind"]; -function formatErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function usesContainer(kind: SignalTransportKind | undefined): boolean { return kind === "container"; } diff --git a/extensions/telegram/src/polling-transport-state.ts b/extensions/telegram/src/polling-transport-state.ts index f97b76739e27..36e8d1be7c2f 100644 --- a/extensions/telegram/src/polling-transport-state.ts +++ b/extensions/telegram/src/polling-transport-state.ts @@ -1,4 +1,5 @@ // Telegram plugin module implements polling transport state behavior. +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import type { TelegramTransport } from "./fetch.js"; type TelegramPollingTransportStateOpts = { @@ -60,7 +61,7 @@ export class TelegramPollingTransportState { await transport.close(); } catch (err) { this.opts.log( - `[telegram][diag] failed to close transport during dispose: ${formatCloseError(err)}`, + `[telegram][diag] failed to close transport during dispose: ${formatErrorMessage(err)}`, ); } } @@ -70,15 +71,8 @@ export class TelegramPollingTransportState { #closeTransportAsync(transport: TelegramTransport, context: string) { void transport.close().catch((err: unknown) => { this.opts.log( - `[telegram][diag] failed to close transport (${context}): ${formatCloseError(err)}`, + `[telegram][diag] failed to close transport (${context}): ${formatErrorMessage(err)}`, ); }); } } - -function formatCloseError(err: unknown): string { - if (err instanceof Error) { - return err.message; - } - return String(err); -} diff --git a/extensions/telegram/src/telegram-ingress-worker.runtime.ts b/extensions/telegram/src/telegram-ingress-worker.runtime.ts index b8381defc191..3ab56c513e97 100644 --- a/extensions/telegram/src/telegram-ingress-worker.runtime.ts +++ b/extensions/telegram/src/telegram-ingress-worker.runtime.ts @@ -1,5 +1,6 @@ // Telegram plugin module implements telegram ingress worker behavior. import { parentPort, workerData } from "node:worker_threads"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime"; import { computeBackoff, @@ -70,13 +71,6 @@ type TelegramIngressWorkerRuntimeData = TelegramIngressWorkerOptions & { runtime: typeof TELEGRAM_INGRESS_WORKER_RUNTIME_MARKER; }; -function formatErrorMessage(err: unknown): string { - if (err instanceof Error) { - return err.message || err.name; - } - return String(err); -} - function readTelegramErrorCode(err: unknown): number | undefined { if (err && typeof err === "object" && "error_code" in err) { const code = (err as { error_code: unknown }).error_code; diff --git a/extensions/zalo/src/webhook-spool.ts b/extensions/zalo/src/webhook-spool.ts index 1b0a784461a0..b52ffda32863 100644 --- a/extensions/zalo/src/webhook-spool.ts +++ b/extensions/zalo/src/webhook-spool.ts @@ -8,6 +8,7 @@ import { type ChannelIngressQueue, } from "openclaw/plugin-sdk/channel-outbound"; import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { normalizeNullableString as nonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards"; import { ZaloApiError, type ZaloUpdate } from "./api.js"; @@ -111,10 +112,6 @@ function parseClaimedUpdate(payload: ZaloWebhookSpoolPayload, claimedId: string) return facts.update as unknown as ZaloUpdate; } -function errorText(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function isZaloAuthenticationFailure(error: unknown): boolean { let current: unknown = error; const seen = new Set(); @@ -194,14 +191,15 @@ function createZaloWebhookIngress(options: { return { reason: "invalid-event", message: error.message }; } if (isZaloAuthenticationFailure(error)) { - return { reason: "authentication-failed", message: errorText(error) }; + return { reason: "authentication-failed", message: formatErrorMessage(error) }; } return null; }, onLog: (message) => options.runtime.error?.(`zalo ingress: ${message}`), }, createStoppedError: () => new Error("Zalo ingress stopped."), - onError: (error) => options.runtime.error?.(`zalo ingress drain failed: ${errorText(error)}`), + onError: (error) => + options.runtime.error?.(`zalo ingress drain failed: ${formatErrorMessage(error)}`), }); return { diff --git a/extensions/zalouser/src/ingress.ts b/extensions/zalouser/src/ingress.ts index 1f7d304b4a1a..7b58b101633d 100644 --- a/extensions/zalouser/src/ingress.ts +++ b/extensions/zalouser/src/ingress.ts @@ -7,7 +7,11 @@ import { type ChannelIngressQueue, } from "openclaw/plugin-sdk/channel-outbound"; import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime"; -import { collectErrorGraphCandidates, extractErrorCode } from "openclaw/plugin-sdk/error-runtime"; +import { + collectErrorGraphCandidates, + extractErrorCode, + formatErrorMessage, +} from "openclaw/plugin-sdk/error-runtime"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime"; import { normalizeNullableString as nonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime"; import { getZalouserRuntime } from "./runtime.js"; @@ -118,10 +122,6 @@ function isZalouserAuthenticationFailure(error: unknown): boolean { return false; } -function errorText(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - export function createZalouserIngressMonitor(options: { accountId: string; ownUserId: string; @@ -196,7 +196,7 @@ export function createZalouserIngressMonitor(options: { return { reason: "invalid-event", message: error.message }; } if (isZalouserAuthenticationFailure(error)) { - return { reason: "authentication-failed", message: errorText(error) }; + return { reason: "authentication-failed", message: formatErrorMessage(error) }; } return null; }, @@ -204,7 +204,7 @@ export function createZalouserIngressMonitor(options: { }, createStoppedError: () => new Error("Zalouser ingress monitor is stopped."), onError: (error) => - options.runtime.error?.(`zalouser ingress drain failed: ${errorText(error)}`), + options.runtime.error?.(`zalouser ingress drain failed: ${formatErrorMessage(error)}`), }); monitor.start(); diff --git a/extensions/zalouser/src/zalo-js.ts b/extensions/zalouser/src/zalo-js.ts index 4dac754439fe..24c72cd9ef07 100644 --- a/extensions/zalouser/src/zalo-js.ts +++ b/extensions/zalouser/src/zalo-js.ts @@ -1,5 +1,6 @@ import { randomUUID } from "node:crypto"; import path from "node:path"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; // Zalouser plugin module implements zalo js behavior. import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { extensionForMime } from "openclaw/plugin-sdk/media-mime"; @@ -103,13 +104,6 @@ function normalizeProfile(profile?: string | null): string { return trimmed && trimmed.length > 0 ? trimmed : "default"; } -function toErrorMessage(error: unknown): string { - if (error instanceof Error) { - return error.message; - } - return String(error); -} - function clampTextStyles( text: string, styles?: ZaloSendOptions["textStyles"], @@ -1288,7 +1282,7 @@ export async function sendZaloTextMessage( } catch (error) { return { ok: false, - error: toErrorMessage(error), + error: formatErrorMessage(error), receipt: createZalouserSendReceipt({ threadId: trimmedThreadId, kind: "unknown" }), }; } @@ -1373,7 +1367,7 @@ export async function sendZaloReaction(params: { { shouldPersist: (result) => result.ok }, ); } catch (error) { - return { ok: false, error: toErrorMessage(error) }; + return { ok: false, error: formatErrorMessage(error) }; } } @@ -1451,7 +1445,7 @@ export async function sendZaloLink( } catch (error) { return { ok: false, - error: toErrorMessage(error), + error: formatErrorMessage(error), receipt: createZalouserSendReceipt({ threadId: trimmedThreadId, kind: "card" }), }; } @@ -1593,7 +1587,7 @@ export async function startZaloQrLogin(params: { } catch (error) { const current = activeQrLogins.get(profile); if (current && current.id === login.id) { - current.error = toErrorMessage(error); + current.error = formatErrorMessage(error); } } })(); diff --git a/packages/memory-host-sdk/src/host/embeddings.ts b/packages/memory-host-sdk/src/host/embeddings.ts index 4be291cde561..ddf3842fff87 100644 --- a/packages/memory-host-sdk/src/host/embeddings.ts +++ b/packages/memory-host-sdk/src/host/embeddings.ts @@ -5,6 +5,7 @@ import { DEFAULT_LOCAL_MODEL } from "./embedding-defaults.js"; import { sanitizeAndNormalizeEmbedding } from "./embedding-vectors.js"; import { createLocalEmbeddingWorkerProvider } from "./embeddings-worker.js"; import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.types.js"; +import { formatErrorMessage } from "./error-utils.js"; import { attachLocalEmbeddingRuntimeFacts, type LocalEmbeddingRuntimeFacts, @@ -85,10 +86,6 @@ async function readLlamaRuntimeFacts(llama: Llama): Promise { - finish({ available: false, reason: "binary", error: formatQmdAvailabilityError(err) }); + finish({ available: false, reason: "binary", error: formatErrorMessage(err) }); }); child.once("spawn", () => { didSpawn = true; @@ -156,7 +157,7 @@ function validateQmdProbeCwd(cwd: string): QmdBinaryAvailability | null { return { available: false, reason: "workspace-cwd", - error: `workspace directory unavailable: ${cwd} (${formatQmdAvailabilityError(err)})`, + error: `workspace directory unavailable: ${cwd} (${formatErrorMessage(err)})`, }; } } @@ -503,10 +504,3 @@ function appendOutputWithCap( } return { text: chars.slice(-maxChars).join(""), truncated: true }; } - -function formatQmdAvailabilityError(err: unknown): string { - if (err instanceof Error && err.message) { - return err.message; - } - return String(err); -} diff --git a/src/agents/embedded-agent-runner/runs.ts b/src/agents/embedded-agent-runner/runs.ts index becad34647d4..2fc4c5f7b705 100644 --- a/src/agents/embedded-agent-runner/runs.ts +++ b/src/agents/embedded-agent-runner/runs.ts @@ -29,6 +29,7 @@ import { getAgentEventLifecycleGeneration, isAgentEventLifecycleGenerationCurrent, } from "../../infra/agent-events.js"; +import { formatErrorMessage } from "../../infra/errors.js"; import { getDiagnosticSessionActivitySnapshot, markDiagnosticEmbeddedRunEnded, @@ -343,7 +344,7 @@ export function queueEmbeddedAgentMessageWithOutcome( .queueMessage(text, options ?? { steeringMode: "all" }) .catch((err: unknown) => { diag.debug( - `queue message rejected after enqueue: sessionId=${sessionId} err=${formatQueueError(err)}`, + `queue message rejected after enqueue: sessionId=${sessionId} err=${formatErrorMessage(err)}`, ); }); return { @@ -355,10 +356,6 @@ export function queueEmbeddedAgentMessageWithOutcome( }; } -function formatQueueError(err: unknown): string { - return err instanceof Error ? err.message : String(err); -} - function logActiveRunMessageAccepted(sessionId: string): void { // Active-run steering is consumed by the current turn, not queued as another // turn for the single idle transition to drain. Keep the event and activity. @@ -463,7 +460,7 @@ export async function queueEmbeddedAgentMessageWithOutcomeAsync( enqueuedAtMs, }; } catch (err) { - const errorMessage = formatQueueError(err); + const errorMessage = formatErrorMessage(err); diag.debug(`queue message rejected: sessionId=${sessionId} err=${errorMessage}`); return createQueueFailureOutcome(sessionId, "runtime_rejected", errorMessage); } diff --git a/src/auto-reply/reply/commands-steer.ts b/src/auto-reply/reply/commands-steer.ts index 05fba8edde49..aa3e1fb053f0 100644 --- a/src/auto-reply/reply/commands-steer.ts +++ b/src/auto-reply/reply/commands-steer.ts @@ -6,6 +6,7 @@ import { } from "../../agents/tools/sessions-helpers.js"; import type { SessionEntry } from "../../config/sessions.js"; import { logVerbose } from "../../globals.js"; +import { formatErrorMessage } from "../../infra/errors.js"; import { isNativeCommandTurn, resolveCommandTurnContext } from "../command-turn-context.js"; import { applyCommandTextToParams } from "./command-context-rewrite.js"; import { commandReply, defineAuthorizedTextCommand } from "./command-gates.js"; @@ -92,10 +93,6 @@ function resolveSteerSessionId(params: { return undefined; } -function formatSteerError(err: unknown): string { - return err instanceof Error ? err.message : String(err); -} - function continueWithSteerFallback( params: HandleCommandsParams, message: string, @@ -143,7 +140,7 @@ export const handleSteerCommand: CommandHandler = defineAuthorizedTextCommand( return continueWithSteerFallback( params, message, - `steer: active session ${sessionId} threw while steering: ${formatSteerError(err)}; continuing with /steer payload as a normal prompt`, + `steer: active session ${sessionId} threw while steering: ${formatErrorMessage(err)}; continuing with /steer payload as a normal prompt`, ); }); if ("shouldContinue" in queueOutcome) { diff --git a/src/channels/message/receive.ts b/src/channels/message/receive.ts index 19d866da47ad..6a797993dc7c 100644 --- a/src/channels/message/receive.ts +++ b/src/channels/message/receive.ts @@ -3,6 +3,7 @@ * * Models ack/nack policy and idempotent receive state transitions for inbound events. */ +import { formatErrorMessage } from "../../infra/errors.js"; import type { ChannelMessageReceiveAckPolicy } from "./types.js"; /** Public alias for channel receive acknowledgement policy names. */ @@ -48,10 +49,6 @@ function shouldAckMessageAfterStage(policy: MessageAckPolicy, stage: MessageAckS return false; } -function normalizeAckErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - /** Creates a receive context with idempotent ack and explicit nack state transitions. */ export function createMessageReceiveContext(params: { id: string; @@ -97,7 +94,7 @@ export function createMessageReceiveContext(params: { nackInFlight = (async () => { await params.onNack?.(error); ctx.ackState = "nacked"; - ctx.nackErrorMessage = normalizeAckErrorMessage(error); + ctx.nackErrorMessage = formatErrorMessage(error); })(); try { await nackInFlight; diff --git a/src/commands/doctor/cron/legacy-store-migration.ts b/src/commands/doctor/cron/legacy-store-migration.ts index 3f0f9276ea0d..d6bb4733b042 100644 --- a/src/commands/doctor/cron/legacy-store-migration.ts +++ b/src/commands/doctor/cron/legacy-store-migration.ts @@ -16,6 +16,7 @@ import type { } from "../../../cron/store.js"; import type { CronStoreFile } from "../../../cron/types.js"; import { syncDirectoryIfSupported } from "../../../infra/directory-durability.js"; +import { formatErrorMessage } from "../../../infra/errors.js"; import { parseJsonWithJson5Fallback } from "../../../utils/parse-json-compat.js"; const LEGACY_CRON_ARCHIVE_SUFFIX = ".migrated"; @@ -90,10 +91,6 @@ async function legacyCronFileExists(filePath: string): Promise { type ArchiveOutcome = { ok: true; archivePath?: string } | { ok: false; reason: string }; -function formatArchiveError(err: unknown): string { - return err instanceof Error ? err.message : String(err); -} - async function sha256File(filePath: string): Promise { return createHash("sha256") .update(await fs.readFile(filePath)) @@ -131,7 +128,7 @@ async function restoreArchivedSource( } catch (err) { return { ok: false, - reason: `archive remains at ${archivePath} because the source path could not be checked: ${formatArchiveError(err)}`, + reason: `archive remains at ${archivePath} because the source path could not be checked: ${formatErrorMessage(err)}`, }; } try { @@ -148,7 +145,7 @@ async function restoreArchivedSource( } return { ok: false, - reason: `archive remains at ${archivePath} because restoration failed: ${formatArchiveError(err)}`, + reason: `archive remains at ${archivePath} because restoration failed: ${formatErrorMessage(err)}`, }; } try { @@ -157,7 +154,7 @@ async function restoreArchivedSource( } catch (err) { return { ok: false, - reason: `the source was restored, but rollback directory sync failed: ${formatArchiveError(err)}`, + reason: `the source was restored, but rollback directory sync failed: ${formatErrorMessage(err)}`, }; } } @@ -228,7 +225,7 @@ async function copyLegacyCronFileAcrossDevices( if (sourceRemoved) { return { ok: false, - reason: `${formatArchiveError(err)}; the durable archive is preserved at ${archivePath} because the source was already removed`, + reason: `${formatErrorMessage(err)}; the durable archive is preserved at ${archivePath} because the source was already removed`, }; } const cleanupFailures: string[] = []; @@ -247,13 +244,13 @@ async function copyLegacyCronFileAcrossDevices( } catch (cleanupErr) { cleanupFailures.push( archiveRemoved - ? `the partial archive was removed, but cleanup directory sync failed: ${formatArchiveError(cleanupErr)}` - : `partial archive remains at ${archivePath} because cleanup failed: ${formatArchiveError(cleanupErr)}`, + ? `the partial archive was removed, but cleanup directory sync failed: ${formatErrorMessage(cleanupErr)}` + : `partial archive remains at ${archivePath} because cleanup failed: ${formatErrorMessage(cleanupErr)}`, ); } } const cleanupReason = cleanupFailures.length > 0 ? `; ${cleanupFailures.join("; ")}` : ""; - return { ok: false, reason: `${formatArchiveError(err)}${cleanupReason}` }; + return { ok: false, reason: `${formatErrorMessage(err)}${cleanupReason}` }; } } @@ -271,7 +268,7 @@ export async function archiveLegacyCronFile( archivePath = `${filePath}${LEGACY_CRON_ARCHIVE_SUFFIX}.${index}`; } } catch (err) { - return { ok: false, reason: formatArchiveError(err) }; + return { ok: false, reason: formatErrorMessage(err) }; } try { @@ -280,7 +277,7 @@ export async function archiveLegacyCronFile( // A cross-device rename can occur when the configured store is a mounted file. // Fsync before source removal and roll back failed cleanup so retries stay idempotent. if ((err as { code?: unknown })?.code !== "EXDEV") { - return { ok: false, reason: formatArchiveError(err) }; + return { ok: false, reason: formatErrorMessage(err) }; } return await copyLegacyCronFileAcrossDevices(filePath, archivePath, expectedSha256); } @@ -302,8 +299,8 @@ export async function archiveLegacyCronFile( return { ok: false, reason: restoreFailure.ok - ? formatArchiveError(err) - : `${formatArchiveError(err)}; ${restoreFailure.reason}`, + ? formatErrorMessage(err) + : `${formatErrorMessage(err)}; ${restoreFailure.reason}`, }; } } @@ -519,7 +516,7 @@ export async function archiveLegacyCronStoreForMigration( ? "legacy cron state appeared after the store was imported; refusing to archive it" : undefined; } catch (err) { - return `legacy cron state path could not be checked: ${formatArchiveError(err)}`; + return `legacy cron state path could not be checked: ${formatErrorMessage(err)}`; } }; diff --git a/src/commands/system-agent-with-inference.ts b/src/commands/system-agent-with-inference.ts index 08c6d57422d8..732635bf2451 100644 --- a/src/commands/system-agent-with-inference.ts +++ b/src/commands/system-agent-with-inference.ts @@ -1,6 +1,7 @@ // OpenClaw command gate: prove inference before starting conversational setup. import { requestExitAfterOneShotOutput } from "../cli/one-shot-exit.js"; +import { formatErrorMessage } from "../infra/errors.js"; import { withConsoleSubsystemsSuppressed } from "../logging/console.js"; import { defaultRuntime, writeRuntimeJson, type RuntimeEnv } from "../runtime.js"; import type { BoundVerifySetupInferenceResult } from "../system-agent/setup-inference.js"; @@ -32,16 +33,12 @@ function isOneShotRequest(opts: SystemAgentCommandOptions): boolean { return Boolean(opts.json || opts.message?.trim() || opts.interactive === false); } -function formatOneShotExecutionError(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function failOneShotExecution( opts: SystemAgentCommandOptions, runtime: RuntimeEnv, error: unknown, ): void { - const message = formatOneShotExecutionError(error); + const message = formatErrorMessage(error); if (opts.json) { writeRuntimeJson(runtime, { ok: false, error: message }); } else { diff --git a/src/config/io.health-state.ts b/src/config/io.health-state.ts index 74450bb959b5..cbc0adff7cd3 100644 --- a/src/config/io.health-state.ts +++ b/src/config/io.health-state.ts @@ -1,3 +1,4 @@ +import { formatErrorMessage } from "../infra/errors.js"; import { executeSqliteQuerySync, getNodeSqliteKysely } from "../infra/kysely-sync.js"; // Stores config health fingerprints in shared SQLite state. import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js"; @@ -65,10 +66,6 @@ function stringifyConfigHealthFingerprint( return value ? JSON.stringify(value) : null; } -function formatConfigHealthStateError(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - export function readConfigHealthStateFromStore(deps: ConfigHealthStateDeps): ConfigHealthState { try { const database = openOpenClawStateDatabase({ env: resolveConfigHealthStateEnv(deps) }); @@ -142,6 +139,6 @@ export function writeConfigHealthStateToStore( { env: resolveConfigHealthStateEnv(deps) }, ); } catch (error) { - deps.logger.warn(`Config health-state write failed: ${formatConfigHealthStateError(error)}`); + deps.logger.warn(`Config health-state write failed: ${formatErrorMessage(error)}`); } } diff --git a/src/gateway/system-ca-warmup.ts b/src/gateway/system-ca-warmup.ts index 8255016207d5..37b52e558894 100644 --- a/src/gateway/system-ca-warmup.ts +++ b/src/gateway/system-ca-warmup.ts @@ -1,6 +1,7 @@ import type { EventEmitter } from "node:events"; import { Worker, type WorkerOptions } from "node:worker_threads"; import { isVitestRuntimeEnv } from "../infra/env.js"; +import { formatErrorMessage } from "../infra/errors.js"; const SYSTEM_CA_WARMUP_TIMEOUT_MS = 10_000; const SYSTEM_CA_WORKER_SOURCE = String.raw` @@ -54,10 +55,6 @@ function isWorkerPermissionDenied(error: unknown): boolean { ); } -function describeError(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - /** Warm Node's effective default CA set without blocking the gateway event loop on macOS. */ export async function warmMacOSSystemCaOffMainThread( options: SystemCaWarmupOptions = {}, @@ -80,7 +77,7 @@ export async function warmMacOSSystemCaOffMainThread( // CA prewarming is an optimization. Node can still load trust settings lazily. const reason = isWorkerPermissionDenied(error) ? "Node denied worker-thread permission" - : `worker creation failed: ${describeError(error)}`; + : `worker creation failed: ${formatErrorMessage(error)}`; options.log?.warn(`macOS CA warmup skipped because ${reason}; trust settings will load lazily`); return; } diff --git a/src/gateway/tools-invoke-shared.ts b/src/gateway/tools-invoke-shared.ts index d0ad2c0f2df0..3ee9521979e8 100644 --- a/src/gateway/tools-invoke-shared.ts +++ b/src/gateway/tools-invoke-shared.ts @@ -20,6 +20,7 @@ import { import { resolveMainSessionKey } from "../config/sessions.js"; import { resolveSessionEntryAccessTarget } from "../config/sessions/session-accessor.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { formatErrorMessage } from "../infra/errors.js"; import { logWarn } from "../logger.js"; import { isTestDefaultMemorySlotDisabled } from "../plugins/config-state.js"; import { defaultSlotIdForKey } from "../plugins/slots.js"; @@ -121,16 +122,6 @@ function mergeActionIntoArgsIfSupported(params: { return hasAction ? { ...args, action } : args; } -function getErrorMessage(err: unknown): string { - if (err instanceof Error) { - return err.message || String(err); - } - if (typeof err === "string") { - return err; - } - return String(err); -} - function resolveToolInputErrorStatus(err: unknown): number | null { if (err instanceof ToolInputError) { const status = (err as { status?: unknown }).status; @@ -337,7 +328,7 @@ export async function invokeGatewayTool(params: { toolName, error: { type: "tool_error", - message: getErrorMessage(err) || "invalid tool arguments", + message: formatErrorMessage(err) || "invalid tool arguments", }, }; } diff --git a/src/infra/net/proxy/proxy-tls.ts b/src/infra/net/proxy/proxy-tls.ts index f45e3fad3c73..ffe0c73fba1f 100644 --- a/src/infra/net/proxy/proxy-tls.ts +++ b/src/infra/net/proxy/proxy-tls.ts @@ -3,6 +3,7 @@ import { readFileSync } from "node:fs"; import { readFile } from "node:fs/promises"; import type { ProxyConfig } from "../../../config/zod-schema.proxy.js"; +import { formatErrorMessage } from "../../errors.js"; /** TLS trust material passed to proxy clients for OpenClaw-managed HTTPS proxies. */ export type ManagedProxyTlsOptions = Readonly<{ @@ -14,10 +15,6 @@ function normalizeOptionalPath(value: string | undefined): string | undefined { return trimmed ? trimmed : undefined; } -function formatReadError(err: unknown): string { - return err instanceof Error ? err.message : String(err); -} - function isHttpsProxyUrl(value: string | undefined): boolean { if (!value) { return false; @@ -65,7 +62,7 @@ export async function loadManagedProxyTlsOptions( try { return { ca: await readFile(caFile, "utf8") }; } catch (err) { - throw new Error(`proxy CA file could not be read (${caFile}): ${formatReadError(err)}`, { + throw new Error(`proxy CA file could not be read (${caFile}): ${formatErrorMessage(err)}`, { cause: err, }); } @@ -81,7 +78,7 @@ export function loadManagedProxyTlsOptionsSync( try { return { ca: readFileSync(caFile, "utf8") }; } catch (err) { - throw new Error(`proxy CA file could not be read (${caFile}): ${formatReadError(err)}`, { + throw new Error(`proxy CA file could not be read (${caFile}): ${formatErrorMessage(err)}`, { cause: err, }); } diff --git a/src/meeting-bot/node-host.ts b/src/meeting-bot/node-host.ts index bbfb752c6779..89e2c5af87cf 100644 --- a/src/meeting-bot/node-host.ts +++ b/src/meeting-bot/node-host.ts @@ -1,5 +1,6 @@ import { spawn, spawnSync, type ChildProcess } from "node:child_process"; import { randomUUID } from "node:crypto"; +import { formatErrorMessage } from "../infra/errors.js"; import { decodeMeetingAudioBase64 } from "./audio-base64.js"; import { terminateMeetingBridgeProcess } from "./bridge-process.js"; import { MeetingNodeAudioPullWaiters } from "./node-audio-pull-waiters.js"; @@ -83,10 +84,6 @@ function readString(value: unknown): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined; } -function formatErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function readNumber(value: unknown, fallback: number): number { return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback; } diff --git a/src/plugins/official-external-plugin-catalog.ts b/src/plugins/official-external-plugin-catalog.ts index 555c11fe0a27..0e0e1b4f70ab 100644 --- a/src/plugins/official-external-plugin-catalog.ts +++ b/src/plugins/official-external-plugin-catalog.ts @@ -4,6 +4,7 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; import { MANIFEST_KEY } from "../compat/legacy-names.js"; import { normalizeClawHubSha256Integrity } from "../infra/clawhub.js"; +import { formatErrorMessage } from "../infra/errors.js"; import { readResponseWithLimit } from "../infra/http-body.js"; import { isRecord } from "../utils.js"; import type { @@ -694,10 +695,6 @@ function resolveOfficialExternalPluginCatalogEntryKey( return undefined; } -function formatHostedCatalogError(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function bundledFallbackResult( error: unknown, metadata?: HostedOfficialExternalPluginCatalogLoadResult["metadata"], @@ -705,7 +702,7 @@ function bundledFallbackResult( return { source: "bundled-fallback", entries: listOfficialExternalPluginCatalogEntries(), - error: formatHostedCatalogError(error), + error: formatErrorMessage(error), ...(metadata ? { metadata } : {}), }; } @@ -714,7 +711,7 @@ function emptyBundledFallbackResult(error: unknown): HostedOfficialExternalPlugi return { source: "bundled-fallback", entries: [], - error: formatHostedCatalogError(error), + error: formatErrorMessage(error), }; } @@ -912,8 +909,8 @@ async function loadHostedCatalogSnapshotResult(params: { snapshot: params.snapshot, ...(parsed.trust ? { trust: parsed.trust } : {}), error: parsed.expired - ? `${formatHostedCatalogError(params.error)}; ${parsed.feed.expiresAt ? `hosted catalog signed feed expired at ${parsed.feed.expiresAt}` : "hosted catalog signed feed has no expiresAt"}` - : formatHostedCatalogError(params.error), + ? `${formatErrorMessage(params.error)}; ${parsed.feed.expiresAt ? `hosted catalog signed feed expired at ${parsed.feed.expiresAt}` : "hosted catalog signed feed has no expiresAt"}` + : formatErrorMessage(params.error), }; } @@ -999,11 +996,11 @@ async function snapshotOrBundledFallbackResult(params: { } catch (snapshotErr) { if (params.verification?.mode === "signed") { return emptyBundledFallbackResult( - `${formatHostedCatalogError(params.error)}; snapshot fallback failed: ${formatHostedCatalogError(snapshotErr)}`, + `${formatErrorMessage(params.error)}; snapshot fallback failed: ${formatErrorMessage(snapshotErr)}`, ); } return bundledFallbackResult( - `${formatHostedCatalogError(params.error)}; snapshot fallback failed: ${formatHostedCatalogError(snapshotErr)}`, + `${formatErrorMessage(params.error)}; snapshot fallback failed: ${formatErrorMessage(snapshotErr)}`, params.metadata, ); } diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index a316a0e6deb3..148931838be4 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -4,6 +4,7 @@ import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; +import { formatErrorMessage } from "../infra/errors.js"; import { resolveRequiredHomeDir } from "../infra/home-dir.js"; import { tryReadJsonSync } from "../infra/json-files.js"; import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; @@ -354,10 +355,6 @@ function listArgvRuntimeFallbackStartDirs(argv1: string | undefined): string[] { return dedupeResolvedPaths(starts); } -function formatResolutionError(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function resolveDevSourceRootParam(params: { devSourceRoot?: string | null }): string | null { return params.devSourceRoot !== undefined ? params.devSourceRoot @@ -1564,7 +1561,7 @@ export function resolvePluginRuntimeModulePathWithDiagnostics( packageRoot, candidates: dedupeResolvedPaths(candidates), resolvedPath: null, - error: formatResolutionError(error), + error: formatErrorMessage(error), }; } return { diff --git a/src/state/openclaw-database-preflight.ts b/src/state/openclaw-database-preflight.ts index 0ccbf6abfe95..f8c900f1dd4f 100644 --- a/src/state/openclaw-database-preflight.ts +++ b/src/state/openclaw-database-preflight.ts @@ -1,6 +1,7 @@ import { existsSync } from "node:fs"; import path from "node:path"; import type { DatabaseSync } from "node:sqlite"; +import { formatErrorMessage } from "../infra/errors.js"; import { clearNodeSqliteKyselyCacheForDatabase, executeSqliteQuerySync, @@ -88,10 +89,6 @@ function readRegisteredAgentDatabases(database: DatabaseSync): Array<{ ); } -function errorReason(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - /** Read schema headers; report unreadable existing files without diagnosing or repairing them. */ export function preflightOpenClawDatabaseSchemas(options: { env: NodeJS.ProcessEnv; @@ -128,7 +125,7 @@ export function preflightOpenClawDatabaseSchemas(options: { result.indeterminate.push({ kind: "state", path: statePath, - reason: `agent database registry query failed: ${errorReason(error)}`, + reason: `agent database registry query failed: ${formatErrorMessage(error)}`, }); return result; } @@ -161,7 +158,7 @@ export function preflightOpenClawDatabaseSchemas(options: { result.indeterminate.push({ kind: "agent", path: agentPath, - reason: errorReason(error), + reason: formatErrorMessage(error), }); } finally { agentDatabase?.close(); @@ -169,7 +166,11 @@ export function preflightOpenClawDatabaseSchemas(options: { } return result; } catch (error) { - result.indeterminate.push({ kind: "state", path: statePath, reason: errorReason(error) }); + result.indeterminate.push({ + kind: "state", + path: statePath, + reason: formatErrorMessage(error), + }); return result; } finally { if (stateDatabase) { diff --git a/ui/src/api/gateway.ts b/ui/src/api/gateway.ts index 271cccc783d5..b89f3021b1c1 100644 --- a/ui/src/api/gateway.ts +++ b/ui/src/api/gateway.ts @@ -1,4 +1,3 @@ -// Control UI module implements gateway behavior. import { buildGatewayConnectAuth, buildDeviceAuthPayload, @@ -31,6 +30,9 @@ import { MIN_CLIENT_PROTOCOL_VERSION, PROTOCOL_VERSION, } from "@openclaw/gateway-client/browser"; +// Control UI module implements gateway behavior. +import { formatErrorMessage } from "@openclaw/normalization-core"; +import { redactToolDetail } from "../lib/browser-redact.ts"; import { clearDeviceAuthToken, loadDeviceAuthToken, @@ -209,10 +211,6 @@ const BROWSER_WEBSOCKET_CONSTRUCTOR_ERROR_CODE = "BROWSER_WEBSOCKET_CONSTRUCTOR_ const BROWSER_WEBSOCKET_SECURITY_ERROR_CODE = "BROWSER_WEBSOCKET_SECURITY_ERROR"; const DEFAULT_GATEWAY_TICK_INTERVAL_MS = 30_000; const MIN_GATEWAY_TICK_WATCH_INTERVAL_MS = 1_000; -function getErrorMessage(err: unknown): string { - return err instanceof Error && err.message ? err.message : String(err); -} - function toGatewayErrorInfo(error: GatewayRequestError): GatewayErrorInfo { const { gatewayCode: code, message, details, retryable, retryAfterMs } = error; return { code, message, details, retryable, retryAfterMs }; @@ -226,7 +224,7 @@ function getErrorName(err: unknown): string | undefined { function isBrowserWebSocketSecurityError(err: unknown): boolean { const name = getErrorName(err)?.toLowerCase(); - const message = getErrorMessage(err).toLowerCase(); + const message = formatErrorMessage(err, { redact: redactToolDetail }).toLowerCase(); return ( name === "securityerror" || message.includes("security error") || @@ -237,7 +235,7 @@ function isBrowserWebSocketSecurityError(err: unknown): boolean { function formatBrowserWebSocketConstructorError(err: unknown, url: string): GatewayErrorInfo { const securityError = isBrowserWebSocketSecurityError(err); - const browserMessage = getErrorMessage(err); + const browserMessage = formatErrorMessage(err, { redact: redactToolDetail }); const isPlaintextWs = url.trim().toLowerCase().startsWith("ws://"); const details = { code: securityError diff --git a/ui/src/app/web-push.ts b/ui/src/app/web-push.ts index 7733ecc7088c..547004144c21 100644 --- a/ui/src/app/web-push.ts +++ b/ui/src/app/web-push.ts @@ -1,5 +1,7 @@ // Application-owned browser push subscription lifecycle. +import { formatErrorMessage } from "@openclaw/normalization-core"; import type { GatewayBrowserClient } from "../api/gateway.ts"; +import { redactToolDetail } from "../lib/browser-redact.ts"; import type { ApplicationGateway } from "./gateway.ts"; type WebPushSnapshot = { @@ -29,10 +31,6 @@ function isWebPushSupported(): boolean { ); } -function webPushError(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - export function createWebPushCapability(gateway: ApplicationGateway): WebPushCapability { const supported = isWebPushSupported(); let snapshot: WebPushSnapshot = { @@ -91,7 +89,7 @@ export function createWebPushCapability(gateway: ApplicationGateway): WebPushCap publish({ loading: true, error: null }); operation = action(client) .catch((error: unknown) => { - publish({ error: webPushError(error) }); + publish({ error: formatErrorMessage(error, { redact: redactToolDetail }) }); }) .finally(() => { operation = null; diff --git a/ui/src/lib/skills/index.ts b/ui/src/lib/skills/index.ts index 277d77116fd8..206e3fc54242 100644 --- a/ui/src/lib/skills/index.ts +++ b/ui/src/lib/skills/index.ts @@ -1,3 +1,4 @@ +import { formatErrorMessage } from "@openclaw/normalization-core"; import { ClawHubTrustErrorCodes, readClawHubTrustErrorDetails, @@ -9,6 +10,7 @@ import type { SkillStatusEntry, SkillStatusReport, } from "../../api/types.ts"; +import { redactToolDetail } from "../browser-redact.ts"; import { normalizeSkillApiKeyReplacement, runSkillConfigMutation, @@ -155,8 +157,6 @@ function setSkillMessage(state: SkillsState, key: string, message: SkillMessage) state.skillMessages = { ...state.skillMessages, [key]: message }; } -const getErrorMessage = (err: unknown) => (err instanceof Error ? err.message : String(err)); - function getClawHubTrustDetailsFromError(err: unknown) { if (!err || typeof err !== "object" || !("details" in err)) { return undefined; @@ -342,7 +342,7 @@ export async function loadSkills( if (!isCurrent()) { return; } - state.skillsError = getErrorMessage(err); + state.skillsError = formatErrorMessage(err, { redact: redactToolDetail }); } finally { // A transient disconnect invalidates the result, not this invocation's // loading ownership. Source/scope identity still protects newer loads. @@ -453,7 +453,10 @@ export async function loadSkillCard(state: SkillsState, skillKey: string) { } } catch (err) { if (isSkillsAgentScopeCurrent(state, agentScope)) { - state.skillCardErrors = { ...state.skillCardErrors, [skillKey]: getErrorMessage(err) }; + state.skillCardErrors = { + ...state.skillCardErrors, + [skillKey]: formatErrorMessage(err, { redact: redactToolDetail }), + }; } } finally { if (isSkillsAgentScopeCurrent(state, agentScope) && state.skillCardLoadingKey === skillKey) { @@ -496,7 +499,7 @@ async function loadClawHubSecurityVerdicts(state: SkillsState, report: SkillStat return; } state.clawhubVerdicts = {}; - state.clawhubVerdictsError = getErrorMessage(err); + state.clawhubVerdictsError = formatErrorMessage(err, { redact: redactToolDetail }); } finally { if (isSkillsAgentScopeCurrent(state, agentScope)) { state.clawhubVerdictsLoading = false; @@ -549,7 +552,7 @@ async function runSkillMutation( ) { return; } - const message = getErrorMessage(err); + const message = formatErrorMessage(err, { redact: redactToolDetail }); state.skillsError = message; setSkillMessage(state, skillKey, { kind: "error", @@ -652,7 +655,7 @@ export async function loadClawHubDetail(state: SkillsState, slug: string) { state.clawhubDetail = res ?? null; }, (err) => { - state.clawhubDetailError = getErrorMessage(err); + state.clawhubDetailError = formatErrorMessage(err, { redact: redactToolDetail }); }, () => { state.clawhubDetailLoading = false; @@ -718,7 +721,10 @@ export async function installFromClawHub( kind: "error", text: needsAcknowledgement ? formatClawHubAcknowledgementMessage(trustDetails?.warning) - : formatClawHubInstallMessage(getErrorMessage(err), trustDetails?.warning), + : formatClawHubInstallMessage( + formatErrorMessage(err, { redact: redactToolDetail }), + trustDetails?.warning, + ), ...(needsAcknowledgement ? { acknowledgeSlug: slug } : {}), ...(needsAcknowledgement && trustDetails?.version ? { acknowledgeVersion: trustDetails.version } diff --git a/ui/src/pages/agents/identity-actions.ts b/ui/src/pages/agents/identity-actions.ts index 83c56927d372..d5e9955b5b54 100644 --- a/ui/src/pages/agents/identity-actions.ts +++ b/ui/src/pages/agents/identity-actions.ts @@ -1,8 +1,10 @@ // Agent identity draft state and persistence, split out of agents-page.ts. +import { formatErrorMessage } from "@openclaw/normalization-core"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; import type { ApplicationContext, ApplicationNavigationPreferences } from "../../app/context.ts"; import { t } from "../../i18n/index.ts"; import { updateAgentIdentity } from "../../lib/agents/index.ts"; +import { redactToolDetail } from "../../lib/browser-redact.ts"; import { fileToAvatarDataUrl } from "./avatar-image.ts"; import type { AgentIdentityDraft } from "./panels-overview.ts"; @@ -12,10 +14,6 @@ type AgentIdentityEditorHost = { identityError: string | null; }; -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - const avatarSelectionEpochs = new WeakMap(); function advanceAvatarSelectionEpoch(host: AgentIdentityEditorHost): number { @@ -99,14 +97,14 @@ export async function saveIdentityDraft(params: { await agents.refreshList(); } catch (error) { refreshErrors.push( - `Agent identity was saved, but the agent list refresh failed: ${errorMessage(error)}`, + `Agent identity was saved, but the agent list refresh failed: ${formatErrorMessage(error, { redact: redactToolDetail })}`, ); } try { await agentIdentity.ensure([agentId]); } catch (error) { refreshErrors.push( - `Agent identity was saved, but the identity refresh failed: ${errorMessage(error)}`, + `Agent identity was saved, but the identity refresh failed: ${formatErrorMessage(error, { redact: redactToolDetail })}`, ); } if (params.isCurrent()) { diff --git a/ui/src/pages/chat/chat-composer-capability-host.ts b/ui/src/pages/chat/chat-composer-capability-host.ts index 33127ae1fe07..3bd504a05ded 100644 --- a/ui/src/pages/chat/chat-composer-capability-host.ts +++ b/ui/src/pages/chat/chat-composer-capability-host.ts @@ -1,3 +1,4 @@ +import { formatErrorMessage } from "@openclaw/normalization-core"; import { asNullableRecord as asRecord } from "@openclaw/normalization-core/record-coerce"; import { html, nothing } from "lit"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; @@ -17,6 +18,7 @@ import { buildToolsEffectiveRequestKey, loadToolsEffective, } from "../../lib/agents/tools-effective.ts"; +import { redactToolDetail } from "../../lib/browser-redact.ts"; import { buildAddMcpServerPatch, MCP_SERVER_NAME_PATTERN, @@ -59,10 +61,6 @@ type CapabilityMutationResult = | { ok: true } | { ok: false; error: string; stage: "config" | "session" }; -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function webSearchBaseEnabled(config: Record | null): boolean { return asRecord(asRecord(asRecord(config?.tools)?.web)?.search)?.enabled !== false; } @@ -129,7 +127,11 @@ export class ChatComposerCapabilityHost { try { globalResult = await options.patchGlobal(globalConfig); } catch (error) { - return { ok: false, error: errorMessage(error), stage: "config" }; + return { + ok: false, + error: formatErrorMessage(error, { redact: redactToolDetail }), + stage: "config", + }; } if (!globalResult.ok) { return { ...globalResult, stage: "config" }; @@ -141,7 +143,11 @@ export class ChatComposerCapabilityHost { try { loaded = await options.loadSessionOverrides(); } catch (error) { - return { ok: false, error: errorMessage(error), stage: "session" }; + return { + ok: false, + error: formatErrorMessage(error, { redact: redactToolDetail }), + stage: "session", + }; } if (!loaded.ok) { return { ...loaded, stage: "session" }; @@ -157,7 +163,11 @@ export class ChatComposerCapabilityHost { try { sessionResult = await options.patchSession(next); } catch (error) { - return { ok: false, error: errorMessage(error), stage: "session" }; + return { + ok: false, + error: formatErrorMessage(error, { redact: redactToolDetail }), + stage: "session", + }; } return sessionResult.ok ? sessionResult : { ...sessionResult, stage: "session" }; } @@ -395,7 +405,7 @@ export class ChatComposerCapabilityHost { } catch (error) { return { ok: false as const, - error: errorMessage(error), + error: formatErrorMessage(error, { redact: redactToolDetail }), }; } if (!identityMatches()) { diff --git a/ui/src/pages/config/memory-memories.ts b/ui/src/pages/config/memory-memories.ts index c3d23ecd737c..02a1e3c54123 100644 --- a/ui/src/pages/config/memory-memories.ts +++ b/ui/src/pages/config/memory-memories.ts @@ -1,9 +1,11 @@ +import { formatErrorMessage } from "@openclaw/normalization-core"; import { html, nothing, type PropertyValues } from "lit"; import { property, state } from "lit/decorators.js"; import type { AgentsWorkspaceGetResult } from "../../../../packages/gateway-protocol/src/index.js"; import type { MemorySearchResponse } from "../../../../src/gateway/server-methods/memory-search.ts"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; import { t } from "../../i18n/index.ts"; +import { redactToolDetail } from "../../lib/browser-redact.ts"; import { OpenClawLightDomElement } from "../../lit/openclaw-element.ts"; import "../../styles/memory-memories.css"; @@ -18,10 +20,6 @@ type DetailState = | { kind: "ready"; content: string } | { kind: "error"; message: string }; -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function resultKey(result: SearchResult, index: number): string { return `${index}:${result.path}:${result.startLine}:${result.endLine}`; } @@ -113,7 +111,11 @@ class MemoryMemoriesElement extends OpenClawLightDomElement { if (this.searchRequest !== request || this.agentId !== agentId || this.client !== client) { return; } - this.searchState = { kind: "error", query: normalizedQuery, message: errorMessage(error) }; + this.searchState = { + kind: "error", + query: normalizedQuery, + message: formatErrorMessage(error, { redact: redactToolDetail }), + }; } } @@ -157,7 +159,7 @@ class MemoryMemoriesElement extends OpenClawLightDomElement { } this.details = new Map(this.details).set(key, { kind: "error", - message: errorMessage(error), + message: formatErrorMessage(error, { redact: redactToolDetail }), }); } finally { if (this.detailRequests.get(key) === request) { diff --git a/ui/src/pages/config/memory-page.ts b/ui/src/pages/config/memory-page.ts index 60294fad951b..cb09c16db5e2 100644 --- a/ui/src/pages/config/memory-page.ts +++ b/ui/src/pages/config/memory-page.ts @@ -2,6 +2,7 @@ // this element owns the shared agent selection, Overview status, and global // configuration controllers used by Settings. import { consume } from "@lit/context"; +import { formatErrorMessage } from "@openclaw/normalization-core"; import { asNullableRecord as asConfigRecord } from "@openclaw/normalization-core/record-coerce"; import { html, type PropertyValues, type TemplateResult } from "lit"; import { property, state } from "lit/decorators.js"; @@ -14,6 +15,7 @@ import type { AgentSelectOption } from "../../components/agent-select.ts"; import { renderDocsLink } from "../../components/settings-ui.ts"; import { t } from "../../i18n/index.ts"; import { listSelectableAgents, normalizeAgentLabel } from "../../lib/agents/display.ts"; +import { redactToolDetail } from "../../lib/browser-redact.ts"; import { currentConfigObject } from "../../lib/config/index.ts"; import { isGatewayMethodAdvertised } from "../../lib/gateway-methods.ts"; import { @@ -87,10 +89,6 @@ type MemoryPageProps = { buildEditor: (keys: readonly string[]) => TemplateResult; }; -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - class MemorySettingsPage extends OpenClawLightDomElement { @consume({ context: applicationContext, subscribe: true }) private context!: ApplicationContext; @@ -383,7 +381,10 @@ class MemorySettingsPage extends OpenClawLightDomElement { if (!this.isConnected || this.overviewRequest !== request) { return; } - this.overviewStatus = { kind: "error", message: errorMessage(error) }; + this.overviewStatus = { + kind: "error", + message: formatErrorMessage(error, { redact: redactToolDetail }), + }; } finally { if (this.overviewRequest === request) { this.probingEmbeddings = false; @@ -496,7 +497,10 @@ class MemorySettingsPage extends OpenClawLightDomElement { } } catch (error) { if (this.connection === connection) { - this.addonErrors = new Map(this.addonErrors).set(pluginId, errorMessage(error)); + this.addonErrors = new Map(this.addonErrors).set( + pluginId, + formatErrorMessage(error, { redact: redactToolDetail }), + ); } } finally { if (this.addonNoticeOperations.get(pluginId) === noticeOperation) { @@ -547,7 +551,10 @@ class MemorySettingsPage extends OpenClawLightDomElement { } } catch (error) { if (this.connection === connection) { - this.engineOutcome = { kind: "error", message: errorMessage(error) }; + this.engineOutcome = { + kind: "error", + message: formatErrorMessage(error, { redact: redactToolDetail }), + }; } } finally { if (this.connection === connection) { diff --git a/ui/src/pages/new-session/cloud-target.ts b/ui/src/pages/new-session/cloud-target.ts index 29134b226cd3..611a2b683c7a 100644 --- a/ui/src/pages/new-session/cloud-target.ts +++ b/ui/src/pages/new-session/cloud-target.ts @@ -1,3 +1,4 @@ +import { formatErrorMessage } from "@openclaw/normalization-core"; import { html, nothing } from "lit"; import type { EnvironmentsListResult, @@ -6,6 +7,7 @@ import type { import { GatewayRequestError, type GatewayBrowserClient } from "../../api/gateway.ts"; import { icons } from "../../components/icons.ts"; import { t } from "../../i18n/index.ts"; +import { redactToolDetail } from "../../lib/browser-redact.ts"; import { generateUUID } from "../../lib/uuid.ts"; import type { DraftCloudProfile } from "./discovery.ts"; import { readDraftCloudProfiles } from "./discovery.ts"; @@ -45,10 +47,6 @@ const PENDING_PLACEMENT_STATES = new Set([ "reconciling", ]); -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function isAmbiguousDispatchError(error: unknown): boolean { if (error instanceof GatewayRequestError) { return error.retryable || error.gatewayCode === "UNAVAILABLE"; @@ -70,7 +68,10 @@ async function readPlacement( return { status: "read", placement: described?.session?.placement }; } catch (error) { if (!isAmbiguousDispatchError(error)) { - return { status: "rejected", error: errorMessage(error) }; + return { + status: "rejected", + error: formatErrorMessage(error, { redact: redactToolDetail }), + }; } return { status: "unavailable" }; } @@ -95,7 +96,7 @@ async function cancelActivePlacement( await client.request("environments.destroy", { environmentId }); return undefined; } catch (error) { - return errorMessage(error); + return formatErrorMessage(error, { redact: redactToolDetail }); } } @@ -231,7 +232,7 @@ export async function deleteCloudDraftSession( await client.request("sessions.delete", { key, agentId, deleteTranscript: true }); return undefined; } catch (error) { - return errorMessage(error); + return formatErrorMessage(error, { redact: redactToolDetail }); } } @@ -333,7 +334,7 @@ export async function startCloudInitialTurn( isCurrent, ); } catch (error) { - dispatchError = errorMessage(error); + dispatchError = formatErrorMessage(error, { redact: redactToolDetail }); if (!isAmbiguousDispatchError(error)) { return { status: "dispatch-rejected", error: dispatchError }; } @@ -430,9 +431,17 @@ export async function startCloudInitialTurn( }); return cleanupError ? { status: "cleanup-rejected", error: cleanupError, messageId } - : { status: "send-definitive-rejected", error: errorMessage(error), messageId }; + : { + status: "send-definitive-rejected", + error: formatErrorMessage(error, { redact: redactToolDetail }), + messageId, + }; } - return { status: "send-rejected", error: errorMessage(error), messageId }; + return { + status: "send-rejected", + error: formatErrorMessage(error, { redact: redactToolDetail }), + messageId, + }; } } diff --git a/ui/src/pages/plugins/plugins-page.ts b/ui/src/pages/plugins/plugins-page.ts index 5e1af7c2acbb..a99ab588ed82 100644 --- a/ui/src/pages/plugins/plugins-page.ts +++ b/ui/src/pages/plugins/plugins-page.ts @@ -1,5 +1,6 @@ import { consume } from "@lit/context"; import { initialState, Task, TaskStatus } from "@lit/task"; +import { formatErrorMessage } from "@openclaw/normalization-core"; import type { RouteLocation } from "@openclaw/uirouter"; import { html, type PropertyValues } from "lit"; import { property, state } from "lit/decorators.js"; @@ -18,6 +19,7 @@ import type { McpServerForm } from "../../components/mcp-server-form.ts"; import { renderDocsLink } from "../../components/settings-ui.ts"; import { renderSettingsWorkspace } from "../../components/settings-workspace.ts"; import { t } from "../../i18n/index.ts"; +import { redactToolDetail } from "../../lib/browser-redact.ts"; import { resolveEditableSnapshotConfig } from "../../lib/config/index.ts"; import { buildAddMcpServerPatch, @@ -69,10 +71,6 @@ export type PluginsRouteData = { location: RouteLocation; }; -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function committedMutationMessage(success: string, refreshError: string | null): PluginRowMessage { return { kind: "success", @@ -162,7 +160,7 @@ class PluginsPage extends OpenClawLightDomElement { this.replaceResult(result); }, onError: (error) => { - this.error = errorMessage(error); + this.error = formatErrorMessage(error, { redact: redactToolDetail }); }, }); @@ -532,14 +530,14 @@ class PluginsPage extends OpenClawLightDomElement { private get searchError(): string | null { return this.searchTask.status === TaskStatus.ERROR && this.debouncedSearchQuery === this.query.trim() - ? errorMessage(this.searchTask.error) + ? formatErrorMessage(this.searchTask.error, { redact: redactToolDetail }) : null; } private get configRefreshError(): string | null { const failure = this.configTask.status === TaskStatus.ERROR - ? errorMessage(this.configTask.error) + ? formatErrorMessage(this.configTask.error, { redact: redactToolDetail }) : this.configTask.status === TaskStatus.COMPLETE ? this.configTask.value : null; @@ -736,7 +734,10 @@ class PluginsPage extends OpenClawLightDomElement { isCurrent: () => boolean, ) => Promise, onError: (error: unknown) => void = (error) => { - this.setMessage(rowKey, { kind: "error", text: errorMessage(error) }); + this.setMessage(rowKey, { + kind: "error", + text: formatErrorMessage(error, { redact: redactToolDetail }), + }); }, ): Promise { const client = this.client; @@ -795,7 +796,10 @@ class PluginsPage extends OpenClawLightDomElement { }); return; } - this.setMessage(rowKey, { kind: "error", text: errorMessage(error) }); + this.setMessage(rowKey, { + kind: "error", + text: formatErrorMessage(error, { redact: redactToolDetail }), + }); }, ); } @@ -889,7 +893,7 @@ class PluginsPage extends OpenClawLightDomElement { this.mcpMessage = { kind: "success", text: params.successText }; return true; } catch (error) { - return fail(errorMessage(error)); + return fail(formatErrorMessage(error, { redact: redactToolDetail })); } finally { this.mcpBusy = false; if (params.busyKey) { diff --git a/ui/src/pages/plugins/route.ts b/ui/src/pages/plugins/route.ts index c78eacca7f3d..0c3bcfea14cb 100644 --- a/ui/src/pages/plugins/route.ts +++ b/ui/src/pages/plugins/route.ts @@ -1,15 +1,13 @@ +import { formatErrorMessage } from "@openclaw/normalization-core"; import { definePage, type RouteLoaderOptions, type RouteLocation } from "@openclaw/uirouter"; import { html } from "lit"; import { routePageSpec } from "../../app-route-paths.ts"; import type { ApplicationContext } from "../../app/context.ts"; +import { redactToolDetail } from "../../lib/browser-redact.ts"; import { loadPluginCatalog } from "../../lib/plugins/index.ts"; import type { PluginsRouteData } from "./plugins-page.ts"; import { pluginsRouteLocation } from "./route-data.ts"; -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - async function loadPluginsRouteData( context: ApplicationContext, options: RouteLoaderOptions, @@ -25,7 +23,13 @@ async function loadPluginsRouteData( const result = await loadPluginCatalog(client); return { gateway, gatewaySnapshot, result, error: null, location }; } catch (error) { - return { gateway, gatewaySnapshot, result: null, error: errorMessage(error), location }; + return { + gateway, + gatewaySnapshot, + result: null, + error: formatErrorMessage(error, { redact: redactToolDetail }), + location, + }; } } diff --git a/ui/src/pages/skill-workshop/history-scan.ts b/ui/src/pages/skill-workshop/history-scan.ts index 594346f42a57..1012caee5a24 100644 --- a/ui/src/pages/skill-workshop/history-scan.ts +++ b/ui/src/pages/skill-workshop/history-scan.ts @@ -1,12 +1,10 @@ +import { formatErrorMessage } from "@openclaw/normalization-core"; import { html, nothing } from "lit"; import type { ApplicationGateway } from "../../app/context.ts"; import { t } from "../../i18n/index.ts"; +import { redactToolDetail } from "../../lib/browser-redact.ts"; import type { SkillWorkshopHistoryScanResult, SkillWorkshopHistoryScanState } from "./state.ts"; -function getErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - type SkillWorkshopHistoryStatusLoadParams = { agentId: string; gateway: ApplicationGateway; @@ -70,7 +68,7 @@ export async function loadSkillWorkshopHistoryScanStatus( ); current.state.loaded = true; } catch (error) { - current.state.error = getErrorMessage(error); + current.state.error = formatErrorMessage(error, { redact: redactToolDetail }); // Loaded means this scope attempted a read. A scan action can still // force a retry because the result remains absent. current.state.loaded = true; @@ -130,7 +128,7 @@ export async function runSkillWorkshopHistoryScan(params: { params.state.loaded = true; return true; } catch (error) { - const scanError = getErrorMessage(error); + const scanError = formatErrorMessage(error, { redact: redactToolDetail }); try { params.state.result = await client.request( "skills.proposals.historyStatus", diff --git a/ui/src/pages/skill-workshop/proposals.ts b/ui/src/pages/skill-workshop/proposals.ts index b1191fa5bb43..cff40d2c6adf 100644 --- a/ui/src/pages/skill-workshop/proposals.ts +++ b/ui/src/pages/skill-workshop/proposals.ts @@ -1,8 +1,9 @@ // Control UI controller manages skill workshop gateway state. -import { formatByteSize } from "@openclaw/normalization-core"; +import { formatByteSize, formatErrorMessage } from "@openclaw/normalization-core"; import type { AgentSelectionCapability } from "../../app/agent-selection.ts"; import type { ApplicationGateway } from "../../app/context.ts"; import { t } from "../../i18n/index.ts"; +import { redactToolDetail } from "../../lib/browser-redact.ts"; import { normalizeAgentId, parseAgentSessionKey, @@ -100,10 +101,6 @@ export type SkillWorkshopContext = { agentSelection: Pick; }; -function getErrorMessage(err: unknown): string { - return err instanceof Error ? err.message : String(err); -} - function skillWorkshopAgentParams(context: SkillWorkshopContext): { agentId: string } { const snapshot = context.gateway.snapshot; const sessionAgentId = parseAgentSessionKey(snapshot.sessionKey)?.agentId; @@ -415,7 +412,7 @@ export async function loadSkillWorkshopProposals( await loadSkillWorkshopProposalDetail(state, context, state.skillWorkshopSelectedKey); } } catch (err) { - state.skillWorkshopError = getErrorMessage(err); + state.skillWorkshopError = formatErrorMessage(err, { redact: redactToolDetail }); } finally { state.skillWorkshopLoading = false; if (skillWorkshopAgentParams(context).agentId !== requestAgentId) { @@ -465,7 +462,7 @@ async function loadSkillWorkshopProposalDetail( return true; } catch (err) { if (state.skillWorkshopAgentId === requestAgentId) { - state.skillWorkshopError = getErrorMessage(err); + state.skillWorkshopError = formatErrorMessage(err, { redact: redactToolDetail }); } return false; } finally { @@ -530,7 +527,7 @@ export async function runSkillWorkshopLifecycleAction( t(action === "apply" ? "skillWorkshop.notices.applied" : "skillWorkshop.notices.rejected"), ); } catch (err) { - state.skillWorkshopError = getErrorMessage(err); + state.skillWorkshopError = formatErrorMessage(err, { redact: redactToolDetail }); } finally { if ( state.skillWorkshopActionBusy?.key === proposalId && @@ -594,7 +591,7 @@ export async function runSkillWorkshopEvaluation( return true; } catch (err) { if (state.skillWorkshopAgentId === requestAgentId) { - state.skillWorkshopError = getErrorMessage(err); + state.skillWorkshopError = formatErrorMessage(err, { redact: redactToolDetail }); } return false; } finally { @@ -645,7 +642,7 @@ export async function requestSkillWorkshopRevision( showActionNotice(state, proposal, t("skillWorkshop.notices.revisionRequested")); return true; } catch (err) { - state.skillWorkshopError = getErrorMessage(err); + state.skillWorkshopError = formatErrorMessage(err, { redact: redactToolDetail }); return false; } finally { if ( diff --git a/ui/src/pages/skills/route.ts b/ui/src/pages/skills/route.ts index f9687109ced1..362657351cf9 100644 --- a/ui/src/pages/skills/route.ts +++ b/ui/src/pages/skills/route.ts @@ -1,14 +1,12 @@ +import { formatErrorMessage } from "@openclaw/normalization-core"; import { definePage } from "@openclaw/uirouter"; import { html } from "lit"; import { routePageSpec } from "../../app-route-paths.ts"; import type { ApplicationContext } from "../../app/context.ts"; +import { redactToolDetail } from "../../lib/browser-redact.ts"; import { loadSkillStatusReport } from "../../lib/skills/index.ts"; import type { SkillsRouteData } from "./skills-page.ts"; -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - async function loadSkillsRouteData(context: ApplicationContext): Promise { const gateway = context.gateway; const gatewaySnapshot = gateway.snapshot; @@ -32,12 +30,12 @@ async function loadSkillsRouteData(context: ApplicationContext): Promise