mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor: consolidate coercion contracts (#122458)
* refactor: consolidate coercion contracts Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics. Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit. * fix: preserve standalone script coercions Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
This commit is contained in:
committed by
GitHub
parent
66fe424590
commit
b080dd1e76
@@ -9,6 +9,7 @@ import { isPathInside } from "openclaw/plugin-sdk/security-runtime";
|
||||
import {
|
||||
asOptionalRecord,
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
normalizeStringEntries,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
@@ -132,11 +133,6 @@ function resolveToolsAllow(params: { pluginToolsAllow: unknown; cfg?: OpenClawCo
|
||||
);
|
||||
}
|
||||
|
||||
function normalizePromptConfigText(value: unknown): string | undefined {
|
||||
const text = typeof value === "string" ? value.trim() : "";
|
||||
return text ? text : undefined;
|
||||
}
|
||||
|
||||
function hasDeprecatedModelFallbackPolicy(pluginConfig: unknown): boolean {
|
||||
const raw = asOptionalRecord(pluginConfig);
|
||||
return raw ? Object.hasOwn(raw, "modelFallbackPolicy") : false;
|
||||
@@ -239,8 +235,8 @@ function normalizePluginConfig(
|
||||
fastMode: normalizeActiveMemoryFastMode(raw.fastMode),
|
||||
promptStyle: resolvePromptStyle(raw.promptStyle, raw.queryMode),
|
||||
toolsAllow: resolveToolsAllow({ pluginToolsAllow: raw.toolsAllow, cfg }),
|
||||
promptOverride: normalizePromptConfigText(raw.promptOverride),
|
||||
promptAppend: normalizePromptConfigText(raw.promptAppend),
|
||||
promptOverride: normalizeOptionalString(raw.promptOverride),
|
||||
promptAppend: normalizeOptionalString(raw.promptAppend),
|
||||
timeoutMs: clampInt(
|
||||
parseOptionalPositiveInt(raw.timeoutMs, DEFAULT_TIMEOUT_MS),
|
||||
DEFAULT_TIMEOUT_MS,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { parseDateStringTimestampMs } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { withSessionTranscriptWriteLock } from "openclaw/plugin-sdk/session-transcript-runtime";
|
||||
import { CLAUDE_CLI_BACKEND_ID } from "./cli-constants.js";
|
||||
import type { ClaudeTranscriptItem } from "./session-catalog-transcript.js";
|
||||
@@ -8,8 +9,7 @@ function importedClaudeMessage(
|
||||
item: ClaudeTranscriptItem,
|
||||
fallbackTimestamp: number,
|
||||
): AgentMessage | undefined {
|
||||
const parsedTimestamp = item.timestamp ? Date.parse(item.timestamp) : Number.NaN;
|
||||
const timestamp = Number.isFinite(parsedTimestamp) ? parsedTimestamp : fallbackTimestamp;
|
||||
const timestamp = parseDateStringTimestampMs(item.timestamp) ?? fallbackTimestamp;
|
||||
const importedText = item.text?.trim();
|
||||
if (!importedText && item.type === "reasoning") {
|
||||
return undefined;
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
SessionCatalogTranscriptItem,
|
||||
} from "openclaw/plugin-sdk/session-catalog";
|
||||
import {
|
||||
asPositiveSafeInteger as pullRequestNumber,
|
||||
isRecord,
|
||||
normalizeBoundedOptionalString as readBoundedString,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
@@ -241,10 +242,6 @@ function pullRequestState(value: unknown): SessionCatalogPullRequestSummary["sta
|
||||
}
|
||||
}
|
||||
|
||||
function pullRequestNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
// Desktop retains historical PRs in order and marks hidden ones as dismissed;
|
||||
// the top-level pair identifies the current PR whose state labels the row.
|
||||
function desktopPullRequestSummary(
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type SessionUpstreamActivity,
|
||||
type SessionUpstreamProbe,
|
||||
} from "openclaw/plugin-sdk/session-catalog";
|
||||
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { asSafeIntegerInRange, isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { ClaudeTranscriptItem } from "./session-catalog-transcript.js";
|
||||
|
||||
const MAX_CLAUDE_UPSTREAM_SCAN_BYTES = 1024 * 1024;
|
||||
@@ -119,7 +119,7 @@ function readMarkerOffset(probe: SessionUpstreamProbe): number | undefined {
|
||||
return undefined;
|
||||
}
|
||||
const offset = probe.marker.offset ?? probe.marker.size;
|
||||
return Number.isSafeInteger(offset) && (offset as number) >= 0 ? (offset as number) : undefined;
|
||||
return asSafeIntegerInRange(offset, { min: 0 });
|
||||
}
|
||||
|
||||
async function checkClaudeSessionUpstreamActivity(
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
parseStrictInteger,
|
||||
resolveTimerTimeoutMs,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { BrowserActRequest } from "./client-actions.types.js";
|
||||
import { DEFAULT_BROWSER_ACTION_TIMEOUT_MS } from "./constants.js";
|
||||
|
||||
@@ -110,7 +111,7 @@ function addNavigationGraceMs(durationMs: number, count = 1): number {
|
||||
}
|
||||
|
||||
function isActionObject(value: unknown): value is BrowserActRequest {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
return isRecord(value);
|
||||
}
|
||||
|
||||
function resolveLeafExecutionBudgetMs(
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type StableChannelIngressIdentityParams,
|
||||
} from "openclaw/plugin-sdk/channel-ingress-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { parseDateStringTimestampMs } from "openclaw/plugin-sdk/number-runtime";
|
||||
import {
|
||||
normalizeAgentId,
|
||||
type ResolvedAgentRoute,
|
||||
@@ -249,6 +250,7 @@ export async function resolveClickClackInboundAccess(params: {
|
||||
preparedRoute,
|
||||
};
|
||||
}
|
||||
const botLoopNowMs = parseDateStringTimestampMs(params.message.created_at);
|
||||
const botLoopProtection =
|
||||
isBotAuthor && params.message.author_id !== params.account.botUserId && params.account.botUserId
|
||||
? {
|
||||
@@ -263,9 +265,7 @@ export async function resolveClickClackInboundAccess(params: {
|
||||
senderId: params.message.author_id,
|
||||
receiverId: params.account.botUserId,
|
||||
eventId: params.message.id,
|
||||
...(Number.isFinite(Date.parse(params.message.created_at))
|
||||
? { nowMs: Date.parse(params.message.created_at) }
|
||||
: {}),
|
||||
...(botLoopNowMs !== undefined ? { nowMs: botLoopNowMs } : {}),
|
||||
config: effectiveBotPolicy.botLoopProtection,
|
||||
defaultsConfig: cfg.channels?.defaults?.botLoopProtection,
|
||||
defaultEnabled: true,
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
* Codex-backed media understanding provider for bounded image description and
|
||||
* structured extraction turns.
|
||||
*/
|
||||
import {
|
||||
type JsonSchemaObject,
|
||||
validateJsonSchemaValue,
|
||||
} from "openclaw/plugin-sdk/json-schema-runtime";
|
||||
import { validateJsonSchemaValue } from "openclaw/plugin-sdk/json-schema-runtime";
|
||||
import type {
|
||||
ImagesDescriptionRequest,
|
||||
ImagesDescriptionResult,
|
||||
@@ -13,6 +10,7 @@ import type {
|
||||
StructuredExtractionRequest,
|
||||
StructuredExtractionResult,
|
||||
} from "openclaw/plugin-sdk/media-understanding";
|
||||
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
runBoundedCodexAppServerTurn,
|
||||
type CodexBoundedTurnOptions,
|
||||
@@ -179,10 +177,6 @@ function buildStructuredExtractionPrompt(req: StructuredExtractionRequest): stri
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
function isJsonSchemaObject(value: unknown): value is JsonSchemaObject {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function normalizeStructuredExtractionResult(params: {
|
||||
text: string;
|
||||
model: string;
|
||||
@@ -201,7 +195,7 @@ function normalizeStructuredExtractionResult(params: {
|
||||
} catch {
|
||||
throw new Error("Codex structured extraction returned invalid JSON.");
|
||||
}
|
||||
if (isJsonSchemaObject(params.req.jsonSchema)) {
|
||||
if (isRecord(params.req.jsonSchema)) {
|
||||
const validation = validateJsonSchemaValue({
|
||||
schema: params.req.jsonSchema,
|
||||
cacheKey: "codex.media-understanding.extractStructured",
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
SessionTranscriptTargetParams,
|
||||
TranscriptTurnAdmission,
|
||||
} from "openclaw/plugin-sdk/session-transcript-runtime";
|
||||
import { readNonBlankString as readNonEmptyString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { EmbeddedRunAttemptResult } from "./attempt-terminal.js";
|
||||
import type { CodexDynamicToolFunctionSpec, CodexDynamicToolSpec, JsonValue } from "./protocol.js";
|
||||
import { flattenCodexDynamicToolFunctions } from "./protocol.js";
|
||||
@@ -502,10 +503,6 @@ function readPositiveNumber(value: unknown): number | undefined {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function readNonEmptyString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds OpenClaw-provided workspace prompt context for the current Codex turn.
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { resolveAgentDir, resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
|
||||
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
|
||||
import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { isIncognitoSessionKey } from "../incognito-session.js";
|
||||
@@ -963,13 +964,10 @@ function isCodexThreadNotFoundError(error: unknown): boolean {
|
||||
// compaction.rs asserts message.contains("thread not found")). So the message
|
||||
// is the authoritative positive signal here, not the generic code. This is a
|
||||
// self-heal recovery gate, not user-facing classification.
|
||||
return formatCompactionError(error).toLowerCase().includes("thread not found");
|
||||
return coerceErrorMessage(error).toLowerCase().includes("thread not found");
|
||||
}
|
||||
|
||||
function formatCompactionError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
return coerceErrorMessage(error);
|
||||
}
|
||||
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|
||||
|
||||
@@ -2,6 +2,8 @@ import { createHmac, randomBytes } from "node:crypto";
|
||||
import { resolvePositiveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
||||
import {
|
||||
asOptionalRecord as readRecord,
|
||||
normalizeOptionalString as readNonEmptyString,
|
||||
normalizeTrimmedStringList,
|
||||
parseBooleanValue,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
@@ -12,11 +14,7 @@ const START_OPTIONS_KEY_SECRET_SYMBOL = Symbol.for("openclaw.codexAppServerStart
|
||||
const START_OPTIONS_KEY_SECRET = getStartOptionsKeySecret();
|
||||
const PLAIN_DECIMAL_NUMBER_RE = /^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))$/;
|
||||
|
||||
export function readRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: undefined;
|
||||
}
|
||||
export { readNonEmptyString, readRecord };
|
||||
|
||||
export function normalizeCodexServiceTier(value: unknown): CodexServiceTier | undefined {
|
||||
if (typeof value !== "string") {
|
||||
@@ -108,14 +106,6 @@ export function resolveArgs(configArgs: unknown, envArgs: string | undefined): s
|
||||
return splitShellWords(envArgs ?? "");
|
||||
}
|
||||
|
||||
export function readNonEmptyString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
export function hashSecretForKey(value: string | undefined, label: string): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
|
||||
@@ -46,6 +46,7 @@ import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import type { ImageContent, TextContent } from "openclaw/plugin-sdk/llm";
|
||||
import { normalizeOpenAIToolSchemas } from "openclaw/plugin-sdk/provider-tools";
|
||||
import {
|
||||
asNonArrayRecord,
|
||||
asOptionalRecord,
|
||||
isRecord,
|
||||
normalizeOptionalString,
|
||||
@@ -557,7 +558,7 @@ export function createCodexDynamicToolBridge(params: {
|
||||
handleToolCall: async (call, options) => {
|
||||
const toolEntry = toolMap.get(call.tool);
|
||||
if (!toolEntry) {
|
||||
const executedArguments = jsonObjectToRecord(call.arguments);
|
||||
const executedArguments = asNonArrayRecord(call.arguments);
|
||||
const message = registeredToolNames.has(call.tool)
|
||||
? `OpenClaw tool is not available for this turn: ${call.tool}`
|
||||
: `Unknown OpenClaw tool: ${call.tool}`;
|
||||
@@ -582,7 +583,7 @@ export function createCodexDynamicToolBridge(params: {
|
||||
});
|
||||
}
|
||||
const { tool, name: toolName } = toolEntry;
|
||||
const args = jsonObjectToRecord(call.arguments);
|
||||
const args = asNonArrayRecord(call.arguments);
|
||||
const startedAt = Date.now();
|
||||
const signal = composeAbortSignals(params.signal, options?.signal);
|
||||
let didStartExecution = false;
|
||||
@@ -1530,12 +1531,6 @@ function convertToolContent(
|
||||
},
|
||||
];
|
||||
}
|
||||
function jsonObjectToRecord(value: JsonValue | undefined): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
function readFirstString(record: Record<string, unknown>, keys: string[]): string | undefined {
|
||||
for (const key of keys) {
|
||||
const value = record[key];
|
||||
|
||||
@@ -2,7 +2,10 @@ import {
|
||||
formatToolAggregate,
|
||||
formatToolProgressOutput,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { readStringField as readString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
asNonArrayRecord,
|
||||
readStringField as readString,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { isJsonObject, type CodexThreadItem } from "./protocol.js";
|
||||
|
||||
@@ -104,10 +107,7 @@ export function toolOutputRawEchoSignature(
|
||||
}
|
||||
|
||||
export function normalizeToolTranscriptArguments(value: unknown): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
return asNonArrayRecord(value);
|
||||
}
|
||||
|
||||
export function collectDynamicToolContentText(
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { normalizeUsage } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
asFiniteNumber,
|
||||
asSafeIntegerInRange,
|
||||
readStringField as readString,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { readNonNegativeInteger } from "./event-projector-values.js";
|
||||
import { isJsonObject, type JsonObject } from "./protocol.js";
|
||||
|
||||
function readTokenCount(record: JsonObject, key: string): number | undefined {
|
||||
const value = readNonNegativeInteger(record, key);
|
||||
return value !== undefined && Number.isSafeInteger(value) ? value : undefined;
|
||||
return asSafeIntegerInRange(record[key], { min: 0 });
|
||||
}
|
||||
|
||||
function readCodexThreadTokenUsage(params: JsonObject): ReturnType<typeof normalizeUsage> {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { asFiniteNumber, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
asFiniteNumber,
|
||||
normalizeOptionalString,
|
||||
readStringField,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { isJsonObject, type CodexThreadItem, type JsonObject, type JsonValue } from "./protocol.js";
|
||||
|
||||
export function normalizeNonEmptyString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
return value.trim() || undefined;
|
||||
}
|
||||
export { normalizeOptionalString as normalizeNonEmptyString };
|
||||
|
||||
export function readNonEmptyString(record: JsonObject, key: string): string | undefined {
|
||||
return normalizeNonEmptyString(record[key]);
|
||||
return normalizeOptionalString(record[key]);
|
||||
}
|
||||
|
||||
export function readNonEmptyStringArray(record: JsonObject, key: string): string[] {
|
||||
@@ -19,7 +18,7 @@ export function readNonEmptyStringArray(record: JsonObject, key: string): string
|
||||
}
|
||||
const entries: string[] = [];
|
||||
for (const entry of value) {
|
||||
const normalized = normalizeNonEmptyString(entry);
|
||||
const normalized = normalizeOptionalString(entry);
|
||||
if (normalized) {
|
||||
entries.push(normalized);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Lists and normalizes models exposed by the Codex app-server `model/list`
|
||||
* endpoint, including pagination and shared-client lease handling.
|
||||
*/
|
||||
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type {
|
||||
CodexAppServerAuthRequirement,
|
||||
resolveCodexAppServerAuthProfileIdForAgent,
|
||||
@@ -145,8 +145,8 @@ export function readModelListResult(value: unknown): CodexAppServerModelListResu
|
||||
}
|
||||
|
||||
function readCodexModel(value: CodexModel): CodexAppServerModel {
|
||||
const id = readNonEmptyString(value.id);
|
||||
const model = readNonEmptyString(value.model);
|
||||
const id = normalizeOptionalString(value.id);
|
||||
const model = normalizeOptionalString(value.model);
|
||||
if (!id || !model) {
|
||||
throw new Error(
|
||||
"Invalid Codex app-server model/list response: model id and name must be non-empty strings",
|
||||
@@ -155,37 +155,29 @@ function readCodexModel(value: CodexModel): CodexAppServerModel {
|
||||
return {
|
||||
id,
|
||||
model,
|
||||
...(readNonEmptyString(value.displayName)
|
||||
? { displayName: readNonEmptyString(value.displayName) }
|
||||
...(normalizeOptionalString(value.displayName)
|
||||
? { displayName: normalizeOptionalString(value.displayName) }
|
||||
: {}),
|
||||
...(readNonEmptyString(value.description)
|
||||
? { description: readNonEmptyString(value.description) }
|
||||
...(normalizeOptionalString(value.description)
|
||||
? { description: normalizeOptionalString(value.description) }
|
||||
: {}),
|
||||
hidden: value.hidden,
|
||||
isDefault: value.isDefault,
|
||||
inputModalities: value.inputModalities,
|
||||
supportedReasoningEfforts: readReasoningEfforts(value.supportedReasoningEfforts),
|
||||
...(readNonEmptyString(value.defaultReasoningEffort)
|
||||
? { defaultReasoningEffort: readNonEmptyString(value.defaultReasoningEffort) }
|
||||
...(normalizeOptionalString(value.defaultReasoningEffort)
|
||||
? { defaultReasoningEffort: normalizeOptionalString(value.defaultReasoningEffort) }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
function readReasoningEfforts(value: CodexReasoningEffortOption[]): string[] {
|
||||
const efforts = value
|
||||
.map((entry) => readNonEmptyString(entry.reasoningEffort))
|
||||
.map((entry) => normalizeOptionalString(entry.reasoningEffort))
|
||||
.filter((entry): entry is string => entry !== undefined);
|
||||
return uniqueStrings(efforts);
|
||||
}
|
||||
|
||||
function readNonEmptyString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function normalizeMaxPages(value: unknown): number {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 20;
|
||||
}
|
||||
|
||||
@@ -715,17 +715,11 @@ function mergeJsonObjects(left: JsonObject, right: JsonObject): JsonObject {
|
||||
for (const [key, value] of Object.entries(right)) {
|
||||
const existing = merged[key];
|
||||
merged[key] =
|
||||
isPlainJsonObject(existing) && isPlainJsonObject(value)
|
||||
? mergeJsonObjects(existing, value)
|
||||
: value;
|
||||
isJsonObject(existing) && isJsonObject(value) ? mergeJsonObjects(existing, value) : value;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function isPlainJsonObject(value: JsonValue | undefined): value is JsonObject {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function fingerprintJson(value: JsonValue): string {
|
||||
return crypto.createHash("sha256").update(stableStringify(value)).digest("hex");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { CodexCommandExecParams, CodexCommandExecResponse } from "./command-exec-protocol.js";
|
||||
import type {
|
||||
CodexAppInfo,
|
||||
@@ -707,7 +708,7 @@ type CodexAppServerRequestResultMap = {
|
||||
};
|
||||
|
||||
export function isJsonObject(value: unknown): value is JsonObject {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
return isRecord(value);
|
||||
}
|
||||
|
||||
export function isRpcResponse(message: RpcMessage): message is RpcResponse {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { JsonValue } from "./protocol.js";
|
||||
import { isJsonObject, type JsonValue } from "./protocol.js";
|
||||
|
||||
/** RPC error wrapper that preserves app-server error code and data. */
|
||||
export class CodexAppServerRpcError extends Error {
|
||||
@@ -36,7 +36,3 @@ function readCodexAppServerRpcReloginDetail(data: JsonValue | undefined): string
|
||||
const detail = typeof nested.detail === "string" ? nested.detail.trim() : "";
|
||||
return isRelogin && detail ? detail : undefined;
|
||||
}
|
||||
|
||||
function isJsonObject(value: unknown): value is { [key: string]: JsonValue } {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import type { AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { JsonValue } from "./protocol.js";
|
||||
import { readUpstreamUserText } from "./upstream-prompt-provenance.js";
|
||||
|
||||
@@ -18,10 +18,6 @@ type ProjectedMessageGroup = {
|
||||
bytes: number;
|
||||
};
|
||||
|
||||
function readNonEmptyString(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value.trim() || undefined : undefined;
|
||||
}
|
||||
|
||||
function readBoundedText(
|
||||
value: unknown,
|
||||
label: string,
|
||||
@@ -49,7 +45,7 @@ function responseItemBytes(item: JsonValue): number {
|
||||
}
|
||||
|
||||
function requireCallId(value: unknown): string {
|
||||
const callId = readNonEmptyString(value);
|
||||
const callId = normalizeOptionalString(value);
|
||||
if (!callId || callId.length > 256) {
|
||||
throw new Error("Codex settled-turn projection found an invalid tool call id");
|
||||
}
|
||||
@@ -57,7 +53,7 @@ function requireCallId(value: unknown): string {
|
||||
}
|
||||
|
||||
function requireToolName(value: unknown): string {
|
||||
const name = readNonEmptyString(value);
|
||||
const name = normalizeOptionalString(value);
|
||||
if (!name || !TOOL_NAME_PATTERN.test(name)) {
|
||||
throw new Error("Codex settled-turn projection found an invalid tool name");
|
||||
}
|
||||
@@ -207,7 +203,7 @@ function projectToolResult(message: Record<string, unknown>): {
|
||||
throw new Error("Codex settled-turn projection found malformed tool result content");
|
||||
}
|
||||
if (value.type === "image") {
|
||||
const mimeType = readNonEmptyString(value.mimeType) ?? "unknown type";
|
||||
const mimeType = normalizeOptionalString(value.mimeType) ?? "unknown type";
|
||||
// The finalizer selects by text capability. Preserve image evidence as
|
||||
// metadata without embedding an executable or oversized multimodal payload.
|
||||
parts.push(`[Image tool result: ${mimeType}]`);
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
isActiveHarnessContextEngine,
|
||||
type EmbeddedRunAttemptParamsV2 as EmbeddedRunAttemptParams,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
resolveCodexContextEngineProjectionMaxChars,
|
||||
resolveCodexContextEngineProjectionReserveTokens,
|
||||
@@ -88,16 +89,12 @@ function areContextEngineProjectionBindingsCompatible(
|
||||
}
|
||||
|
||||
function resolveContextEngineCitationsMode(config: unknown): JsonValue | undefined {
|
||||
const rootConfig = isUnknownRecord(config) ? config : undefined;
|
||||
const memoryConfig = isUnknownRecord(rootConfig?.memory) ? rootConfig.memory : undefined;
|
||||
const rootConfig = isRecord(config) ? config : undefined;
|
||||
const memoryConfig = isRecord(rootConfig?.memory) ? rootConfig.memory : undefined;
|
||||
const citations = memoryConfig?.citations;
|
||||
return isJsonConfigValue(citations) ? citations : undefined;
|
||||
}
|
||||
|
||||
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function isJsonConfigValue(value: unknown): value is JsonValue {
|
||||
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
||||
return true;
|
||||
@@ -108,5 +105,5 @@ function isJsonConfigValue(value: unknown): value is JsonValue {
|
||||
if (Array.isArray(value)) {
|
||||
return value.every(isJsonConfigValue);
|
||||
}
|
||||
return isUnknownRecord(value) && Object.values(value).every(isJsonConfigValue);
|
||||
return isRecord(value) && Object.values(value).every(isJsonConfigValue);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
deleteSessionUpstreamLink,
|
||||
upsertSessionUpstreamLink,
|
||||
} from "openclaw/plugin-sdk/session-catalog";
|
||||
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { isIncognitoSessionKey } from "../incognito-session.js";
|
||||
import type { CodexSessionCatalogControl } from "../session-catalog-types.js";
|
||||
import { codexLastTerminalTurnId, codexUpstreamBaseline } from "../session-upstream-marker.js";
|
||||
@@ -32,7 +32,7 @@ function readConnectionFingerprint(ref: unknown): string | undefined {
|
||||
}
|
||||
|
||||
function normalizeTurnId(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
return normalizeOptionalString(value);
|
||||
}
|
||||
|
||||
export async function forkCodexUpstreamSession(
|
||||
|
||||
@@ -12,7 +12,11 @@ import {
|
||||
ModelSelectionLockedError,
|
||||
} from "openclaw/plugin-sdk/model-session-runtime";
|
||||
import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { asBoolean, asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
asBoolean,
|
||||
asOptionalRecord,
|
||||
asSafeIntegerInRange,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { Type } from "typebox";
|
||||
import { resolveCodexBindingAppServerConnection } from "./app-server/binding-connection.js";
|
||||
import { CODEX_CONTROL_METHODS } from "./app-server/capabilities.js";
|
||||
@@ -112,12 +116,6 @@ type CodexThreadsToolOptions = {
|
||||
request?: typeof codexControlRequest;
|
||||
};
|
||||
|
||||
function readLimit(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 100
|
||||
? value
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function resolveToolSession(
|
||||
context: OpenClawPluginToolContext,
|
||||
runtime: PluginRuntime,
|
||||
@@ -291,7 +289,7 @@ export function createCodexThreadsTool(options: CodexThreadsToolOptions): AnyAge
|
||||
CODEX_CONTROL_METHODS.listThreads,
|
||||
{
|
||||
archived: asBoolean(params.archived) ?? false,
|
||||
limit: readLimit(params.limit) ?? 20,
|
||||
limit: asSafeIntegerInRange(params.limit, { min: 1, max: 100 }) ?? 20,
|
||||
modelProviders: [],
|
||||
sortKey: "recency_at",
|
||||
sortDirection: "desc",
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { readNonEmptyStringPreservingWhitespace as normalizeTurnId } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { CodexThread } from "./app-server/protocol.js";
|
||||
import { codexUpstreamBaseline } from "./session-upstream-marker.js";
|
||||
|
||||
const normalizeTurnId = (value: unknown) =>
|
||||
typeof value === "string" && value ? value : undefined;
|
||||
|
||||
describe("codexUpstreamBaseline", () => {
|
||||
it("baselines an active adoption-time turn including its current user items", () => {
|
||||
const thread = {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
wrapWebContent,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-contract";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
runBoundedCodexAppServerTurn,
|
||||
type CodexBoundedTurnOptions,
|
||||
@@ -66,7 +67,7 @@ function summarizeCodexWebSearchItem(item: CodexThreadItem): Record<string, unkn
|
||||
const actionType = readNonEmptyString(action, "type");
|
||||
const queries = actionType === "search" ? readNonEmptyStringArray(action, "queries") : [];
|
||||
const query =
|
||||
normalizeNonEmptyString(item.query) ??
|
||||
normalizeOptionalString(item.query) ??
|
||||
(actionType === "search" ? readNonEmptyString(action, "query") : undefined) ??
|
||||
queries[0];
|
||||
const url = readNonEmptyString(action, "url");
|
||||
@@ -81,7 +82,7 @@ function summarizeCodexWebSearchItem(item: CodexThreadItem): Record<string, unkn
|
||||
}
|
||||
|
||||
function readNonEmptyString(record: JsonObject | undefined, key: string): string | undefined {
|
||||
return record ? normalizeNonEmptyString(record[key]) : undefined;
|
||||
return record ? normalizeOptionalString(record[key]) : undefined;
|
||||
}
|
||||
|
||||
function readNonEmptyStringArray(record: JsonObject | undefined, key: string): string[] {
|
||||
@@ -90,11 +91,7 @@ function readNonEmptyStringArray(record: JsonObject | undefined, key: string): s
|
||||
return [];
|
||||
}
|
||||
return value.flatMap((entry) => {
|
||||
const normalized = normalizeNonEmptyString(entry);
|
||||
const normalized = normalizeOptionalString(entry);
|
||||
return normalized ? [normalized] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeNonEmptyString(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value.trim() || undefined : undefined;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
// - `src/agents/pi-embedded-runner/run/types.ts` —
|
||||
// `AgentHarnessAttemptResult.replayMetadata` field requirement.
|
||||
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
type ReplayDecision =
|
||||
| {
|
||||
readonly action: "resume";
|
||||
@@ -38,11 +40,7 @@ interface ReplayShimInput {
|
||||
}
|
||||
|
||||
function normalizeSdkSessionId(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
return normalizeOptionalString(value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
normalizeDiagnosticValue,
|
||||
normalizeDiagnosticLane,
|
||||
} from "openclaw/plugin-sdk/diagnostic-runtime";
|
||||
import { asNonNegativeFiniteNumber as numericValue } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import type {
|
||||
DiagnosticEventMetadata,
|
||||
@@ -56,10 +57,6 @@ const RATIO_BUCKETS = [0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 1, 2, 4, 8, 16];
|
||||
const MAX_PROMETHEUS_SERIES = 2048;
|
||||
const DROPPED_SERIES_COUNTER_NAME = "openclaw_prometheus_series_dropped_total";
|
||||
|
||||
function numericValue(value: number | undefined): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function seconds(ms: number | undefined): number | undefined {
|
||||
const value = numericValue(ms);
|
||||
return value === undefined ? undefined : value / 1000;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/proxy-capture";
|
||||
import { danger, warn } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { asFiniteNumber } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import * as ws from "ws";
|
||||
import * as discordGateway from "../internal/gateway.js";
|
||||
@@ -78,8 +79,7 @@ function readStringProperty(value: object, key: string): string | undefined {
|
||||
}
|
||||
|
||||
function readNumberProperty(value: object, key: string): number | undefined {
|
||||
const property = (value as Record<string, unknown>)[key];
|
||||
return typeof property === "number" && Number.isFinite(property) ? property : undefined;
|
||||
return asFiniteNumber((value as Record<string, unknown>)[key]);
|
||||
}
|
||||
|
||||
function describeDiscordGatewayTransportError(error: Error): DiscordGatewayTransportErrorDetails {
|
||||
|
||||
@@ -33,17 +33,6 @@ vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
|
||||
danger: (value: string) => value,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/string-coerce-runtime", () => {
|
||||
const normalizeMockOptionalString = (value: string | null | undefined) => {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = value.trim();
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
};
|
||||
return { normalizeOptionalString: normalizeMockOptionalString };
|
||||
});
|
||||
|
||||
vi.mock("../proxy-request-client.js", () => ({
|
||||
DISCORD_REST_TIMEOUT_MS: 15_000,
|
||||
createDiscordRequestClient: vi.fn(() => ({
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Feishu plugin module implements conversation id behavior.
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString as normalizeText,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
export type FeishuGroupSessionScope =
|
||||
| "group"
|
||||
@@ -26,14 +29,6 @@ export function resolveConfiguredFeishuGroupSessionScope(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeText(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
export function buildFeishuConversationId(params: {
|
||||
chatId: string;
|
||||
scope: FeishuGroupSessionScope;
|
||||
|
||||
@@ -3,6 +3,7 @@ import crypto from "node:crypto";
|
||||
import * as http from "node:http";
|
||||
import * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import { channelBlockedPatch, channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
|
||||
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { waitForAbortableDelay } from "./async.js";
|
||||
import { createFeishuWSClient } from "./client.js";
|
||||
@@ -56,7 +57,7 @@ const FEISHU_WS_AUTORECONNECT_DISABLED_ERROR =
|
||||
"WebSocket connect failed and autoReconnect is disabled";
|
||||
|
||||
function isFeishuWebhookPayload(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
return isRecord(value);
|
||||
}
|
||||
|
||||
const BLOCKED_FEISHU_WEBHOOK_PAYLOAD_KEYS = new Set([
|
||||
|
||||
@@ -50,6 +50,7 @@ import path from "node:path";
|
||||
import { minimatch } from "minimatch";
|
||||
import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
|
||||
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
||||
import { asNullableRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
export type FilePolicyKind = "read" | "write";
|
||||
type FilePolicyAskMode = "off" | "on-miss" | "always";
|
||||
@@ -85,10 +86,7 @@ type NodeFilePolicyConfig = {
|
||||
type FilePolicyConfig = Record<string, NodeFilePolicyConfig>;
|
||||
|
||||
function asFilePolicyConfig(value: unknown): FilePolicyConfig | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return value as FilePolicyConfig;
|
||||
return asNullableRecord(value) as FilePolicyConfig | null;
|
||||
}
|
||||
|
||||
function readFilePolicyConfigFromPluginConfig(pluginConfig: unknown): FilePolicyConfig | null {
|
||||
|
||||
@@ -15,7 +15,11 @@ import {
|
||||
resolveSpeechProviderApiKey,
|
||||
trimToUndefined,
|
||||
} from "openclaw/plugin-sdk/speech-core";
|
||||
import { asFiniteNumberInRange, asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
asFiniteNumberInRange,
|
||||
asOptionalRecord,
|
||||
parseBooleanValue,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
FISH_AUDIO_STREAM_MAX_BYTES,
|
||||
type FishAudioFormat,
|
||||
@@ -207,12 +211,9 @@ function parseDirectiveToken(ctx: SpeechDirectiveTokenParseContext) {
|
||||
if (!ctx.policy.allowNormalization) {
|
||||
return { handled: true };
|
||||
}
|
||||
const value = ctx.value.trim().toLowerCase();
|
||||
if (["true", "1", "yes", "on"].includes(value)) {
|
||||
return { handled: true, overrides: { ...ctx.currentOverrides, normalize: true } };
|
||||
}
|
||||
if (["false", "0", "no", "off"].includes(value)) {
|
||||
return { handled: true, overrides: { ...ctx.currentOverrides, normalize: false } };
|
||||
const normalize = parseBooleanValue(ctx.value);
|
||||
if (normalize !== undefined) {
|
||||
return { handled: true, overrides: { ...ctx.currentOverrides, normalize } };
|
||||
}
|
||||
return { handled: true, warnings: [`invalid Fish Audio normalize "${ctx.value}"`] };
|
||||
}
|
||||
|
||||
@@ -7,13 +7,15 @@ const coerceSecretRefMock = vi.hoisted(() => vi.fn());
|
||||
const resolveConfiguredSecretInputWithFallbackMock = vi.hoisted(() => vi.fn());
|
||||
const resolveRequiredConfiguredSecretRefInputStringMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
|
||||
coerceSecretRef: coerceSecretRefMock,
|
||||
ensureAuthProfileStore: ensureAuthProfileStoreMock,
|
||||
listProfilesForProvider: listProfilesForProviderMock,
|
||||
normalizeOptionalSecretInput: (value: unknown) =>
|
||||
typeof value === "string" && value.trim() ? value.trim() : undefined,
|
||||
}));
|
||||
vi.mock("openclaw/plugin-sdk/provider-auth", async () => {
|
||||
const { normalizeOptionalString } = await import("openclaw/plugin-sdk/string-coerce-runtime");
|
||||
return {
|
||||
coerceSecretRef: coerceSecretRefMock,
|
||||
ensureAuthProfileStore: ensureAuthProfileStoreMock,
|
||||
listProfilesForProvider: listProfilesForProviderMock,
|
||||
normalizeOptionalSecretInput: normalizeOptionalString,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/secret-input-runtime", () => ({
|
||||
resolveConfiguredSecretInputWithFallback: resolveConfiguredSecretInputWithFallbackMock,
|
||||
|
||||
@@ -6,7 +6,10 @@ import { normalizeAccountId, type OpenClawConfig } from "openclaw/plugin-sdk/acc
|
||||
// Imessage plugin module implements accounts behavior.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
asOptionalRecord,
|
||||
normalizeOptionalString,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { IMessageAccountConfig } from "./account-types.js";
|
||||
import {
|
||||
expandIMessageUserPath,
|
||||
@@ -45,9 +48,7 @@ function resolveIMessageAccountConfig(
|
||||
type IMessageStreamingConfig = NonNullable<IMessageAccountConfig["streaming"]>;
|
||||
|
||||
function asStreamingConfigObject(value: unknown): IMessageStreamingConfig | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as IMessageStreamingConfig)
|
||||
: undefined;
|
||||
return asOptionalRecord(value) as IMessageStreamingConfig | undefined;
|
||||
}
|
||||
|
||||
function mergeIMessageStreamingConfig(
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
parseStrictInteger,
|
||||
resolveExpiresAtMsFromDurationMs,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import { normalizeOptionalString as stringFromUnknown } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { IMessageActionTransportOptions } from "./actions-rpc.js";
|
||||
import { normalizeDirectChatIdentifier } from "./chat-context.js";
|
||||
import { createIMessageRpcClient } from "./client.js";
|
||||
@@ -41,10 +42,6 @@ function numberFromUnknown(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : parseStrictInteger(value);
|
||||
}
|
||||
|
||||
function stringFromUnknown(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function chatListCacheKey(options: IMessageActionTransportOptions): string {
|
||||
return `${options.cliPath}\0${options.dbPath ?? ""}\0${options.remoteHost ?? ""}`;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import {
|
||||
asDateTimestampMs,
|
||||
asPositiveFiniteNumber,
|
||||
resolveExpiresAtMsFromDurationMs,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import type { IMessageApprovalGatewayRuntime } from "./approval-gateway-types.js";
|
||||
@@ -38,7 +39,7 @@ type HistoryMessage = IMessagePayload & {
|
||||
};
|
||||
|
||||
function normalizeChatId(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
||||
return asPositiveFiniteNumber(value) ?? null;
|
||||
}
|
||||
|
||||
function listTargetChatIds(
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
formatInboundEnvelope,
|
||||
type resolveEnvelopeFormatOptions,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { parseDateStringTimestampMs } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { IMessageRpcClient } from "../client.js";
|
||||
import { normalizeIMessageHandle } from "../targets.js";
|
||||
@@ -87,8 +88,7 @@ function historyEntryFromMessage(message: IMessagePayload, fallbackSender: strin
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
const timestamp =
|
||||
typeof message.created_at === "string" ? Date.parse(message.created_at) : Number.NaN;
|
||||
const timestamp = parseDateStringTimestampMs(message.created_at);
|
||||
return {
|
||||
sender:
|
||||
message.is_from_me === true
|
||||
@@ -96,7 +96,7 @@ function historyEntryFromMessage(message: IMessagePayload, fallbackSender: strin
|
||||
: normalizeIMessageHandle(normalizeOptionalString(message.sender) ?? fallbackSender) ||
|
||||
fallbackSender,
|
||||
body,
|
||||
...(Number.isFinite(timestamp) ? { timestamp } : {}),
|
||||
...(timestamp !== undefined ? { timestamp } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
|
||||
import { collectErrorGraphCandidates, formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { asSafeIntegerInRange } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { getIMessageRuntime } from "../runtime.js";
|
||||
import { parseIMessageNotification } from "./parse-notification.js";
|
||||
import type { IMessagePayload } from "./types.js";
|
||||
@@ -62,8 +63,7 @@ function rawMessageRecord(raw: unknown): Record<string, unknown> | null {
|
||||
}
|
||||
|
||||
function rawRowid(raw: unknown): number | null {
|
||||
const rowid = rawMessageRecord(raw)?.id;
|
||||
return typeof rowid === "number" && Number.isSafeInteger(rowid) && rowid >= 0 ? rowid : null;
|
||||
return asSafeIntegerInRange(rawMessageRecord(raw)?.id, { min: 0 }) ?? null;
|
||||
}
|
||||
|
||||
/** Read only stable transport metadata; payload normalization waits for dispatch. */
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
||||
import { sleep as delay } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime";
|
||||
import { normalizeOptionalString as stringValue } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { resolvePreferredOpenClawTmpDir, withTempWorkspace } from "openclaw/plugin-sdk/temp-path";
|
||||
import { convertMarkdownTables } from "openclaw/plugin-sdk/text-chunking";
|
||||
import { stripInlineDirectiveTagsForDelivery } from "openclaw/plugin-sdk/text-chunking";
|
||||
@@ -518,10 +519,6 @@ async function runIMessageCliJson(
|
||||
});
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function resultService(value: unknown): Exclude<IMessageService, "auto"> | undefined {
|
||||
const normalized = stringValue(value)?.toLowerCase();
|
||||
return normalized === "imessage" || normalized === "sms" ? normalized : undefined;
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ChannelLegacyStateMigrationPlan } from "openclaw/plugin-sdk/channe
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { fileExists } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
||||
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { asFiniteNumber, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
listIMessageAccountIds,
|
||||
resolveDefaultIMessageAccountId,
|
||||
@@ -131,7 +131,7 @@ function readReplyCounterValue(value: unknown): number | null {
|
||||
return null;
|
||||
}
|
||||
const counter = (value as { counter?: unknown }).counter;
|
||||
return typeof counter === "number" && Number.isFinite(counter) ? counter : null;
|
||||
return asFiniteNumber(counter) ?? null;
|
||||
}
|
||||
|
||||
function shouldReplaceReplyCounter(existingValue: unknown, incomingValue: unknown): boolean {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { randomUUID } from "node:crypto";
|
||||
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { runExec } from "openclaw/plugin-sdk/process-runtime";
|
||||
import { asFiniteNumber } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
|
||||
|
||||
type LogbookSnapshotParams = {
|
||||
@@ -22,11 +23,11 @@ function readParams(value: unknown): LogbookSnapshotParams {
|
||||
return {};
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
const num = (key: string) => {
|
||||
const candidate = record[key];
|
||||
return typeof candidate === "number" && Number.isFinite(candidate) ? candidate : undefined;
|
||||
return {
|
||||
screenIndex: asFiniteNumber(record.screenIndex),
|
||||
maxWidth: asFiniteNumber(record.maxWidth),
|
||||
quality: asFiniteNumber(record.quality),
|
||||
};
|
||||
return { screenIndex: num("screenIndex"), maxWidth: num("maxWidth"), quality: num("quality") };
|
||||
}
|
||||
|
||||
export async function handleLogbookSnapshot(rawParams: unknown): Promise<LogbookSnapshotPayload> {
|
||||
|
||||
@@ -17,7 +17,7 @@ function readPackageJson(packageRoot) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLowercaseStringOrEmpty(value) {
|
||||
function lowercaseStringOrEmptyWithoutTrim(value) {
|
||||
return typeof value === "string" ? value.toLowerCase() : "";
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ function hasTrustedOpenClawRootIndicator(packageRoot, packageJson) {
|
||||
const hasCliEntryExport = Object.hasOwn(packageExports, "./cli-entry");
|
||||
const hasOpenClawBin =
|
||||
(typeof packageJson?.bin === "string" &&
|
||||
normalizeLowercaseStringOrEmpty(packageJson.bin).includes("openclaw")) ||
|
||||
lowercaseStringOrEmptyWithoutTrim(packageJson.bin).includes("openclaw")) ||
|
||||
(typeof packageJson?.bin === "object" &&
|
||||
packageJson.bin !== null &&
|
||||
typeof packageJson.bin.openclaw === "string");
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
// Mattermost plugin module implements gateway auth bypass behavior.
|
||||
import {
|
||||
asOptionalRecord,
|
||||
normalizeOptionalString as readTrimmedString,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
const DEFAULT_SLASH_CALLBACK_PATH = "/api/channels/mattermost/command";
|
||||
|
||||
type MattermostSlashCommandConfigInput = {
|
||||
@@ -14,10 +19,6 @@ type MattermostConfigInput = MattermostAccountConfigInput & {
|
||||
accounts?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function readTrimmedString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function normalizeCallbackPath(value: unknown): string {
|
||||
const trimmed = readTrimmedString(value);
|
||||
if (!trimmed) {
|
||||
@@ -27,9 +28,7 @@ function normalizeCallbackPath(value: unknown): string {
|
||||
}
|
||||
|
||||
function readMattermostCommands(value: unknown): MattermostSlashCommandConfigInput | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as MattermostSlashCommandConfigInput)
|
||||
: undefined;
|
||||
return asOptionalRecord(value) as MattermostSlashCommandConfigInput | undefined;
|
||||
}
|
||||
|
||||
function isMattermostBypassPath(path: string): boolean {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Memory Core tests cover embeddings plugin behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import type { EmbeddingProviderAdapter } from "openclaw/plugin-sdk/embedding-providers";
|
||||
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import type { MemoryEmbeddingProviderAdapter } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
@@ -105,7 +106,7 @@ function createMissingCredentialsAdapter(
|
||||
id: "bedrock",
|
||||
transport: "remote",
|
||||
autoSelectPriority: 60,
|
||||
formatSetupError: (err) => (err instanceof Error ? err.message : String(err)),
|
||||
formatSetupError: coerceErrorMessage,
|
||||
shouldContinueAutoSelection: (err) =>
|
||||
err instanceof Error && err.message.includes("No API key found for provider"),
|
||||
create: async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Msteams plugin module implements bot framework behavior.
|
||||
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { parseMediaContentLength } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
|
||||
import {
|
||||
@@ -108,7 +109,7 @@ async function fetchBotFrameworkAttachmentInfo(params: {
|
||||
});
|
||||
} catch (err) {
|
||||
params.logger?.warn?.("msteams botFramework attachmentInfo fetch failed", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
@@ -126,7 +127,7 @@ async function fetchBotFrameworkAttachmentInfo(params: {
|
||||
);
|
||||
} catch (err) {
|
||||
params.logger?.warn?.("msteams botFramework attachmentInfo parse failed", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
@@ -168,7 +169,7 @@ async function saveBotFrameworkAttachmentView(params: {
|
||||
});
|
||||
} catch (err) {
|
||||
params.logger?.warn?.("msteams botFramework attachmentView fetch failed", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
@@ -185,7 +186,7 @@ async function saveBotFrameworkAttachmentView(params: {
|
||||
} catch (err) {
|
||||
await response.body?.cancel().catch(() => undefined);
|
||||
params.logger?.warn?.("msteams botFramework attachmentView invalid content-length", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
@@ -204,7 +205,7 @@ async function saveBotFrameworkAttachmentView(params: {
|
||||
});
|
||||
} catch (err) {
|
||||
params.logger?.warn?.("msteams botFramework attachmentView save failed", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
});
|
||||
return undefined;
|
||||
} finally {
|
||||
@@ -256,7 +257,7 @@ async function downloadMSTeamsBotFrameworkAttachment(params: {
|
||||
});
|
||||
} catch (err) {
|
||||
params.logger?.warn?.("msteams botFramework token acquisition failed", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
@@ -401,7 +402,7 @@ export async function downloadMSTeamsBotFrameworkAttachments(params: {
|
||||
} catch (err) {
|
||||
media.push({ kind: "document", sourceId: attachmentId });
|
||||
params.logger?.warn?.("msteams botFramework attachment download failed", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
attachmentId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Msteams plugin module implements download behavior.
|
||||
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
@@ -334,7 +335,7 @@ export async function downloadMSTeamsAttachments(params: {
|
||||
} catch (err) {
|
||||
out.push(withSourceId({ kind: candidate.mediaKind }, candidate.sourceId));
|
||||
params.logger?.warn?.("msteams inline attachment decode failed", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
@@ -370,7 +371,7 @@ export async function downloadMSTeamsAttachments(params: {
|
||||
out.push(withSourceId(media, candidate.sourceId));
|
||||
} catch (err) {
|
||||
out.push(withSourceId({ kind: candidate.mediaKind }, candidate.sourceId));
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const msg = coerceErrorMessage(err);
|
||||
params.logger?.warn?.(
|
||||
`msteams attachment download failed host=${safeHostForLog(candidate.url)} error=${msg}`,
|
||||
);
|
||||
|
||||
@@ -4,8 +4,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
// Mock shared.js to avoid transitive runtime-api imports that pull in uninstalled packages.
|
||||
vi.mock("./shared.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./shared.js")>();
|
||||
const isMockRecord = (value: unknown) =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
const { isRecord } = await import("openclaw/plugin-sdk/string-coerce-runtime");
|
||||
return {
|
||||
...actual,
|
||||
applyAuthorizationHeaderForUrl: vi.fn(),
|
||||
@@ -13,7 +12,7 @@ vi.mock("./shared.js", async (importOriginal) => {
|
||||
resolveMSTeamsMediaKind: vi.fn(({ contentType }: { contentType?: string }) =>
|
||||
contentType?.startsWith("image/") ? "image" : "document",
|
||||
),
|
||||
isRecord: isMockRecord,
|
||||
isRecord,
|
||||
isUrlAllowed: vi.fn(() => true),
|
||||
normalizeContentType: vi.fn((ct: string | null | undefined) => ct ?? undefined),
|
||||
resolveMediaSsrfPolicy: vi.fn(() => undefined),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Msteams plugin module implements graph behavior.
|
||||
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import {
|
||||
readProviderJsonArrayFieldResponse,
|
||||
readProviderJsonResponse,
|
||||
@@ -188,7 +189,7 @@ async function downloadGraphHostedContent(params: {
|
||||
})) as { status: number; items: GraphHostedContent[] };
|
||||
} catch (err) {
|
||||
params.logger?.warn?.("msteams graph hostedContents fetch failed", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
});
|
||||
return { media: [], count: 0 };
|
||||
}
|
||||
@@ -240,7 +241,7 @@ async function downloadGraphHostedContent(params: {
|
||||
} catch (err) {
|
||||
out.push(createGraphHostedContentFact(item));
|
||||
params.logger?.warn?.("msteams graph hostedContent value fetch failed", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -324,10 +325,10 @@ export async function downloadMSTeamsGraphMedia(params: {
|
||||
} catch (err) {
|
||||
params.logger?.debug?.("graph media message parse failed", {
|
||||
messageUrl,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
});
|
||||
params.logger?.warn?.("msteams graph message parse failed", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
messageUrl,
|
||||
});
|
||||
msgData = {};
|
||||
@@ -349,10 +350,10 @@ export async function downloadMSTeamsGraphMedia(params: {
|
||||
} catch (err) {
|
||||
params.logger?.debug?.("graph media message fetch failed", {
|
||||
messageUrl,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
});
|
||||
params.logger?.warn?.("msteams graph message fetch failed", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -423,7 +424,7 @@ export async function downloadMSTeamsGraphMedia(params: {
|
||||
} catch (err) {
|
||||
sharePointMedia.push(unavailableMedia);
|
||||
params.logger?.warn?.("msteams SharePoint reference download failed", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
name,
|
||||
});
|
||||
}
|
||||
@@ -472,7 +473,7 @@ export async function downloadMSTeamsGraphMedia(params: {
|
||||
});
|
||||
} catch (err) {
|
||||
params.logger?.warn?.("msteams graph attachment download failed", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: coerceErrorMessage(err),
|
||||
messageUrl,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,20 +45,22 @@ const keepHttpServerTaskAliveMock = vi.hoisted(() =>
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("../runtime-api.js", () => ({
|
||||
DEFAULT_WEBHOOK_MAX_BODY_BYTES: 1024 * 1024,
|
||||
isDangerousNameMatchingEnabled,
|
||||
normalizeSecretInputString: (value: unknown) =>
|
||||
typeof value === "string" && value.trim() ? value.trim() : undefined,
|
||||
hasConfiguredSecretInput: (value: unknown) =>
|
||||
typeof value === "string" && value.trim().length > 0,
|
||||
normalizeResolvedSecretInputString: (params: { value?: unknown }) =>
|
||||
typeof params?.value === "string" && params.value.trim() ? params.value.trim() : undefined,
|
||||
keepHttpServerTaskAlive: keepHttpServerTaskAliveMock,
|
||||
mergeAllowlist: (params: { existing?: string[]; additions: string[] }) =>
|
||||
Array.from(new Set([...(params.existing ?? []), ...params.additions])),
|
||||
summarizeMapping: vi.fn(),
|
||||
}));
|
||||
vi.mock("../runtime-api.js", async () => {
|
||||
const { normalizeOptionalString } = await import("openclaw/plugin-sdk/string-coerce-runtime");
|
||||
return {
|
||||
DEFAULT_WEBHOOK_MAX_BODY_BYTES: 1024 * 1024,
|
||||
isDangerousNameMatchingEnabled,
|
||||
normalizeSecretInputString: normalizeOptionalString,
|
||||
hasConfiguredSecretInput: (value: unknown) =>
|
||||
typeof value === "string" && value.trim().length > 0,
|
||||
normalizeResolvedSecretInputString: (params: { value?: unknown }) =>
|
||||
typeof params?.value === "string" && params.value.trim() ? params.value.trim() : undefined,
|
||||
keepHttpServerTaskAlive: keepHttpServerTaskAliveMock,
|
||||
mergeAllowlist: (params: { existing?: string[]; additions: string[] }) =>
|
||||
Array.from(new Set([...(params.existing ?? []), ...params.additions])),
|
||||
summarizeMapping: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("express", () => ({
|
||||
default: () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
||||
import { once } from "node:events";
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
|
||||
type MSTeamsQaOutboundActivity = {
|
||||
activity: Record<string, unknown>;
|
||||
@@ -74,7 +75,7 @@ export async function startMSTeamsQaBotFrameworkServer(options: ServerOptions) {
|
||||
sendJson(response, 200, { id: activityId });
|
||||
})().catch((error: unknown) => {
|
||||
sendJson(response, 500, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
error: coerceErrorMessage(error),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Msteams plugin module implements sdk behavior.
|
||||
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
||||
import { readSecretFile } from "openclaw/plugin-sdk/secret-file";
|
||||
import { normalizeBotFrameworkServiceUrl } from "./bot-framework-service-url.js";
|
||||
@@ -333,7 +334,7 @@ async function createFederatedApp(
|
||||
try {
|
||||
privateKey = await readSecretFile(creds.certificatePath, "Microsoft Teams certificate");
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const msg = coerceErrorMessage(err);
|
||||
throw new Error(`Failed to read certificate file at '${creds.certificatePath}': ${msg}`, {
|
||||
cause: err,
|
||||
});
|
||||
|
||||
@@ -26,13 +26,15 @@ vi.mock("./oauth.token.js", () => ({
|
||||
refreshMSTeamsDelegatedTokens: oauthTokenMocks.refreshMSTeamsDelegatedTokens,
|
||||
}));
|
||||
|
||||
vi.mock("./secret-input.js", () => ({
|
||||
normalizeSecretInputString: (v: unknown) =>
|
||||
typeof v === "string" && v.trim() ? v.trim() : undefined,
|
||||
normalizeResolvedSecretInputString: (opts: { value: unknown; path: string }) =>
|
||||
typeof opts.value === "string" && opts.value.trim() ? opts.value.trim() : undefined,
|
||||
hasConfiguredSecretInput: (v: unknown) => typeof v === "string" && v.trim().length > 0,
|
||||
}));
|
||||
vi.mock("./secret-input.js", async () => {
|
||||
const { normalizeOptionalString } = await import("openclaw/plugin-sdk/string-coerce-runtime");
|
||||
return {
|
||||
normalizeSecretInputString: normalizeOptionalString,
|
||||
normalizeResolvedSecretInputString: (opts: { value: unknown; path: string }) =>
|
||||
typeof opts.value === "string" && opts.value.trim() ? opts.value.trim() : undefined,
|
||||
hasConfiguredSecretInput: (v: unknown) => typeof v === "string" && v.trim().length > 0,
|
||||
};
|
||||
});
|
||||
|
||||
const ENV_KEYS = [
|
||||
"MSTEAMS_APP_ID",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
archiveLegacyStateSource,
|
||||
type PluginDoctorStateMigration,
|
||||
} from "openclaw/plugin-sdk/runtime-doctor-migrations";
|
||||
import { asFiniteNumber } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { normalizeNostrStateAccountId } from "./src/state-account-id.js";
|
||||
|
||||
type NostrBusState = {
|
||||
@@ -26,10 +27,6 @@ const BUS_STATE_NAMESPACE = "bus-state";
|
||||
const PROFILE_STATE_NAMESPACE = "profile-state";
|
||||
const MAX_NOSTR_STATE_ENTRIES = 256;
|
||||
|
||||
function finiteNumberOrNull(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function parseBusState(value: unknown): NostrBusState | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
@@ -40,8 +37,8 @@ function parseBusState(value: unknown): NostrBusState | null {
|
||||
}
|
||||
return {
|
||||
version: 2,
|
||||
lastProcessedAt: finiteNumberOrNull(parsed.lastProcessedAt),
|
||||
gatewayStartedAt: finiteNumberOrNull(parsed.gatewayStartedAt),
|
||||
lastProcessedAt: asFiniteNumber(parsed.lastProcessedAt) ?? null,
|
||||
gatewayStartedAt: asFiniteNumber(parsed.gatewayStartedAt) ?? null,
|
||||
recentEventIds:
|
||||
parsed.version === 2 && Array.isArray(parsed.recentEventIds)
|
||||
? parsed.recentEventIds.filter((entry): entry is string => typeof entry === "string")
|
||||
@@ -68,7 +65,7 @@ function parseProfileState(value: unknown): NostrProfileState | null {
|
||||
}
|
||||
return {
|
||||
version: 1,
|
||||
lastPublishedAt: finiteNumberOrNull(parsed.lastPublishedAt),
|
||||
lastPublishedAt: asFiniteNumber(parsed.lastPublishedAt) ?? null,
|
||||
lastPublishedEventId:
|
||||
typeof parsed.lastPublishedEventId === "string" ? parsed.lastPublishedEventId : null,
|
||||
lastPublishResults:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
// Ollama tests cover embedding provider plugin behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -14,7 +15,7 @@ const { fetchConfiguredLocalOriginWithSsrFGuardMock } = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
|
||||
fetchWithSsrFGuard: vi.fn(),
|
||||
formatErrorMessage: (error: unknown) => (error instanceof Error ? error.message : String(error)),
|
||||
formatErrorMessage: coerceErrorMessage,
|
||||
ssrfPolicyFromHttpBaseUrlAllowedOrigin: (baseUrl: string) => {
|
||||
const parsed = new URL(baseUrl);
|
||||
return { allowedOrigins: [parsed.origin] };
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
readResponseTextLimited,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { asNullableRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { asFiniteNumber, asNullableRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { OLLAMA_DEFAULT_BASE_URL } from "./defaults.js";
|
||||
import {
|
||||
DEFAULT_INFERENCE_TIMEOUT_MS,
|
||||
@@ -101,10 +101,6 @@ function durationMs(value: unknown): number | undefined {
|
||||
return Math.round((value / 1_000_000) * 100) / 100;
|
||||
}
|
||||
|
||||
function optionalNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
async function requestOllamaJson<T>(params: {
|
||||
baseUrl: string;
|
||||
path: string;
|
||||
@@ -300,8 +296,8 @@ async function runOllamaNodeChat(params: {
|
||||
`Ollama stopped after reaching maxTokens (${params.maxTokens}); retry with a larger maxTokens value`,
|
||||
);
|
||||
}
|
||||
const promptTokens = optionalNumber(data.prompt_eval_count);
|
||||
const completionTokens = optionalNumber(data.eval_count);
|
||||
const promptTokens = asFiniteNumber(data.prompt_eval_count);
|
||||
const completionTokens = asFiniteNumber(data.eval_count);
|
||||
const loadMs = durationMs(data.load_duration);
|
||||
const totalMs = durationMs(data.total_duration);
|
||||
return {
|
||||
|
||||
@@ -18,7 +18,11 @@ import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
||||
import { createPlainTextToolCallCompatWrapper } from "openclaw/plugin-sdk/provider-stream-shared";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { isRecord, readStringValue } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
isRecord,
|
||||
normalizeOptionalString,
|
||||
readStringValue,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { estimateStringChars, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { OLLAMA_CLOUD_BASE_URL, OLLAMA_DEFAULT_BASE_URL } from "./defaults.js";
|
||||
import { normalizeOllamaWireModelId } from "./model-id.js";
|
||||
@@ -663,7 +667,7 @@ type OllamaAssistantMessageBuildOptions = OllamaToolCallNameOptions & {
|
||||
};
|
||||
|
||||
function readOllamaToolCallId(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
||||
return normalizeOptionalString(value);
|
||||
}
|
||||
|
||||
function extractToolCalls(
|
||||
|
||||
@@ -6,6 +6,7 @@ import OpenAI from "openai";
|
||||
import type { ResolvedTtsConfig } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { AuthStorage, ModelRegistry } from "openclaw/plugin-sdk/agent-sessions";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { coerceErrorMessage as formatLiveOpenAIError } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { encodePngRgba, fillPixel } from "openclaw/plugin-sdk/media-runtime";
|
||||
import {
|
||||
registerProviderPlugin,
|
||||
@@ -81,10 +82,6 @@ function createReferencePng(): Buffer {
|
||||
return encodePngRgba(buf, width, height);
|
||||
}
|
||||
|
||||
function formatLiveOpenAIError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function resolveLiveOpenAISkipReason(error: unknown): string | null {
|
||||
const message = formatLiveOpenAIError(error);
|
||||
if (isTimeoutErrorMessage(message) || /timed out|operation was aborted/i.test(message)) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
ProviderResponseModelEquivalenceContext,
|
||||
ProviderResolveModelRoutesContext,
|
||||
} from "openclaw/plugin-sdk/provider-model-types";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
classifyOpenAIBaseUrl,
|
||||
isOpenAICodexBaseUrl,
|
||||
@@ -44,11 +45,7 @@ type OpenAIResolveSingleModelRouteContext = Omit<
|
||||
};
|
||||
|
||||
function normalizeOptionalRouteApi(value: ModelApi | null | undefined): ModelApi | undefined {
|
||||
return typeof value === "string" && value.trim() ? (value.trim() as ModelApi) : undefined;
|
||||
}
|
||||
|
||||
function normalizeOptionalRouteBaseUrl(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
return normalizeOptionalString(value) as ModelApi | undefined;
|
||||
}
|
||||
|
||||
/** Canonical logical id for OpenAI catalog projection. */
|
||||
@@ -138,7 +135,7 @@ function firstRouteBaseUrl(...values: unknown[]): unknown {
|
||||
}
|
||||
|
||||
function concreteBaseUrl(value: unknown, fallback: string): string {
|
||||
return normalizeOptionalRouteBaseUrl(value) ?? fallback;
|
||||
return normalizeOptionalString(value) ?? fallback;
|
||||
}
|
||||
|
||||
function resolveOpenAIEnvironmentBaseUrl(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Openai tests cover speech provider plugin behavior.
|
||||
import { createServer } from "node:http";
|
||||
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildOpenAISpeechProvider } from "./speech-provider.js";
|
||||
|
||||
@@ -26,7 +27,7 @@ function isSpeechRequestBody(value: unknown): value is {
|
||||
speed?: number;
|
||||
response_format?: string;
|
||||
} {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
return isRecord(value);
|
||||
}
|
||||
|
||||
function parseRequestBody(init: RequestInit | undefined): {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
HealthRepairResult,
|
||||
OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/health";
|
||||
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { CHECK_IDS, type POLICY_CHECK_IDS } from "./check-ids.js";
|
||||
import { POLICY_FIX_METADATA_BY_CHECK_ID } from "./fix-metadata.js";
|
||||
|
||||
@@ -447,7 +448,3 @@ function ensureRecord(parent: ConfigRecord, key: string): ConfigRecord {
|
||||
parent[key] = next;
|
||||
return next;
|
||||
}
|
||||
|
||||
function uniqueStrings(values: readonly string[]): readonly string[] {
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
HealthRepairEffect,
|
||||
HealthRepairResult,
|
||||
} from "openclaw/plugin-sdk/health";
|
||||
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { CHECK_IDS, type POLICY_CHECK_IDS } from "./check-ids.js";
|
||||
import { POLICY_FIX_METADATA_BY_CHECK_ID } from "./fix-metadata.js";
|
||||
|
||||
@@ -121,10 +122,6 @@ function previewGatewayNodeDenyCommand(
|
||||
];
|
||||
}
|
||||
|
||||
function uniqueStrings(values: readonly string[]): readonly string[] {
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
function uniqueEffects(values: readonly HealthRepairEffect[]): readonly HealthRepairEffect[] {
|
||||
const seen = new Set<string>();
|
||||
return values.filter((value) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@openclaw/crabline";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { parseBooleanValue, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
buildQaAgenticParityComparison,
|
||||
buildQaRuntimeParityReport,
|
||||
@@ -238,20 +238,11 @@ function parseQaModelThinkingOverrides(entries: readonly string[] | undefined) {
|
||||
}
|
||||
|
||||
function parseQaBooleanModelOption(label: string, value: string) {
|
||||
switch (value.trim().toLowerCase()) {
|
||||
case "1":
|
||||
case "on":
|
||||
case "true":
|
||||
case "yes":
|
||||
return true;
|
||||
case "0":
|
||||
case "false":
|
||||
case "no":
|
||||
case "off":
|
||||
return false;
|
||||
default:
|
||||
throw new Error(`${label} fast must be one of true, false, on, off, yes, no, 1, 0`);
|
||||
const parsed = parseBooleanValue(value);
|
||||
if (parsed === undefined) {
|
||||
throw new Error(`${label} fast must be one of true, false, on, off, yes, no, 1, 0`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseQaPositiveIntegerOption(label: string, value: number | undefined) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Qa Lab plugin module implements coverage report behavior.
|
||||
import { normalizeStringEntriesLower } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
normalizeOptionalString as stringifyConfigValue,
|
||||
normalizeStringEntriesLower,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { QaSeedScenarioWithSource } from "./scenario-catalog.js";
|
||||
import {
|
||||
readQaScorecardTaxonomyReport,
|
||||
@@ -134,10 +137,6 @@ function scenarioSearchText(scenario: QaSeedScenarioWithSource) {
|
||||
);
|
||||
}
|
||||
|
||||
function stringifyConfigValue(value: unknown) {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function summarizeScenarioSearchMatch(scenario: QaSeedScenarioWithSource): QaScenarioSearchMatch {
|
||||
const config = scenario.execution.config ?? {};
|
||||
return {
|
||||
|
||||
@@ -287,7 +287,7 @@ function resolveMatrixQaStreamingMode(
|
||||
function isMatrixQaStreamingConfig(
|
||||
value: MatrixQaConfigOverrides["streaming"],
|
||||
): value is MatrixQaStreamingConfig {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
return isRecord(value);
|
||||
}
|
||||
|
||||
function resolveMatrixQaStreamingPreviewToolProgress(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// QA Lab Slack credentials, instrumentation, and channel config.
|
||||
import type { WebClient } from "@slack/web-api";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { asNonArrayRecord, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
type SlackQaRuntimeEnv,
|
||||
type SlackQaConfigOverrides,
|
||||
@@ -52,9 +52,7 @@ export function parseSlackQaCredentialPayload(payload: unknown): SlackQaRuntimeE
|
||||
}
|
||||
|
||||
export function asPlainRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
return asNonArrayRecord(value);
|
||||
}
|
||||
|
||||
type SlackQaPostMessageAttempt = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createQaBusState } from "./bus-state.js";
|
||||
import { runLoadedScenarioFlow } from "./scenario-flow-runner.test-support.js";
|
||||
@@ -168,8 +169,7 @@ async function runSessionMemoryRankingFlow(params: {
|
||||
seedQaSessionTranscript: async () => undefined,
|
||||
forceMemoryIndex,
|
||||
runAgentPrompt,
|
||||
normalizeLowercaseStringOrEmpty: (value: unknown) =>
|
||||
typeof value === "string" ? value.trim().toLowerCase() : "",
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
fetchJson,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createQaBusState } from "./bus-state.js";
|
||||
import { runLoadedScenarioFlow } from "./scenario-flow-runner.test-support.js";
|
||||
@@ -19,8 +20,7 @@ async function runMemoryRecallScenario(recallReply?: string) {
|
||||
fs: { rm: async () => undefined },
|
||||
path,
|
||||
formatMemoryDreamingDay: () => "2026-08-05",
|
||||
normalizeLowercaseStringOrEmpty: (value: unknown) =>
|
||||
typeof value === "string" ? value.trim().toLowerCase() : "",
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
runAgentPrompt: async (_env: unknown, params: { message: string }) => {
|
||||
turnCount += 1;
|
||||
state.addInboundMessage({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createQaBusState } from "./bus-state.js";
|
||||
import { runLoadedScenarioFlow } from "./scenario-flow-runner.test-support.js";
|
||||
@@ -118,8 +119,7 @@ async function runFollowUp(params?: {
|
||||
runAgentPrompt,
|
||||
splitModelRef,
|
||||
normalizeModelRef,
|
||||
normalizeLowercaseStringOrEmpty: (value: unknown) =>
|
||||
typeof value === "string" ? value.trim().toLowerCase() : "",
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
resolveQaLiveTurnTimeoutMs: (_env: unknown, timeoutMs: number) => timeoutMs,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createQaBusState } from "./bus-state.js";
|
||||
import { hasModelSwitchContinuitySignal } from "./model-switch-eval.js";
|
||||
@@ -103,8 +104,7 @@ async function runToolContinuity(
|
||||
},
|
||||
splitModelRef,
|
||||
normalizeModelRef,
|
||||
normalizeLowercaseStringOrEmpty: (value: unknown) =>
|
||||
typeof value === "string" ? value.trim().toLowerCase() : "",
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
resolveQaLiveTurnTimeoutMs: (_env: unknown, timeoutMs: number) => timeoutMs,
|
||||
hasModelSwitchContinuitySignal,
|
||||
runAgentPrompt,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { join } from "node:path";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createQaBusState } from "./bus-state.js";
|
||||
import { readQaScenarioById } from "./scenario-catalog.js";
|
||||
@@ -44,8 +45,7 @@ function createCharacterScenarioApi(
|
||||
writeFile: async () => undefined,
|
||||
},
|
||||
path: { join },
|
||||
normalizeLowercaseStringOrEmpty: (value: unknown) =>
|
||||
typeof value === "string" ? value.trim().toLowerCase() : "",
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
resolveQaLiveTurnTimeoutMs: () => 10,
|
||||
waitForOutboundMessage: async (
|
||||
state: ReturnType<typeof createQaBusState>,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// Qa Lab tests cover scenario flow runner plugin behavior.
|
||||
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createQaBusState } from "./bus-state.js";
|
||||
import { QaSuiteScenarioSkipError } from "./errors.js";
|
||||
@@ -60,10 +62,8 @@ async function runWebchatTranscriptWait(
|
||||
}
|
||||
throw new Error("test condition was not met");
|
||||
},
|
||||
normalizeLowercaseStringOrEmpty: (value: unknown) =>
|
||||
typeof value === "string" ? value.trim().toLowerCase() : "",
|
||||
formatErrorMessage: (error: unknown) =>
|
||||
error instanceof Error ? error.message : String(error),
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
formatErrorMessage: coerceErrorMessage,
|
||||
liveTurnTimeoutMs: (_env: unknown, timeoutMs: number) => timeoutMs,
|
||||
},
|
||||
});
|
||||
@@ -284,8 +284,7 @@ function runPlanningEvidenceFixture(
|
||||
return summary;
|
||||
},
|
||||
resolveQaLiveTurnTimeoutMs: (_env: unknown, timeoutMs: number) => timeoutMs,
|
||||
normalizeLowercaseStringOrEmpty: (value: unknown) =>
|
||||
typeof value === "string" ? value.trim().toLowerCase() : "",
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
runAgentPrompt: async () => ({ started: { runId: "current-run" }, waited: { status: "ok" } }),
|
||||
},
|
||||
});
|
||||
@@ -397,8 +396,7 @@ describe("scenario-flow-runner", () => {
|
||||
throw new Error("goal artifact has not been written");
|
||||
},
|
||||
},
|
||||
normalizeLowercaseStringOrEmpty: (value: unknown) =>
|
||||
typeof value === "string" ? value.trim().toLowerCase() : "",
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
},
|
||||
onWaitForOutboundMessage: ({ waitCount, state: currentState }) => {
|
||||
const currentInbound = currentState
|
||||
@@ -562,8 +560,7 @@ describe("scenario-flow-runner", () => {
|
||||
runLoadedScenarioFlow(id, {
|
||||
state,
|
||||
api: {
|
||||
normalizeLowercaseStringOrEmpty: (value: unknown) =>
|
||||
typeof value === "string" ? value.trim().toLowerCase() : "",
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
runAgentPrompt: async () => {
|
||||
turnCount += 1;
|
||||
state.addOutboundMessage({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Qa Lab plugin module implements suite summary behavior.
|
||||
import fs from "node:fs/promises";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { asSafeIntegerInRange, isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { QaSuiteArtifactError } from "./errors.js";
|
||||
import type { QaEvidenceSummaryJson, QaEvidenceTiming } from "./evidence-summary.js";
|
||||
import type { QaProviderMode } from "./model-selection.js";
|
||||
@@ -110,7 +110,7 @@ async function readQaSuiteSummaryFile(summaryPath: string): Promise<unknown> {
|
||||
}
|
||||
|
||||
function readNonNegativeCount(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
||||
return asSafeIntegerInRange(value, { min: 0 }) ?? null;
|
||||
}
|
||||
|
||||
function assertQaSuiteSummaryHasExecutedScenarios(
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
normalizeOpenAICompatibleReasoningReplay,
|
||||
setQwenChatTemplateThinking,
|
||||
} from "openclaw/plugin-sdk/provider-stream-shared";
|
||||
import { asOptionalRecord as asPayloadRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
isQwenTokenPlanDeepSeekV4ModelId,
|
||||
isQwenTokenPlanGlmModelId,
|
||||
@@ -26,12 +27,6 @@ type QwenTokenPlanThinkingContract =
|
||||
| { family: "kimi" }
|
||||
| { family: "glm"; supportsMax: boolean };
|
||||
|
||||
function asPayloadRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function resolveQwenThinkingLevel(
|
||||
thinkingLevel: QwenThinkingLevel,
|
||||
options: Parameters<StreamFn>[2],
|
||||
|
||||
@@ -7,8 +7,11 @@ import {
|
||||
type ChannelIngressMonitorLifecycle,
|
||||
} from "openclaw/plugin-sdk/channel-outbound";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { normalizeNullableString as normalizeRawString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
asPositiveSafeInteger,
|
||||
isRecord,
|
||||
normalizeNullableString as normalizeRawString,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { SignalSseEvent } from "./client-adapter.js";
|
||||
import { getOptionalSignalRuntime } from "./runtime.js";
|
||||
|
||||
@@ -52,7 +55,7 @@ const SignalIngressPermanentError = createChannelIngressError<
|
||||
>("SignalIngressPermanentError", { withReason: true });
|
||||
|
||||
function normalizeTimestamp(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
|
||||
return asPositiveSafeInteger(value) ?? null;
|
||||
}
|
||||
|
||||
function parseReceiveEnvelope(event: SignalSseEvent): SignalIngressEnvelope | null {
|
||||
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
type ChannelDmPolicy,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
asOptionalRecord,
|
||||
normalizeOptionalString,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { SlackAccountSurfaceFields } from "./account-surface-fields.js";
|
||||
import type { SlackAccountConfig } from "./runtime-api.js";
|
||||
import { resolveSlackAppToken, resolveSlackBotToken, resolveSlackUserToken } from "./token.js";
|
||||
@@ -123,9 +126,7 @@ type SlackStreamingConfig = NonNullable<SlackAccountConfig["streaming"]>;
|
||||
type SlackStreamingConfigValue = SlackStreamingConfig | boolean | string;
|
||||
|
||||
function asStreamingConfigObject(value: unknown): SlackStreamingConfig | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as SlackStreamingConfig)
|
||||
: undefined;
|
||||
return asOptionalRecord(value) as SlackStreamingConfig | undefined;
|
||||
}
|
||||
|
||||
function asLegacyStreamingScalar(value: unknown): boolean | string | undefined {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
timestampMsToIsoString,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import {
|
||||
asOptionalRecord,
|
||||
normalizeOptionalString,
|
||||
normalizeUniqueTrimmedStringList,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
@@ -189,13 +190,6 @@ function summarizeRichTextPreview(value: unknown): string | undefined {
|
||||
return joined.length <= max ? joined : truncateSlackText(joined, max);
|
||||
}
|
||||
|
||||
function readInteractionAction(raw: unknown) {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
return raw as Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function summarizeAction(action: Record<string, unknown>): SlackActionSummary {
|
||||
const typed = action as {
|
||||
type?: string;
|
||||
@@ -431,7 +425,7 @@ function parseSlackBlockAction(params: {
|
||||
log?: (message: string) => void;
|
||||
}): ParsedSlackBlockAction | null {
|
||||
const typedBody = params.body as SlackBlockActionBody;
|
||||
const typedAction = readInteractionAction(params.action);
|
||||
const typedAction = asOptionalRecord(params.action);
|
||||
if (!typedAction) {
|
||||
params.log?.(
|
||||
`slack:interaction malformed action payload channel=${typedBody.channel?.id ?? typedBody.container?.channel_id ?? "unknown"} user=${
|
||||
|
||||
@@ -871,13 +871,14 @@ vi.mock("openclaw/plugin-sdk/security-runtime", () => ({
|
||||
resolvePinnedMainDmOwnerFromAllowlist: () => mockedPinnedMainDmOwner,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/string-coerce-runtime", () => {
|
||||
const isMockRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
vi.mock("openclaw/plugin-sdk/string-coerce-runtime", async (importOriginal) => {
|
||||
const { asOptionalRecord, isRecord } =
|
||||
await importOriginal<typeof import("openclaw/plugin-sdk/string-coerce-runtime")>();
|
||||
const normalizeMockLowercaseString = (value?: string) => value?.toLowerCase();
|
||||
const readMockOptionalString = (value?: string) => value;
|
||||
return {
|
||||
isRecord: isMockRecord,
|
||||
asOptionalRecord,
|
||||
isRecord,
|
||||
normalizeOptionalLowercaseString: normalizeMockLowercaseString,
|
||||
normalizeOptionalString: readMockOptionalString,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { ClientRequest, IncomingMessage, RequestOptions } from "node:http";
|
||||
import { PassThrough } from "node:stream";
|
||||
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest";
|
||||
|
||||
const ssrfMocks = {
|
||||
@@ -26,7 +27,7 @@ vi.mock("node:http", async () => {
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
|
||||
formatErrorMessage: (err: unknown) => (err instanceof Error ? err.message : String(err)),
|
||||
formatErrorMessage: coerceErrorMessage,
|
||||
resolvePinnedHostnameWithPolicy: ssrfMocks.resolvePinnedHostnameWithPolicy,
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MeetingPlatformAdapter } from "openclaw/plugin-sdk/meeting-runtime";
|
||||
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { Type } from "typebox";
|
||||
import { teamsMeetingsConfig } from "./src/config.js";
|
||||
import { TeamsMeetingsInvalidRequestError, teamsMeetingsInvalidRequest } from "./src/errors.js";
|
||||
@@ -26,8 +27,7 @@ export default MeetingPlatformAdapter.createPluginShellEntry({
|
||||
message: Type.Optional(Type.String({ description: "Instructions to speak" })),
|
||||
}),
|
||||
resolveGatewayTimeoutMs: teamsMeetingsConfig.resolveGatewayOperationTimeoutMs,
|
||||
normalizeRequesterSessionKey: (value) =>
|
||||
typeof value === "string" && value.trim() ? value.trim() : undefined,
|
||||
normalizeRequesterSessionKey: normalizeOptionalString,
|
||||
normalizeToolAgentId: (agentId) => (agentId ? normalizeAgentId(agentId) : undefined),
|
||||
resolveToolRuntime: async (api, agentId) => {
|
||||
const trustedRouting = Boolean(agentId && agentId !== "main");
|
||||
|
||||
@@ -11,19 +11,14 @@ vi.mock("openclaw/plugin-sdk/text-utility-runtime", () => ({
|
||||
fetchWithTimeout: fetchWithTimeoutMock,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/string-coerce-runtime", () => {
|
||||
vi.mock("openclaw/plugin-sdk/string-coerce-runtime", async (importOriginal) => {
|
||||
const { normalizeOptionalString } =
|
||||
await importOriginal<typeof import("openclaw/plugin-sdk/string-coerce-runtime")>();
|
||||
const isMockRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null;
|
||||
const normalizeMockOptionalString = (value: unknown) => {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
};
|
||||
return {
|
||||
isRecord: isMockRecord,
|
||||
normalizeOptionalString: normalizeMockOptionalString,
|
||||
normalizeOptionalString,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { isApprovalNotFoundError } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { logVerbose, sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { TelegramApprovalCallback } from "./approval-callback-data.js";
|
||||
import {
|
||||
buildTelegramCanonicalApprovalTerminalText,
|
||||
@@ -420,11 +421,7 @@ const updateMultiSelectKeyboard = (
|
||||
);
|
||||
|
||||
const resolvePluginCallbackSubmitText = (submitText: unknown): string | undefined => {
|
||||
if (typeof submitText !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = submitText.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
return normalizeOptionalString(submitText);
|
||||
};
|
||||
|
||||
const isReplySessionInitConflictError = (err: unknown): boolean =>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
resolveAmbientTranscriptWatermarkKey,
|
||||
type SessionEntry,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { asFiniteNumber } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { stripInlineDirectiveTagsForDelivery } from "openclaw/plugin-sdk/text-chunking";
|
||||
import { resolveDefaultModelForAgent } from "./bot-handlers.agent.runtime.js";
|
||||
import type { RegisterTelegramHandlerParams } from "./bot-handlers.types.js";
|
||||
@@ -98,7 +99,7 @@ export type ResolvePromptContextAmbientWatermarkParams = {
|
||||
};
|
||||
|
||||
export const normalizePromptContextMinTimestampMs = (timestampMs?: number) =>
|
||||
typeof timestampMs === "number" && Number.isFinite(timestampMs) ? timestampMs : undefined;
|
||||
asFiniteNumber(timestampMs);
|
||||
|
||||
export function promptContextBoundaryOptions(
|
||||
timestampMs?: number,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Telegram tests cover bot.create telegram bot.media group skip warning plugin behavior.
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { telegramBotInfoForTest } from "./bot.create-telegram-bot.test-support.js";
|
||||
|
||||
@@ -21,7 +22,7 @@ vi.mock("./bot/delivery.resolve-media.runtime.js", async () => {
|
||||
);
|
||||
return {
|
||||
readRemoteMediaBuffer: (...args: unknown[]) => readRemoteMediaBuffer(...args),
|
||||
formatErrorMessage: (err: unknown) => (err instanceof Error ? err.message : String(err)),
|
||||
formatErrorMessage: coerceErrorMessage,
|
||||
logVerbose: () => {},
|
||||
MediaFetchError: actual.MediaFetchError,
|
||||
resolveTelegramApiBase: (apiRoot?: string) =>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GrammyError } from "grammy";
|
||||
import type { Message } from "grammy/types";
|
||||
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
|
||||
// Telegram tests cover delivery.resolve media retry plugin behavior.
|
||||
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
|
||||
@@ -56,7 +57,7 @@ vi.mock("./delivery.resolve-media.runtime.js", () => {
|
||||
}
|
||||
return {
|
||||
readRemoteMediaBuffer: (...args: unknown[]) => readRemoteMediaBuffer(...args),
|
||||
formatErrorMessage: (err: unknown) => (err instanceof Error ? err.message : String(err)),
|
||||
formatErrorMessage: coerceErrorMessage,
|
||||
logVerbose: () => {},
|
||||
MediaFetchError,
|
||||
resolveTelegramApiBase: (apiRoot?: string) =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Message } from "grammy/types";
|
||||
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
|
||||
// Telegram tests cover delivery.resolve media retry plugin behavior.
|
||||
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
|
||||
@@ -55,7 +56,7 @@ vi.mock("./delivery.resolve-media.runtime.js", () => {
|
||||
}
|
||||
return {
|
||||
readRemoteMediaBuffer: (...args: unknown[]) => readRemoteMediaBuffer(...args),
|
||||
formatErrorMessage: (err: unknown) => (err instanceof Error ? err.message : String(err)),
|
||||
formatErrorMessage: coerceErrorMessage,
|
||||
logVerbose: () => {},
|
||||
MediaFetchError,
|
||||
resolveTelegramApiBase: (apiRoot?: string) =>
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
import type { TelegramNetworkConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
|
||||
isTruthyEnvValue: (value: string | undefined) =>
|
||||
typeof value === "string" && /^(1|true|yes|on)$/i.test(value.trim()),
|
||||
vi.mock("openclaw/plugin-sdk/runtime-env", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("openclaw/plugin-sdk/runtime-env")>()),
|
||||
isWSL2Sync: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { GrammyError } from "grammy";
|
||||
import type { MessageEntity } from "grammy/types";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { asFiniteNumber } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { buildTelegramThreadParams, type TelegramThreadSpec } from "./bot/helpers.js";
|
||||
import { normalizeTelegramReplyToMessageId } from "./outbound-params.js";
|
||||
|
||||
@@ -118,7 +119,7 @@ export function getTelegramNativeQuoteReplyMessageId(
|
||||
return undefined;
|
||||
}
|
||||
const messageId = (replyParameters as { message_id?: unknown }).message_id;
|
||||
return typeof messageId === "number" && Number.isFinite(messageId) ? messageId : undefined;
|
||||
return asFiniteNumber(messageId);
|
||||
}
|
||||
|
||||
export function isTelegramQuoteParamError(err: unknown): boolean {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
resolveEnabledConfiguredAccountId,
|
||||
type AccountStatusSnapshot,
|
||||
} from "openclaw/plugin-sdk/status-helpers";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { asFiniteNumber, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
const TELEGRAM_POLLING_CONNECT_GRACE_MS = 120_000;
|
||||
const TELEGRAM_POLLING_STALE_TRANSPORT_MS = 30 * 60_000;
|
||||
@@ -41,10 +41,6 @@ type TelegramGroupMembershipAuditSummary = {
|
||||
}>;
|
||||
};
|
||||
|
||||
function asFiniteNumberOrNull(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function appendTelegramRuntimeError(message: string, lastError: unknown): string {
|
||||
const error = normalizeOptionalString(lastError);
|
||||
return error ? `${message}: ${error}` : message;
|
||||
@@ -69,8 +65,8 @@ function collectTelegramPollingRuntimeIssues(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastStartAt = asFiniteNumberOrNull(account.lastStartAt);
|
||||
const lastTransportActivityAt = asFiniteNumberOrNull(account.lastTransportActivityAt);
|
||||
const lastStartAt = asFiniteNumber(account.lastStartAt) ?? null;
|
||||
const lastTransportActivityAt = asFiniteNumber(account.lastTransportActivityAt) ?? null;
|
||||
const fix = `Run: ${formatCliCommand("openclaw channels status --probe")} (or restart the gateway). Check the bot token, proxy/network settings, and logs if it persists.`;
|
||||
|
||||
if (account.connected === false) {
|
||||
@@ -129,7 +125,7 @@ function collectTelegramWebhookRuntimeIssues(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastStartAt = asFiniteNumberOrNull(account.lastStartAt);
|
||||
const lastStartAt = asFiniteNumber(account.lastStartAt) ?? null;
|
||||
const withinStartupGrace =
|
||||
lastStartAt != null && now - lastStartAt < TELEGRAM_WEBHOOK_CONNECT_GRACE_MS;
|
||||
if (withinStartupGrace) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
sleepWithAbort,
|
||||
type BackoffPolicy,
|
||||
} from "openclaw/plugin-sdk/runtime-env";
|
||||
import { asSafeIntegerInRange } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
const OFFSET_PERSIST_RETRY_POLICY: BackoffPolicy = {
|
||||
initialMs: 250,
|
||||
@@ -21,10 +22,7 @@ type TelegramUpdateOffsetPersistenceOptions = {
|
||||
};
|
||||
|
||||
export function normalizeTelegramUpdateId(value: number | null): number | null {
|
||||
if (value === null || !Number.isSafeInteger(value) || value < 0) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
return asSafeIntegerInRange(value, { min: 0 }) ?? null;
|
||||
}
|
||||
|
||||
export function createTelegramUpdateOffsetPersistence(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Tlon plugin module implements discovery behavior.
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
|
||||
import { asNullableRecord as asRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { Foreigns } from "../urbit/foreigns.js";
|
||||
import { formatErrorMessage } from "./utils.js";
|
||||
|
||||
interface InitData {
|
||||
channels: string[];
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Tlon plugin module implements history behavior.
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
|
||||
import { asNullableRecord as asRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { extractMessageText, formatErrorMessage } from "./utils.js";
|
||||
import { extractMessageText } from "./utils.js";
|
||||
|
||||
/**
|
||||
* Format a number as @ud (with dots every 3 digits from the right)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
bindIngressLifecycleToReplyOptions,
|
||||
waitUntilAbort,
|
||||
} from "openclaw/plugin-sdk/channel-outbound";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import type { GetReplyOptions, ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
|
||||
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
|
||||
@@ -44,7 +45,6 @@ import {
|
||||
shouldMigrateTlonSetting,
|
||||
} from "./settings-helpers.js";
|
||||
import { createActiveSnapshotTracker, createParticipatedThreadTracker } from "./tracking.js";
|
||||
import { formatErrorMessage } from "./utils.js";
|
||||
import {
|
||||
extractMessageText,
|
||||
formatModelName,
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
type StableChannelIngressIdentityParams,
|
||||
} from "openclaw/plugin-sdk/channel-ingress-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { formatErrorMessage as sharedFormatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
// Tlon helper module supports utils behavior.
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import { asNullableRecord, readStringField } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
@@ -243,8 +242,6 @@ export async function resolveAuthorizedMessageText(params: {
|
||||
return citedContent + rawText;
|
||||
}
|
||||
|
||||
export const formatErrorMessage = sharedFormatErrorMessage;
|
||||
|
||||
// Helper to recursively extract text from inline content
|
||||
function renderInlineItem(
|
||||
item: unknown,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// Together tests cover together plugin behavior.
|
||||
import { completeSimple, type Model } from "openclaw/plugin-sdk/llm";
|
||||
import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TOGETHER_BASE_URL, TOGETHER_MODEL_CATALOG } from "./models.js";
|
||||
|
||||
const TOGETHER_KEY = process.env.TOGETHER_API_KEY ?? "";
|
||||
const LIVE = ["LIVE", "OPENCLAW_LIVE_TEST", "TOGETHER_LIVE_TEST"].some((name) => {
|
||||
const value = process.env[name]?.trim().toLowerCase();
|
||||
return value === "1" || value === "true" || value === "yes" || value === "on";
|
||||
});
|
||||
const LIVE = ["LIVE", "OPENCLAW_LIVE_TEST", "TOGETHER_LIVE_TEST"].some((name) =>
|
||||
isTruthyEnvValue(process.env[name]),
|
||||
);
|
||||
const TOGETHER_LIVE_TIMEOUT_MS = 45_000;
|
||||
|
||||
const describeLive = LIVE && TOGETHER_KEY ? describe : describe.skip;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { EventEmitter } from "node:events";
|
||||
import fsSync from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { createChannelIngressQueueForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime";
|
||||
import { resetLogger, setLoggerOverride } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { afterEach, beforeEach, expect, vi } from "vitest";
|
||||
@@ -238,7 +239,7 @@ vi.mock("./session.js", async () => {
|
||||
}),
|
||||
waitForWaConnection: vi.fn().mockResolvedValue(undefined),
|
||||
getStatusCode: vi.fn(() => 500),
|
||||
formatError: (err: unknown) => (err instanceof Error ? err.message : String(err)),
|
||||
formatError: coerceErrorMessage,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Whatsapp tests cover qa driver plugin behavior.
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { proto, WAMessage } from "baileys";
|
||||
import { coerceErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { startWhatsAppQaDriverSession, type WhatsAppQaDriverSession } from "./qa-driver.runtime.js";
|
||||
import { DEFAULT_WHATSAPP_SOCKET_TIMING } from "./socket-timing.js";
|
||||
@@ -22,7 +23,7 @@ const mocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("./session.js", () => ({
|
||||
createWaSocket: mocks.createWaSocket,
|
||||
formatError: (error: unknown) => (error instanceof Error ? error.message : String(error)),
|
||||
formatError: coerceErrorMessage,
|
||||
getStatusCode: (error: unknown) =>
|
||||
(error as { output?: { statusCode?: number } } | undefined)?.output?.statusCode,
|
||||
waitForWaConnection: mocks.waitForWaConnection,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// ACP Core module implements meta behavior.
|
||||
import { asFiniteNumber, asSafeIntegerInRange } from "@openclaw/normalization-core/number-coercion";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
|
||||
function readMetaValue<T>(
|
||||
@@ -39,9 +40,7 @@ export function readMetadataNumber(
|
||||
meta: Record<string, unknown> | null | undefined,
|
||||
keys: string[],
|
||||
): number | undefined {
|
||||
return readMetaValue(meta, keys, (value) =>
|
||||
typeof value === "number" && Number.isFinite(value) ? value : undefined,
|
||||
);
|
||||
return readMetaValue(meta, keys, asFiniteNumber);
|
||||
}
|
||||
|
||||
/** Reads the first safe non-negative integer metadata value, preserving zero. */
|
||||
@@ -49,7 +48,5 @@ export function readNonNegativeInteger(
|
||||
meta: Record<string, unknown> | null | undefined,
|
||||
keys: string[],
|
||||
): number | undefined {
|
||||
return readMetaValue(meta, keys, (value) =>
|
||||
typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined,
|
||||
);
|
||||
return readMetaValue(meta, keys, (value) => asSafeIntegerInRange(value, { min: 0 }));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
ToolResultMessage,
|
||||
} from "@openclaw/llm-core";
|
||||
import type { EventStream as SourceEventStream } from "@openclaw/llm-core";
|
||||
import { coerceErrorMessage } from "@openclaw/normalization-core/error-coercion";
|
||||
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { TranscriptNotContinuableError } from "./errors.js";
|
||||
import { uuidv7 } from "./harness/session/uuid.js";
|
||||
@@ -1332,7 +1333,7 @@ async function prepareToolCall(
|
||||
} catch (error) {
|
||||
return {
|
||||
kind: "immediate",
|
||||
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
|
||||
result: createErrorToolResult(coerceErrorMessage(error)),
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
@@ -1360,11 +1361,7 @@ async function validateToolCallForBatchAdmission(
|
||||
outcome: {
|
||||
kind: "immediate",
|
||||
result: createErrorToolResult(
|
||||
signal?.aborted
|
||||
? "Operation aborted"
|
||||
: resolution.error instanceof Error
|
||||
? resolution.error.message
|
||||
: String(resolution.error),
|
||||
signal?.aborted ? "Operation aborted" : coerceErrorMessage(resolution.error),
|
||||
),
|
||||
isError: true,
|
||||
},
|
||||
@@ -1390,7 +1387,7 @@ async function validateToolCallForBatchAdmission(
|
||||
kind: "immediate",
|
||||
outcome: {
|
||||
kind: "immediate",
|
||||
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
|
||||
result: createErrorToolResult(coerceErrorMessage(error)),
|
||||
isError: true,
|
||||
},
|
||||
};
|
||||
@@ -1404,7 +1401,7 @@ async function validateToolCallForBatchAdmission(
|
||||
kind: "immediate",
|
||||
outcome: {
|
||||
kind: "immediate",
|
||||
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
|
||||
result: createErrorToolResult(coerceErrorMessage(error)),
|
||||
isError: true,
|
||||
errorKind: "argument-validation",
|
||||
},
|
||||
@@ -1452,7 +1449,7 @@ async function prepareToolCallExecution(
|
||||
return {
|
||||
kind: "immediate",
|
||||
outcome: {
|
||||
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
|
||||
result: createErrorToolResult(coerceErrorMessage(error)),
|
||||
isError: true,
|
||||
executionStarted: false,
|
||||
},
|
||||
@@ -1508,7 +1505,7 @@ async function prepareToolCallExecution(
|
||||
throw implementationStartError.error;
|
||||
}
|
||||
return {
|
||||
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
|
||||
result: createErrorToolResult(coerceErrorMessage(error)),
|
||||
isError: true,
|
||||
executionStarted,
|
||||
...(executionStarted && signal?.aborted && error === signal.reason
|
||||
@@ -1570,11 +1567,7 @@ async function prepareToolCallExecution(
|
||||
return {
|
||||
kind: "immediate",
|
||||
outcome: {
|
||||
result: createErrorToolResult(
|
||||
internalPreparation.outcome.error instanceof Error
|
||||
? internalPreparation.outcome.error.message
|
||||
: String(internalPreparation.outcome.error),
|
||||
),
|
||||
result: createErrorToolResult(coerceErrorMessage(internalPreparation.outcome.error)),
|
||||
isError: true,
|
||||
executionStarted: false,
|
||||
},
|
||||
@@ -1627,7 +1620,7 @@ async function finalizeExecutedToolCall(
|
||||
isError = afterResult.isError ?? isError;
|
||||
}
|
||||
} catch (error) {
|
||||
result = createErrorToolResult(error instanceof Error ? error.message : String(error));
|
||||
result = createErrorToolResult(coerceErrorMessage(error));
|
||||
isError = true;
|
||||
}
|
||||
}
|
||||
@@ -1692,9 +1685,7 @@ async function finalizeToolCallOutcome(
|
||||
isError: afterResult.isError ?? finalized.isError,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorResult = createErrorToolResult(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
const errorResult = createErrorToolResult(coerceErrorMessage(error));
|
||||
return {
|
||||
...finalized,
|
||||
result: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asNonNegativeFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||
import type { Usage } from "../types.js";
|
||||
|
||||
type AnthropicUsagePayload = {
|
||||
@@ -31,7 +32,7 @@ export type AnthropicIterationUsageResult =
|
||||
| { state: "valid"; usage: AnthropicIterationUsageSnapshot };
|
||||
|
||||
export function readAnthropicUsageTokenCount(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
||||
return asNonNegativeFiniteNumber(value);
|
||||
}
|
||||
|
||||
export function readAnthropicCacheWriteUsage(
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* package and managed transports from drifting on token buckets, service-tier pricing, or future
|
||||
* terminal-event semantics.
|
||||
*/
|
||||
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||
import type OpenAI from "openai";
|
||||
import type { StopReason, Usage } from "../types.js";
|
||||
|
||||
@@ -53,10 +54,7 @@ export function mapResponsesTerminalUsage(
|
||||
export function readResponsesReasoningTokens(
|
||||
usage: ResponsesTerminalUsagePayload | undefined | null,
|
||||
): number | undefined {
|
||||
const reasoningTokens = usage?.output_tokens_details?.reasoning_tokens;
|
||||
return typeof reasoningTokens === "number" && Number.isFinite(reasoningTokens)
|
||||
? reasoningTokens
|
||||
: undefined;
|
||||
return asFiniteNumber(usage?.output_tokens_details?.reasoning_tokens);
|
||||
}
|
||||
|
||||
function mapResponsesTerminalStopReason(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user