mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(core): adopt normalization-core leaf helpers across production (#120350)
* refactor(core): adopt normalization-core leaf helpers across production * fix(ci): keep plugin contract source-resolvable * fix(errors): preserve adapter-owned error fields * fix(errors): short-circuit existing errors before stringifying * fix(errors): skip throwing structured getters * ci: retrigger checks on current base * fix(errors): guard structured error enumeration * fix: harden error detail copying
This commit is contained in:
committed by
GitHub
parent
e81d7e62e8
commit
e7a9f33d89
@@ -1,16 +1,10 @@
|
|||||||
// Memory Host SDK module implements embeddings remote fetch behavior.
|
// Memory Host SDK module implements embeddings remote fetch behavior.
|
||||||
|
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { postJson } from "./post-json.js";
|
import { postJson } from "./post-json.js";
|
||||||
import type { SsrFPolicy } from "./ssrf-policy.js";
|
import type { SsrFPolicy } from "./ssrf-policy.js";
|
||||||
|
|
||||||
// Fetches and validates OpenAI-compatible embedding responses.
|
// Fetches and validates OpenAI-compatible embedding responses.
|
||||||
|
|
||||||
/** Narrow unknown JSON payloads to plain objects. */
|
|
||||||
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
||||||
? (value as Record<string, unknown>)
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Build the common malformed embedding response error. */
|
/** Build the common malformed embedding response error. */
|
||||||
function malformedEmbeddingResponse(errorPrefix: string): Error {
|
function malformedEmbeddingResponse(errorPrefix: string): Error {
|
||||||
return new Error(`${errorPrefix}: malformed JSON response`);
|
return new Error(`${errorPrefix}: malformed JSON response`);
|
||||||
@@ -31,7 +25,7 @@ function readEmbeddingVector(value: unknown, errorPrefix: string): number[] {
|
|||||||
|
|
||||||
/** Resolve expected response count from the request body when input is an array. */
|
/** Resolve expected response count from the request body when input is an array. */
|
||||||
function resolveExpectedEmbeddingCount(body: unknown): number | undefined {
|
function resolveExpectedEmbeddingCount(body: unknown): number | undefined {
|
||||||
const input = asRecord(body)?.input;
|
const input = asOptionalRecord(body)?.input;
|
||||||
return Array.isArray(input) ? input.length : undefined;
|
return Array.isArray(input) ? input.length : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +48,7 @@ export async function fetchRemoteEmbeddingVectors(params: {
|
|||||||
body: params.body,
|
body: params.body,
|
||||||
errorPrefix: params.errorPrefix,
|
errorPrefix: params.errorPrefix,
|
||||||
parse: (payload) => {
|
parse: (payload) => {
|
||||||
const root = asRecord(payload);
|
const root = asOptionalRecord(payload);
|
||||||
if (!root || !Array.isArray(root.data)) {
|
if (!root || !Array.isArray(root.data)) {
|
||||||
throw malformedEmbeddingResponse(params.errorPrefix);
|
throw malformedEmbeddingResponse(params.errorPrefix);
|
||||||
}
|
}
|
||||||
@@ -63,7 +57,7 @@ export async function fetchRemoteEmbeddingVectors(params: {
|
|||||||
throw malformedEmbeddingResponse(params.errorPrefix);
|
throw malformedEmbeddingResponse(params.errorPrefix);
|
||||||
}
|
}
|
||||||
return root.data.map((entry) => {
|
return root.data.map((entry) => {
|
||||||
const record = asRecord(entry);
|
const record = asOptionalRecord(entry);
|
||||||
if (!record) {
|
if (!record) {
|
||||||
throw malformedEmbeddingResponse(params.errorPrefix);
|
throw malformedEmbeddingResponse(params.errorPrefix);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Memory Host SDK module implements qmd query parser behavior.
|
// Memory Host SDK module implements qmd query parser behavior.
|
||||||
|
import { asPositiveSafeInteger as parseQmdLineNumber } from "@openclaw/normalization-core/number-coercion";
|
||||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||||
import { formatErrorMessage } from "./error-utils.js";
|
import { formatErrorMessage } from "./error-utils.js";
|
||||||
@@ -124,11 +125,6 @@ function parseQmdQueryResultArray(raw: string): QmdQueryResult[] | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Normalize qmd line numbers, rejecting zero, negative, and non-integer values. */
|
|
||||||
function parseQmdLineNumber(value: unknown): number | undefined {
|
|
||||||
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Extract the first complete, standalone JSON result array from noisy stdout. */
|
/** Extract the first complete, standalone JSON result array from noisy stdout. */
|
||||||
function extractFirstJsonArray(raw: string): string | null {
|
function extractFirstJsonArray(raw: string): string | null {
|
||||||
let start = -1;
|
let start = -1;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
// Secret input parsing shared by memory provider config and gateway-resolved snapshots.
|
// Secret input parsing shared by memory provider config and gateway-resolved snapshots.
|
||||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { hasNonEmptyString } from "@openclaw/normalization-core/string-coerce";
|
import {
|
||||||
|
hasNonEmptyString,
|
||||||
|
normalizeOptionalString,
|
||||||
|
} from "@openclaw/normalization-core/string-coerce";
|
||||||
|
|
||||||
/** Supported secret reference backing stores. */
|
/** Supported secret reference backing stores. */
|
||||||
type SecretRefSource = "env" | "file" | "exec";
|
type SecretRefSource = "env" | "file" | "exec";
|
||||||
@@ -18,15 +21,6 @@ const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:";
|
|||||||
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
|
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
|
||||||
const SECRET_REF_SOURCES = new Set<SecretRefSource>(["env", "file", "exec"]);
|
const SECRET_REF_SOURCES = new Set<SecretRefSource>(["env", "file", "exec"]);
|
||||||
|
|
||||||
/** Normalize literal secret strings and reject empty placeholders. */
|
|
||||||
function normalizeSecretInputString(value: unknown): string | undefined {
|
|
||||||
if (typeof value !== "string") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const trimmed = value.trim();
|
|
||||||
return trimmed.length > 0 ? trimmed : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Narrow a string to a supported SecretRef source. */
|
/** Narrow a string to a supported SecretRef source. */
|
||||||
function hasSecretRefSource(value: unknown): value is SecretRefSource {
|
function hasSecretRefSource(value: unknown): value is SecretRefSource {
|
||||||
return typeof value === "string" && SECRET_REF_SOURCES.has(value as SecretRefSource);
|
return typeof value === "string" && SECRET_REF_SOURCES.has(value as SecretRefSource);
|
||||||
@@ -111,7 +105,7 @@ function coerceSecretRef(value: unknown): SecretRef | null {
|
|||||||
|
|
||||||
/** Return true when a secret input has either a literal value or resolvable reference shape. */
|
/** Return true when a secret input has either a literal value or resolvable reference shape. */
|
||||||
export function hasConfiguredMemorySecretInputValue(value: unknown): boolean {
|
export function hasConfiguredMemorySecretInputValue(value: unknown): boolean {
|
||||||
if (normalizeSecretInputString(value)) {
|
if (normalizeOptionalString(value)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return coerceSecretRef(value) !== null;
|
return coerceSecretRef(value) !== null;
|
||||||
@@ -139,7 +133,7 @@ export function normalizeResolvedMemorySecretInputString(params: {
|
|||||||
value: unknown;
|
value: unknown;
|
||||||
path: string;
|
path: string;
|
||||||
}): string | undefined {
|
}): string | undefined {
|
||||||
const normalized = normalizeSecretInputString(params.value);
|
const normalized = normalizeOptionalString(params.value);
|
||||||
if (normalized) {
|
if (normalized) {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
@@ -152,5 +146,5 @@ export function normalizeResolvedMemorySecretInputString(params: {
|
|||||||
|
|
||||||
/** Normalize env-provided secret values before use. */
|
/** Normalize env-provided secret values before use. */
|
||||||
export function normalizeEnvSecretInputString(value: unknown): string | undefined {
|
export function normalizeEnvSecretInputString(value: unknown): string | undefined {
|
||||||
return normalizeSecretInputString(value);
|
return normalizeOptionalString(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Model Catalog Core helper module supports model catalog normalize behavior.
|
// Model Catalog Core helper module supports model catalog normalize behavior.
|
||||||
|
import { asFiniteNumber as normalizeFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import {
|
import {
|
||||||
@@ -127,10 +128,6 @@ function normalizeNonNegativeNumber(value: unknown): number | undefined {
|
|||||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeFiniteNumber(value: unknown): number | undefined {
|
|
||||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeStringOrNumber(value: unknown): string | number | undefined {
|
function normalizeStringOrNumber(value: unknown): string | number | undefined {
|
||||||
return normalizeOptionalString(value) ?? normalizeFiniteNumber(value);
|
return normalizeOptionalString(value) ?? normalizeFiniteNumber(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"build": "tsdown src/index.ts src/ip.ts src/ipv4.ts src/redact-sensitive-url.ts src/url-protocol.ts src/url-userinfo.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
|
"build": "tsdown src/index.ts src/ip.ts src/ipv4.ts src/redact-sensitive-url.ts src/url-protocol.ts src/url-userinfo.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@openclaw/normalization-core": "workspace:*",
|
||||||
"ipaddr.js": "2.4.0"
|
"ipaddr.js": "2.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
// Network Policy module implements ip behavior.
|
// Network Policy module implements ip behavior.
|
||||||
|
import {
|
||||||
|
normalizeLowercaseStringOrEmpty,
|
||||||
|
normalizeOptionalString,
|
||||||
|
} from "@openclaw/normalization-core/string-coerce";
|
||||||
import ipaddr from "ipaddr.js";
|
import ipaddr from "ipaddr.js";
|
||||||
|
|
||||||
function normalizeOptionalString(value: unknown): string | undefined {
|
|
||||||
if (typeof value !== "string") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const trimmed = value.trim();
|
|
||||||
return trimmed || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeLowercaseStringOrEmpty(value: unknown): string {
|
|
||||||
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Parsed IP address value returned by the net-policy parsing helpers. */
|
/** Parsed IP address value returned by the net-policy parsing helpers. */
|
||||||
export type ParsedIpAddress = ipaddr.IPv4 | ipaddr.IPv6;
|
export type ParsedIpAddress = ipaddr.IPv4 | ipaddr.IPv6;
|
||||||
type Ipv4Range = ReturnType<ipaddr.IPv4["range"]>;
|
type Ipv4Range = ReturnType<ipaddr.IPv4["range"]>;
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
// Network Policy module implements redact sensitive url behavior.
|
// Network Policy module implements redact sensitive url behavior.
|
||||||
|
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||||
|
|
||||||
type ConfigUiHintTags = {
|
type ConfigUiHintTags = {
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeLowercaseStringOrEmpty(value: unknown): string {
|
|
||||||
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Config UI hint tag for URL-like values that may embed credentials or tokens. */
|
/** Config UI hint tag for URL-like values that may embed credentials or tokens. */
|
||||||
export const SENSITIVE_URL_HINT_TAG = "url-secret";
|
export const SENSITIVE_URL_HINT_TAG = "url-secret";
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
avoidTrailingHighSurrogateBreak,
|
avoidTrailingHighSurrogateBreak,
|
||||||
sliceUtf16Safe,
|
sliceUtf16Safe,
|
||||||
truncateUtf16Safe,
|
truncateUtf16Safe,
|
||||||
|
truncateWithMarker,
|
||||||
} from "./utf16-slice.js";
|
} from "./utf16-slice.js";
|
||||||
|
|
||||||
describe("avoidTrailingHighSurrogateBreak", () => {
|
describe("avoidTrailingHighSurrogateBreak", () => {
|
||||||
@@ -105,3 +106,52 @@ describe("truncateUtf16Safe", () => {
|
|||||||
expect(truncateUtf16Safe(input, 1)).toBe("");
|
expect(truncateUtf16Safe(input, 1)).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("truncateWithMarker", () => {
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
name: "returns values at the boundary unchanged",
|
||||||
|
value: "hello",
|
||||||
|
max: 5,
|
||||||
|
options: { marker: "...", reserve: 3, trimEnd: false },
|
||||||
|
expected: "hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reserves marker width",
|
||||||
|
value: "hello world",
|
||||||
|
max: 8,
|
||||||
|
options: { marker: "...", reserve: 3, trimEnd: false },
|
||||||
|
expected: "hello...",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "supports markers outside the limit",
|
||||||
|
value: "hello world",
|
||||||
|
max: 5,
|
||||||
|
options: { marker: "...", reserve: 0, trimEnd: false },
|
||||||
|
expected: "hello...",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "trims only the truncated prefix",
|
||||||
|
value: "hello world",
|
||||||
|
max: 9,
|
||||||
|
options: { marker: "...", reserve: 3, trimEnd: true },
|
||||||
|
expected: "hello...",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "keeps surrogate pairs well formed",
|
||||||
|
value: "ab🚀tail",
|
||||||
|
max: 4,
|
||||||
|
options: { marker: "…", reserve: 1, trimEnd: false },
|
||||||
|
expected: "ab…",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preserves marker output at zero limits",
|
||||||
|
value: "hello",
|
||||||
|
max: 0,
|
||||||
|
options: { marker: "…", reserve: 1, trimEnd: false },
|
||||||
|
expected: "…",
|
||||||
|
},
|
||||||
|
] as const)("$name", ({ value, max, options, expected }) => {
|
||||||
|
expect(truncateWithMarker(value, max, options)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -62,3 +62,16 @@ export function truncateUtf16Safe(input: string, maxLen: number): string {
|
|||||||
}
|
}
|
||||||
return sliceUtf16Safe(input, 0, limit);
|
return sliceUtf16Safe(input, 0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Truncates text and appends a marker while preserving the caller's reserved width contract. */
|
||||||
|
export function truncateWithMarker(
|
||||||
|
value: string,
|
||||||
|
max: number,
|
||||||
|
options: { marker: string; reserve: number; trimEnd: boolean },
|
||||||
|
): string {
|
||||||
|
if (value.length <= max) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const prefix = truncateUtf16Safe(value, max - options.reserve);
|
||||||
|
return `${options.trimEnd ? prefix.trimEnd() : prefix}${options.marker}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
// External code plugin package.json compatibility and validation contracts.
|
// External code plugin package.json compatibility and validation contracts.
|
||||||
|
import { isRecord } from "../../normalization-core/src/record-coerce.js";
|
||||||
|
import { normalizeOptionalString } from "../../normalization-core/src/string-coerce.js";
|
||||||
|
|
||||||
/** JSON object shape accepted by package contract helpers. */
|
/** JSON object shape accepted by package contract helpers. */
|
||||||
export type JsonObject = Record<string, unknown>;
|
export type JsonObject = Record<string, unknown>;
|
||||||
@@ -29,20 +31,6 @@ export const EXTERNAL_CODE_PLUGIN_REQUIRED_FIELD_PATHS = [
|
|||||||
"openclaw.build.openclawVersion",
|
"openclaw.build.openclawVersion",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** Narrow unknown values to plain records. */
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Normalize optional package metadata strings. */
|
|
||||||
function normalizeOptionalString(value: unknown): string | undefined {
|
|
||||||
if (typeof value !== "string") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const trimmed = value.trim();
|
|
||||||
return trimmed ? trimmed : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Read OpenClaw package.json blocks without trusting caller input shape. */
|
/** Read OpenClaw package.json blocks without trusting caller input shape. */
|
||||||
function readOpenClawBlock(packageJson: unknown) {
|
function readOpenClawBlock(packageJson: unknown) {
|
||||||
const root = isRecord(packageJson) ? packageJson : undefined;
|
const root = isRecord(packageJson) ? packageJson : undefined;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"build": "tsdown src/index.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
|
"build": "tsdown src/index.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openclaw/gateway-client": "workspace:*"
|
"@openclaw/gateway-client": "workspace:*",
|
||||||
|
"@openclaw/normalization-core": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// OpenClaw SDK module implements client behavior.
|
// OpenClaw SDK module implements client behavior.
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { asRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { EventHub } from "./event-hub.js";
|
import { EventHub } from "./event-hub.js";
|
||||||
import { normalizeGatewayEvent } from "./normalize.js";
|
import { normalizeGatewayEvent } from "./normalize.js";
|
||||||
import { GatewayClientTransport, isConnectableTransport } from "./transport.js";
|
import { GatewayClientTransport, isConnectableTransport } from "./transport.js";
|
||||||
@@ -222,10 +223,6 @@ type ChatProjection = {
|
|||||||
payload: Record<string, unknown>;
|
payload: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function asRecord(value: unknown): Record<string, unknown> {
|
|
||||||
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasArtifactQueryScope(params: unknown): params is ArtifactQuery {
|
function hasArtifactQueryScope(params: unknown): params is ArtifactQuery {
|
||||||
const record = asRecord(params);
|
const record = asRecord(params);
|
||||||
return [record.sessionKey, record.runId, record.taskId].some(
|
return [record.sessionKey, record.runId, record.taskId].some(
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
// OpenClaw SDK helper module supports normalize behavior.
|
// OpenClaw SDK helper module supports normalize behavior.
|
||||||
|
import { asFiniteNumber as readNumber } from "@openclaw/normalization-core/number-coercion";
|
||||||
|
import { asRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import type { GatewayEvent, JsonObject, OpenClawEvent, OpenClawEventType } from "./types.js";
|
import type { GatewayEvent, JsonObject, OpenClawEvent, OpenClawEventType } from "./types.js";
|
||||||
|
|
||||||
// Normalize raw Gateway events into stable SDK event types and common metadata.
|
|
||||||
function asRecord(value: unknown): JsonObject {
|
|
||||||
return typeof value === "object" && value !== null ? (value as JsonObject) : {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readString(value: unknown): string | undefined {
|
function readString(value: unknown): string | undefined {
|
||||||
return typeof value === "string" && value.length > 0 ? value : undefined;
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readNumber(value: unknown): number | undefined {
|
|
||||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readLowerString(value: unknown): string | undefined {
|
function readLowerString(value: unknown): string | undefined {
|
||||||
return readString(value)?.toLowerCase();
|
return readString(value)?.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { asOptionalObjectRecord as asRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import type { PlainTextToolCallProtectedRangeResolver } from "./contracts.js";
|
import type { PlainTextToolCallProtectedRangeResolver } from "./contracts.js";
|
||||||
// Tool Call Repair module implements promote behavior.
|
// Tool Call Repair module implements promote behavior.
|
||||||
import { parseStandalonePlainTextToolCallBlocks, type PlainTextToolCallBlock } from "./payload.js";
|
import { parseStandalonePlainTextToolCallBlocks, type PlainTextToolCallBlock } from "./payload.js";
|
||||||
@@ -45,10 +46,6 @@ export function createPromotedPlainTextToolCallBlock(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
||||||
return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Emits the complete provider-neutral lifecycle for promoted tool-call blocks. */
|
/** Emits the complete provider-neutral lifecycle for promoted tool-call blocks. */
|
||||||
export function createPromotedPlainTextToolCallEvents(
|
export function createPromotedPlainTextToolCallEvents(
|
||||||
message: Record<string, unknown>,
|
message: Record<string, unknown>,
|
||||||
|
|||||||
Generated
+6
@@ -2306,6 +2306,9 @@ importers:
|
|||||||
|
|
||||||
packages/net-policy:
|
packages/net-policy:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@openclaw/normalization-core':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../normalization-core
|
||||||
ipaddr.js:
|
ipaddr.js:
|
||||||
specifier: 2.4.0
|
specifier: 2.4.0
|
||||||
version: 2.4.0
|
version: 2.4.0
|
||||||
@@ -2330,6 +2333,9 @@ importers:
|
|||||||
'@openclaw/gateway-client':
|
'@openclaw/gateway-client':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../gateway-client
|
version: link:../gateway-client
|
||||||
|
'@openclaw/normalization-core':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../normalization-core
|
||||||
|
|
||||||
packages/session-url-contract: {}
|
packages/session-url-contract: {}
|
||||||
|
|
||||||
|
|||||||
+1
-4
@@ -10,6 +10,7 @@ import {
|
|||||||
type AnyMessage,
|
type AnyMessage,
|
||||||
} from "@agentclientprotocol/sdk";
|
} from "@agentclientprotocol/sdk";
|
||||||
import type { AcpServerOptions } from "@openclaw/acp-core/types";
|
import type { AcpServerOptions } from "@openclaw/acp-core/types";
|
||||||
|
import { isRecord as isJsonObject } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import {
|
import {
|
||||||
GATEWAY_CLIENT_CAPS,
|
GATEWAY_CLIENT_CAPS,
|
||||||
@@ -289,10 +290,6 @@ function normalizeAcpInitializeProtocolVersion(message: AnyMessage): AnyMessage
|
|||||||
} as AnyMessage;
|
} as AnyMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isJsonObject(value: unknown): value is JsonObject {
|
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isUint16Integer(value: unknown): value is number {
|
function isUint16Integer(value: unknown): value is number {
|
||||||
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 0xffff;
|
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 0xffff;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { defaultAcpSessionStore, type AcpSessionStore } from "@openclaw/acp-core
|
|||||||
import { toAcpSessionLineageMeta } from "@openclaw/acp-core/session-lineage-meta";
|
import { toAcpSessionLineageMeta } from "@openclaw/acp-core/session-lineage-meta";
|
||||||
import type { AcpServerOptions } from "@openclaw/acp-core/types";
|
import type { AcpServerOptions } from "@openclaw/acp-core/types";
|
||||||
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
|
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
|
||||||
|
import { normalizeLowercaseStringOrEmpty as normalizedChatSendAckStatus } from "@openclaw/normalization-core/string-coerce";
|
||||||
import {
|
import {
|
||||||
normalizeFastMode,
|
normalizeFastMode,
|
||||||
normalizeOptionalString,
|
normalizeOptionalString,
|
||||||
@@ -113,10 +114,6 @@ type ChatSendAck = {
|
|||||||
status?: unknown;
|
status?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizedChatSendAckStatus(status: unknown): string {
|
|
||||||
return typeof status === "string" ? status.trim().toLowerCase() : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTerminalChatSendAckFailure(status: unknown): boolean {
|
function isTerminalChatSendAckFailure(status: unknown): boolean {
|
||||||
const normalized = normalizedChatSendAckStatus(status);
|
const normalized = normalizedChatSendAckStatus(status);
|
||||||
return normalized === "timeout" || normalized === "error";
|
return normalized === "timeout" || normalized === "error";
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
/** Relays child ACP session stream updates back into the requester parent session. */
|
/** Relays child ACP session stream updates back into the requester parent session. */
|
||||||
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||||
|
import { asOptionalRecord as asObjectRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { sliceUtf16Safe, truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
import {
|
||||||
|
sliceUtf16Safe,
|
||||||
|
truncateUtf16Safe,
|
||||||
|
truncateWithMarker,
|
||||||
|
} from "@openclaw/normalization-core/utf16-slice";
|
||||||
import {
|
import {
|
||||||
isAcpTagVisible,
|
isAcpTagVisible,
|
||||||
resolveAcpProjectionSettings,
|
resolveAcpProjectionSettings,
|
||||||
@@ -58,7 +63,7 @@ function truncate(value: string, maxChars: number): string {
|
|||||||
if (maxChars <= 1) {
|
if (maxChars <= 1) {
|
||||||
return truncateUtf16Safe(value, maxChars);
|
return truncateUtf16Safe(value, maxChars);
|
||||||
}
|
}
|
||||||
return `${truncateUtf16Safe(value, maxChars - 1)}…`;
|
return truncateWithMarker(value, maxChars, { marker: "…", reserve: 1, trimEnd: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeStringArray(value: unknown): string[] {
|
function normalizeStringArray(value: unknown): string[] {
|
||||||
@@ -75,12 +80,6 @@ function formatProxyEnvSummary(keys: string[]): string {
|
|||||||
return `proxy env: ${keys.join(", ")}`;
|
return `proxy env: ${keys.join(", ")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function asObjectRecord(value: unknown): Record<string, unknown> | undefined {
|
|
||||||
return value && typeof value === "object" && !Array.isArray(value)
|
|
||||||
? (value as Record<string, unknown>)
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeStreamingConfig(base: unknown, override: unknown): unknown {
|
function mergeStreamingConfig(base: unknown, override: unknown): unknown {
|
||||||
const baseRecord = asObjectRecord(base);
|
const baseRecord = asObjectRecord(base);
|
||||||
const overrideRecord = asObjectRecord(override);
|
const overrideRecord = asObjectRecord(override);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import type { ReplyPayload } from "../auto-reply/reply-payload.js";
|
import type { ReplyPayload } from "../auto-reply/reply-payload.js";
|
||||||
import type { RestartRecoveryTerminalDeliveryEvidenceResult } from "../config/sessions/restart-recovery-types.js";
|
import type { RestartRecoveryTerminalDeliveryEvidenceResult } from "../config/sessions/restart-recovery-types.js";
|
||||||
import type { SessionEntry } from "../config/sessions/types.js";
|
import type { SessionEntry } from "../config/sessions/types.js";
|
||||||
@@ -13,10 +14,6 @@ import {
|
|||||||
} from "./embedded-agent-runner/delivery-evidence.js";
|
} from "./embedded-agent-runner/delivery-evidence.js";
|
||||||
import { mergeAttemptToolMediaPayloads } from "./embedded-agent-runner/run/tool-media-payloads.js";
|
import { mergeAttemptToolMediaPayloads } from "./embedded-agent-runner/run/tool-media-payloads.js";
|
||||||
|
|
||||||
function normalizeOptionalString(value: unknown): string | undefined {
|
|
||||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeOptionalThreadId(value: unknown): string | undefined {
|
function normalizeOptionalThreadId(value: unknown): string | undefined {
|
||||||
return (
|
return (
|
||||||
normalizeOptionalString(value) ??
|
normalizeOptionalString(value) ??
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/** Normalizes agent run wait/liveness/timeout metadata into sticky terminal outcomes. */
|
/** Normalizes agent run wait/liveness/timeout metadata into sticky terminal outcomes. */
|
||||||
|
import { asFiniteNumber as asFiniteTimestamp } from "@openclaw/normalization-core/number-coercion";
|
||||||
import {
|
import {
|
||||||
formatAbandonedLivenessError,
|
formatAbandonedLivenessError,
|
||||||
formatBlockedLivenessError,
|
formatBlockedLivenessError,
|
||||||
@@ -461,10 +462,6 @@ export const AGENT_RUN_TERMINAL_RETRY_GRACE_MS = 15_000;
|
|||||||
|
|
||||||
const HARD_TIMEOUT_PHASES = new Set<AgentRunTimeoutPhase>(["preflight", "provider", "post_turn"]);
|
const HARD_TIMEOUT_PHASES = new Set<AgentRunTimeoutPhase>(["preflight", "provider", "post_turn"]);
|
||||||
|
|
||||||
function asFiniteTimestamp(value: unknown): number | undefined {
|
|
||||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function asNonEmptyString(value: unknown): string | undefined {
|
function asNonEmptyString(value: unknown): string | undefined {
|
||||||
return typeof value === "string" && value.trim() ? value : undefined;
|
return typeof value === "string" && value.trim() ? value : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/** Pure, non-resolving credential availability checks shared by status and route selection. */
|
/** Pure, non-resolving credential availability checks shared by status and route selection. */
|
||||||
|
import { hasNonEmptyString as hasSecret } from "@openclaw/normalization-core/string-coerce";
|
||||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||||
import {
|
import {
|
||||||
isSecretRef,
|
isSecretRef,
|
||||||
@@ -20,10 +21,6 @@ import type { AuthProfileCredential } from "./types.js";
|
|||||||
|
|
||||||
type ReadOnlyCredentialAvailability = boolean | undefined;
|
type ReadOnlyCredentialAvailability = boolean | undefined;
|
||||||
|
|
||||||
function hasSecret(value: unknown): value is string {
|
|
||||||
return typeof value === "string" && value.trim().length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasMalformedSecretInputSyntax(value: unknown): boolean {
|
export function hasMalformedSecretInputSyntax(value: unknown): boolean {
|
||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { DatabaseSync } from "node:sqlite";
|
import type { DatabaseSync } from "node:sqlite";
|
||||||
|
import { safeParseJson } from "@openclaw/normalization-core";
|
||||||
import { sha256HexPrefix } from "../../infra/crypto-digest.js";
|
import { sha256HexPrefix } from "../../infra/crypto-digest.js";
|
||||||
import {
|
import {
|
||||||
clearNodeSqliteKyselyCacheForDatabase,
|
clearNodeSqliteKyselyCacheForDatabase,
|
||||||
@@ -85,11 +86,7 @@ function parseJsonCell(raw: string | null | undefined): unknown {
|
|||||||
if (!raw) {
|
if (!raw) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
return safeParseJson(raw) ?? null;
|
||||||
return JSON.parse(raw) as unknown;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PersistedAuthProfileStoreInspection =
|
type PersistedAuthProfileStoreInspection =
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
resolveExpiresAtMsFromEpochSeconds,
|
resolveExpiresAtMsFromEpochSeconds,
|
||||||
} from "@openclaw/normalization-core/number-coercion";
|
} from "@openclaw/normalization-core/number-coercion";
|
||||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||||
|
import { cancelUnreadResponseBody } from "../../infra/http-body.js";
|
||||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||||
import { readProviderJsonResponse } from "../provider-http-errors.js";
|
import { readProviderJsonResponse } from "../provider-http-errors.js";
|
||||||
import { resolveProviderRequestHeaders } from "../provider-request-config.js";
|
import { resolveProviderRequestHeaders } from "../provider-request-config.js";
|
||||||
@@ -253,12 +254,6 @@ function applyWhamCooldownResult(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelUnreadResponseBody(response: Response): Promise<void> {
|
|
||||||
if (!response.bodyUsed) {
|
|
||||||
await response.body?.cancel().catch(() => undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function probeWhamForCooldown(
|
async function probeWhamForCooldown(
|
||||||
store: AuthProfileStore,
|
store: AuthProfileStore,
|
||||||
profileId: string,
|
profileId: string,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* These references are surfaced in agent context so follow-up turns can
|
* These references are surfaced in agent context so follow-up turns can
|
||||||
* reconnect to prior long-running work.
|
* reconnect to prior long-running work.
|
||||||
*/
|
*/
|
||||||
import { truncateUtf16Safe } from "../utils.js";
|
import { truncateUtf16Safe, truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
|
||||||
import { listRunningSessions } from "./bash-process-registry.js";
|
import { listRunningSessions } from "./bash-process-registry.js";
|
||||||
import { deriveSessionName } from "./bash-tools.shared.js";
|
import { deriveSessionName } from "./bash-tools.shared.js";
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ function truncate(value: string, maxChars: number): string {
|
|||||||
if (maxChars <= 1) {
|
if (maxChars <= 1) {
|
||||||
return truncateUtf16Safe(value, maxChars);
|
return truncateUtf16Safe(value, maxChars);
|
||||||
}
|
}
|
||||||
return `${truncateUtf16Safe(value, Math.max(0, maxChars - 3))}...`;
|
return truncateWithMarker(value, maxChars, { marker: "...", reserve: 3, trimEnd: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** List active background process sessions for one scope key, newest first. */
|
/** List active background process sessions for one scope key, newest first. */
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { asNullableRecord } from "@openclaw/normalization-core/record-coerce";
|
import { asNullableRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
|
import { normalizeOptionalString as readString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import type { OperatorScope } from "../gateway/operator-scopes.js";
|
import type { OperatorScope } from "../gateway/operator-scopes.js";
|
||||||
import { renderExecUpdateText } from "./bash-tools.exec-output.js";
|
import { renderExecUpdateText } from "./bash-tools.exec-output.js";
|
||||||
import type { ExecToolDetails } from "./bash-tools.exec-types.js";
|
import type { ExecToolDetails } from "./bash-tools.exec-types.js";
|
||||||
@@ -27,10 +28,6 @@ type NodeSystemRunInvokeResult =
|
|||||||
| { ok: true; raw: unknown }
|
| { ok: true; raw: unknown }
|
||||||
| { ok: false; failure: NodeInvokeFailure };
|
| { ok: false; failure: NodeInvokeFailure };
|
||||||
|
|
||||||
function readString(value: unknown): string | undefined {
|
|
||||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Only NOT_CONNECTED plus explicit pre-dispatch provenance proves a retry cannot duplicate work. */
|
/** Only NOT_CONNECTED plus explicit pre-dispatch provenance proves a retry cannot duplicate work. */
|
||||||
function classifyNodeInvokeFailure(error: unknown): NodeInvokeFailure {
|
function classifyNodeInvokeFailure(error: unknown): NodeInvokeFailure {
|
||||||
const errorRecord = asNullableRecord(error);
|
const errorRecord = asNullableRecord(error);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { sha256Base64Url } from "../infra/crypto-digest.js";
|
import { sha256Base64Url } from "../infra/crypto-digest.js";
|
||||||
|
import { cancelUnreadResponseBody } from "../infra/http-body.js";
|
||||||
import { resolveExpiresAtMsFromDurationSeconds } from "../infra/parse-finite-number.js";
|
import { resolveExpiresAtMsFromDurationSeconds } from "../infra/parse-finite-number.js";
|
||||||
import type { OAuthCredentials } from "../llm/oauth.js";
|
import type { OAuthCredentials } from "../llm/oauth.js";
|
||||||
import { buildOAuthRequestSignal } from "../llm/utils/oauth/abort.js";
|
import { buildOAuthRequestSignal } from "../llm/utils/oauth/abort.js";
|
||||||
@@ -102,12 +103,6 @@ function resolveChutesExpiresAt(value: unknown, now: number): number | undefined
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelUnreadResponseBody(response: Response): Promise<void> {
|
|
||||||
if (!response.bodyUsed) {
|
|
||||||
await response.body?.cancel().catch(() => undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchChutesUserInfo(params: {
|
async function fetchChutesUserInfo(params: {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
fetchFn?: typeof fetch;
|
fetchFn?: typeof fetch;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import { safeParseJson } from "@openclaw/normalization-core";
|
import { safeParseJson } from "@openclaw/normalization-core";
|
||||||
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { readStringValue } from "@openclaw/normalization-core/string-coerce";
|
import { hasNonEmptyString, readStringValue } from "@openclaw/normalization-core/string-coerce";
|
||||||
import type { SourceReplyDeliveryMode } from "../auto-reply/get-reply-options.types.js";
|
import type { SourceReplyDeliveryMode } from "../auto-reply/get-reply-options.types.js";
|
||||||
import {
|
import {
|
||||||
isMessageToolConversationCreateActionName,
|
isMessageToolConversationCreateActionName,
|
||||||
@@ -32,29 +32,22 @@ const BROADCAST_SEND_ENVELOPE_KEYS = ["payload", "result", "sendResult", "toolRe
|
|||||||
const PARTIAL_DELIVERY_ENVELOPE_KEYS = [...RESULT_ENVELOPE_KEYS, "error", "cause"];
|
const PARTIAL_DELIVERY_ENVELOPE_KEYS = [...RESULT_ENVELOPE_KEYS, "error", "cause"];
|
||||||
const SESSIONS_SEND_DELIVERY_STATUSES = new Set(["accepted", "ok"]);
|
const SESSIONS_SEND_DELIVERY_STATUSES = new Set(["accepted", "ok"]);
|
||||||
const BARE_OK_DELIVERY_STATUS = "ok";
|
const BARE_OK_DELIVERY_STATUS = "ok";
|
||||||
function asRecord(value: unknown): Record<string, unknown> {
|
|
||||||
return value && typeof value === "object" && !Array.isArray(value)
|
|
||||||
? (value as Record<string, unknown>)
|
|
||||||
: {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function resultConfirmsCurrentSourceRoute(value: unknown): boolean {
|
function resultConfirmsCurrentSourceRoute(value: unknown): boolean {
|
||||||
return asRecord(asRecord(value).details).sourceReplyRoute === "current-source";
|
return (
|
||||||
}
|
(asOptionalRecord(asOptionalRecord(value)?.details) ?? {}).sourceReplyRoute === "current-source"
|
||||||
|
);
|
||||||
function hasStringValue(value: unknown): boolean {
|
|
||||||
return typeof value === "string" && value.trim().length > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasConversationIdValue(value: unknown): boolean {
|
function hasConversationIdValue(value: unknown): boolean {
|
||||||
return hasStringValue(value) || (typeof value === "number" && Number.isFinite(value));
|
return hasNonEmptyString(value) || (typeof value === "number" && Number.isFinite(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasExplicitMessageRoute(args: Record<string, unknown>): boolean {
|
function hasExplicitMessageRoute(args: Record<string, unknown>): boolean {
|
||||||
if (EXPLICIT_MESSAGE_ROUTE_KEYS.some((key) => hasStringValue(args[key]))) {
|
if (EXPLICIT_MESSAGE_ROUTE_KEYS.some((key) => hasNonEmptyString(args[key]))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return Array.isArray(args.targets) && args.targets.some((value) => hasStringValue(value));
|
return Array.isArray(args.targets) && args.targets.some((value) => hasNonEmptyString(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMessageToolSourceReplyActionName(action: unknown): boolean {
|
function isMessageToolSourceReplyActionName(action: unknown): boolean {
|
||||||
@@ -73,7 +66,7 @@ function isMessageToolSourceReplyActionName(action: unknown): boolean {
|
|||||||
|
|
||||||
/** Read the visible text delivered by a source-reply message action. */
|
/** Read the visible text delivered by a source-reply message action. */
|
||||||
export function readMessageToolSourceReplyText(args: unknown): string | undefined {
|
export function readMessageToolSourceReplyText(args: unknown): string | undefined {
|
||||||
const record = asRecord(args);
|
const record = asOptionalRecord(args) ?? {};
|
||||||
if (!isMessageToolSourceReplyActionName(record.action)) {
|
if (!isMessageToolSourceReplyActionName(record.action)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -110,7 +103,7 @@ function recordHasDeliveredMessageId(record: Record<string, unknown>): boolean {
|
|||||||
const normalized = normalizeStatus(value);
|
const normalized = normalizeStatus(value);
|
||||||
return Boolean(normalized && !NON_DELIVERY_MESSAGE_IDS.has(normalized));
|
return Boolean(normalized && !NON_DELIVERY_MESSAGE_IDS.has(normalized));
|
||||||
};
|
};
|
||||||
const message = asRecord(record.message);
|
const message = asOptionalRecord(record.message) ?? {};
|
||||||
if (
|
if (
|
||||||
hasDeliveredId(record.messageId) ||
|
hasDeliveredId(record.messageId) ||
|
||||||
hasDeliveredId(record.pollId) ||
|
hasDeliveredId(record.pollId) ||
|
||||||
@@ -504,7 +497,7 @@ export function isDeliveredMessagingToolResult(params: {
|
|||||||
hookResult?: unknown;
|
hookResult?: unknown;
|
||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
const args = asRecord(params.args);
|
const args = asOptionalRecord(params.args) ?? {};
|
||||||
const action = normalizeStatus(args.action);
|
const action = normalizeStatus(args.action);
|
||||||
if (
|
if (
|
||||||
args.dryRun === true ||
|
args.dryRun === true ||
|
||||||
@@ -595,7 +588,7 @@ export function isDeliveredMessageToolOnlySourceReplyResult(params: {
|
|||||||
if (normalizeToolName(params.toolName) !== MESSAGE_TOOL_NAME) {
|
if (normalizeToolName(params.toolName) !== MESSAGE_TOOL_NAME) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const args = asRecord(params.args);
|
const args = asOptionalRecord(params.args) ?? {};
|
||||||
const sourceRouteReplyAction =
|
const sourceRouteReplyAction =
|
||||||
(params.allowExplicitSourceRoute === true || confirmedCurrentSourceRoute) &&
|
(params.allowExplicitSourceRoute === true || confirmedCurrentSourceRoute) &&
|
||||||
isMessageToolSourceReplyActionName(args.action);
|
isMessageToolSourceReplyActionName(args.action);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { hasNonEmptyString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { normalizeMediaReferenceForComparison } from "../../media/media-reference-comparison.js";
|
import { normalizeMediaReferenceForComparison } from "../../media/media-reference-comparison.js";
|
||||||
/**
|
/**
|
||||||
* Extracts visible delivery evidence from embedded-agent run results.
|
* Extracts visible delivery evidence from embedded-agent run results.
|
||||||
@@ -97,10 +98,6 @@ export function hasCompletedTerminalDeliveryEvidence(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasNonEmptyString(value: unknown): value is string {
|
|
||||||
return typeof value === "string" && value.trim().length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasNonEmptyArray(value: unknown): boolean {
|
function hasNonEmptyArray(value: unknown): boolean {
|
||||||
return Array.isArray(value) && value.length > 0;
|
return Array.isArray(value) && value.length > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* Builds extension factories available to embedded-agent runtime sessions.
|
* Builds extension factories available to embedded-agent runtime sessions.
|
||||||
*/
|
*/
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||||
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
|
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
|
||||||
import { normalizeAcceptedSessionSpawnResult } from "../accepted-session-spawn.js";
|
import { normalizeAcceptedSessionSpawnResult } from "../accepted-session-spawn.js";
|
||||||
@@ -31,14 +32,8 @@ type AgentToolResultEvent = {
|
|||||||
isError?: boolean;
|
isError?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function recordFromUnknown(value: unknown): Record<string, unknown> {
|
|
||||||
return value && typeof value === "object" && !Array.isArray(value)
|
|
||||||
? (value as Record<string, unknown>)
|
|
||||||
: {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function snapshotToolSendReceipt(details: unknown): unknown {
|
function snapshotToolSendReceipt(details: unknown): unknown {
|
||||||
const toolSend = recordFromUnknown(details).toolSend;
|
const toolSend = (asOptionalRecord(details) ?? {}).toolSend;
|
||||||
return toolSend && typeof toolSend === "object" && !Array.isArray(toolSend)
|
return toolSend && typeof toolSend === "object" && !Array.isArray(toolSend)
|
||||||
? { ...(toolSend as Record<string, unknown>) }
|
? { ...(toolSend as Record<string, unknown>) }
|
||||||
: toolSend;
|
: toolSend;
|
||||||
@@ -66,7 +61,7 @@ function buildAgentToolResultMiddlewareFactory(
|
|||||||
});
|
});
|
||||||
return (agent) => {
|
return (agent) => {
|
||||||
agent.on("tool_result", async (rawEvent: unknown, ctx: { cwd?: string }) => {
|
agent.on("tool_result", async (rawEvent: unknown, ctx: { cwd?: string }) => {
|
||||||
const event = recordFromUnknown(rawEvent) as AgentToolResultEvent;
|
const event = (asOptionalRecord(rawEvent) ?? {}) as AgentToolResultEvent;
|
||||||
if (!event.toolName) {
|
if (!event.toolName) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -94,7 +89,7 @@ function buildAgentToolResultMiddlewareFactory(
|
|||||||
turnId: event.turnId,
|
turnId: event.turnId,
|
||||||
toolCallId,
|
toolCallId,
|
||||||
toolName: event.toolName,
|
toolName: event.toolName,
|
||||||
args: recordFromUnknown(adjustedInput ?? event.input),
|
args: asOptionalRecord(adjustedInput ?? event.input) ?? {},
|
||||||
cwd: ctx.cwd,
|
cwd: ctx.cwd,
|
||||||
isError: event.isError,
|
isError: event.isError,
|
||||||
result: current,
|
result: current,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { parseGeminiAuth } from "../../infra/gemini-auth.js";
|
import { parseGeminiAuth } from "../../infra/gemini-auth.js";
|
||||||
import { normalizeGoogleApiBaseUrl } from "../../infra/google-api-base-url.js";
|
import { normalizeGoogleApiBaseUrl } from "../../infra/google-api-base-url.js";
|
||||||
import { readResponseWithLimit } from "../../infra/http-body.js";
|
import { cancelUnreadResponseBody, readResponseWithLimit } from "../../infra/http-body.js";
|
||||||
import { streamWithPayloadPatch } from "../../llm/providers/stream-wrappers/stream-payload-utils.js";
|
import { streamWithPayloadPatch } from "../../llm/providers/stream-wrappers/stream-payload-utils.js";
|
||||||
import type { Model } from "../../llm/types.js";
|
import type { Model } from "../../llm/types.js";
|
||||||
import { isSecretValueRegisteredForRedaction } from "../../logging/secret-redaction-registry.js";
|
import { isSecretValueRegisteredForRedaction } from "../../logging/secret-redaction-registry.js";
|
||||||
@@ -286,12 +286,6 @@ function buildManagedContextForCachedContent(context: GooglePromptCacheContext)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelUnreadResponseBody(response: Response | undefined): Promise<void> {
|
|
||||||
if (response && !response.bodyUsed) {
|
|
||||||
await response.body?.cancel().catch(() => undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads a Google cachedContents JSON body under a byte cap and parses it.
|
* Reads a Google cachedContents JSON body under a byte cap and parses it.
|
||||||
* Streams through the shared limiter so an oversized response is cancelled
|
* Streams through the shared limiter so an oversized response is cancelled
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { hasNonEmptyString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import {
|
import {
|
||||||
isSilentReplyPayloadText,
|
isSilentReplyPayloadText,
|
||||||
isSilentReplyText,
|
isSilentReplyText,
|
||||||
@@ -24,10 +25,6 @@ type PayloadVisibilityOptions = {
|
|||||||
includeSilentReplyPayloads?: boolean;
|
includeSilentReplyPayloads?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function hasNonEmptyString(value: unknown): value is string {
|
|
||||||
return typeof value === "string" && value.trim().length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasNonEmptyStringArray(value: unknown): boolean {
|
function hasNonEmptyStringArray(value: unknown): boolean {
|
||||||
return Array.isArray(value) && value.some(hasNonEmptyString);
|
return Array.isArray(value) && value.some(hasNonEmptyString);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { asOptionalRecord as readModelParams } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||||
import type { ModelCompatConfig, ModelMediaInputConfig } from "../../config/types.models.js";
|
import type { ModelCompatConfig, ModelMediaInputConfig } from "../../config/types.models.js";
|
||||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||||
@@ -228,13 +229,6 @@ export function hasConfiguredFallbackSurface(params: {
|
|||||||
return Boolean(params.providerConfig?.baseUrl?.trim());
|
return Boolean(params.providerConfig?.baseUrl?.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
function readModelParams(value: unknown): Record<string, unknown> | undefined {
|
|
||||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return value as Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeModelParams(
|
function mergeModelParams(
|
||||||
...entries: Array<Record<string, unknown> | undefined>
|
...entries: Array<Record<string, unknown> | undefined>
|
||||||
): Record<string, unknown> | undefined {
|
): Record<string, unknown> | undefined {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { formatErrorMessage } from "../../infra/errors.js";
|
import { formatErrorMessage } from "../../infra/errors.js";
|
||||||
import { readResponseWithLimit } from "../../infra/http-body.js";
|
import { cancelUnreadResponseBody, readResponseWithLimit } from "../../infra/http-body.js";
|
||||||
import { resolveProxyFetchFromEnv } from "../../infra/net/proxy-fetch.js";
|
import { resolveProxyFetchFromEnv } from "../../infra/net/proxy-fetch.js";
|
||||||
import { parseStrictFiniteNumber } from "../../infra/parse-finite-number.js";
|
import { parseStrictFiniteNumber } from "../../infra/parse-finite-number.js";
|
||||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||||
@@ -177,12 +177,6 @@ function parseModel(model: OpenRouterApiModel): OpenRouterModelCapabilities {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelUnreadResponseBody(response: Response | undefined): Promise<void> {
|
|
||||||
if (response && !response.bodyUsed) {
|
|
||||||
await response.body?.cancel().catch(() => undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// API fetch
|
// API fetch
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* Sanitizes and validates replayed session history before model calls.
|
* Sanitizes and validates replayed session history before model calls.
|
||||||
*/
|
*/
|
||||||
import { isDeepStrictEqual } from "node:util";
|
import { isDeepStrictEqual } from "node:util";
|
||||||
|
import { asFiniteNumber as toFiniteCostNumber } from "@openclaw/normalization-core/number-coercion";
|
||||||
import { stripInternalMetadataForDisplay } from "../../auto-reply/reply/display-text-sanitize.js";
|
import { stripInternalMetadataForDisplay } from "../../auto-reply/reply/display-text-sanitize.js";
|
||||||
import { isSilentReplyPayloadText, SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js";
|
import { isSilentReplyPayloadText, SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js";
|
||||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||||
@@ -522,10 +523,6 @@ function normalizeAssistantUsageCost(usage: unknown): AssistantUsageSnapshot["co
|
|||||||
return { input, output, cacheRead, cacheWrite, total, ...(totalOrigin ? { totalOrigin } : {}) };
|
return { input, output, cacheRead, cacheWrite, total, ...(totalOrigin ? { totalOrigin } : {}) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function toFiniteCostNumber(value: unknown): number | undefined {
|
|
||||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureAssistantUsageSnapshots(messages: AgentMessage[]): AgentMessage[] {
|
function ensureAssistantUsageSnapshots(messages: AgentMessage[]): AgentMessage[] {
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
return messages;
|
return messages;
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
* Normalizes tool-call names, ids, and standalone text calls for providers.
|
* Normalizes tool-call names, ids, and standalone text calls for providers.
|
||||||
*/
|
*/
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { normalizeLowercaseStringOrEmpty } from "../../../../packages/normalization-core/src/string-coerce.js";
|
import {
|
||||||
|
hasNonEmptyString as replayToolCallNonEmptyString,
|
||||||
|
normalizeLowercaseStringOrEmpty,
|
||||||
|
} from "../../../../packages/normalization-core/src/string-coerce.js";
|
||||||
import { normalizeStringEntries } from "../../../../packages/normalization-core/src/string-normalization.js";
|
import { normalizeStringEntries } from "../../../../packages/normalization-core/src/string-normalization.js";
|
||||||
import {
|
import {
|
||||||
createPromotedPlainTextToolCallEvents,
|
createPromotedPlainTextToolCallEvents,
|
||||||
@@ -336,10 +339,6 @@ function collectFollowingToolResults(
|
|||||||
return { ids, displaced };
|
return { ids, displaced };
|
||||||
}
|
}
|
||||||
|
|
||||||
function replayToolCallNonEmptyString(value: unknown): value is string {
|
|
||||||
return typeof value === "string" && value.trim().length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveReplayToolCallName(
|
function resolveReplayToolCallName(
|
||||||
rawName: string,
|
rawName: string,
|
||||||
rawId: string,
|
rawId: string,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import type { LlmRuntime } from "@openclaw/ai";
|
import type { LlmRuntime } from "@openclaw/ai";
|
||||||
import { stripSystemPromptCacheBoundary } from "@openclaw/ai/internal/shared";
|
import { stripSystemPromptCacheBoundary } from "@openclaw/ai/internal/shared";
|
||||||
import { createBoundaryAwareStreamFnForModel } from "@openclaw/ai/transports";
|
import { createBoundaryAwareStreamFnForModel } from "@openclaw/ai/transports";
|
||||||
|
import { hasNonEmptyString as hasResolvedRuntimeApiKey } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { getStreamLlmRuntime } from "../../llm/model-runtime-binding.js";
|
import { getStreamLlmRuntime } from "../../llm/model-runtime-binding.js";
|
||||||
import "../ai-transport-runtime-host.js";
|
import "../ai-transport-runtime-host.js";
|
||||||
import { createAnthropicVertexStreamFnForModel } from "../anthropic-vertex-stream.js";
|
import { createAnthropicVertexStreamFnForModel } from "../anthropic-vertex-stream.js";
|
||||||
@@ -69,10 +70,6 @@ function isDefaultOpenClawStreamFnForModel(
|
|||||||
return streamFn === provider?.streamSimple || streamFn === provider?.stream;
|
return streamFn === provider?.streamSimple || streamFn === provider?.stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasResolvedRuntimeApiKey(apiKey: string | undefined): boolean {
|
|
||||||
return typeof apiKey === "string" && apiKey.trim().length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOpenAICodexResponsesModel(model: EmbeddedRunAttemptParams["model"]): boolean {
|
function isOpenAICodexResponsesModel(model: EmbeddedRunAttemptParams["model"]): boolean {
|
||||||
return model.provider === "openai" && model.api === "openai-chatgpt-responses";
|
return model.provider === "openai" && model.api === "openai-chatgpt-responses";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
import { truncateUtf16Safe, truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
|
||||||
import type {
|
import type {
|
||||||
JsonValue,
|
JsonValue,
|
||||||
NativeHookRelayEvent,
|
NativeHookRelayEvent,
|
||||||
@@ -213,8 +213,5 @@ function snapshotString(value: string, state: { remainingStringLength: number })
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function truncateText(value: string, maxLength: number): string {
|
export function truncateText(value: string, maxLength: number): string {
|
||||||
if (value.length <= maxLength) {
|
return truncateWithMarker(value, maxLength, { marker: "...", reserve: 3, trimEnd: false });
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return `${truncateUtf16Safe(value, Math.max(0, maxLength - 3))}...`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||||
|
import { normalizeOptionalString as readStringParam } from "@openclaw/normalization-core/string-coerce";
|
||||||
import {
|
import {
|
||||||
resolveMergedModelProviderConfig,
|
resolveMergedModelProviderConfig,
|
||||||
resolveMergedModelProviderModels,
|
resolveMergedModelProviderModels,
|
||||||
@@ -258,10 +259,6 @@ function isSupportedHarness(entry: {
|
|||||||
return entry.support.supported;
|
return entry.support.supported;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readStringParam(value: unknown): string | undefined {
|
|
||||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeModelId(provider: string, modelId: string): string {
|
function normalizeModelId(provider: string, modelId: string): string {
|
||||||
const trimmed = modelId.trim();
|
const trimmed = modelId.trim();
|
||||||
const slashIndex = trimmed.indexOf("/");
|
const slashIndex = trimmed.indexOf("/");
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
normalizeProviderId,
|
normalizeProviderId,
|
||||||
normalizeProviderIdForAuth,
|
normalizeProviderIdForAuth,
|
||||||
} from "@openclaw/model-catalog-core/provider-id";
|
} from "@openclaw/model-catalog-core/provider-id";
|
||||||
|
import { hasNonEmptyString as hasSecret } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
||||||
import { resolveMergedModelProviderConfig } from "../config/model-provider-config.js";
|
import { resolveMergedModelProviderConfig } from "../config/model-provider-config.js";
|
||||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||||
@@ -128,10 +129,6 @@ type AuthSourceEvaluation = Pick<
|
|||||||
"availability" | "selectedAuthMode" | "evidence" | "selectedProfileId"
|
"availability" | "selectedAuthMode" | "evidence" | "selectedProfileId"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
function hasSecret(value: unknown): value is string {
|
|
||||||
return typeof value === "string" && value.trim().length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function modeAllowed(provider: string, target: AuthTarget, mode: string | undefined): boolean {
|
function modeAllowed(provider: string, target: AuthTarget, mode: string | undefined): boolean {
|
||||||
const requirement = resolveProviderModelRouteAuthRequirement(mode);
|
const requirement = resolveProviderModelRouteAuthRequirement(mode);
|
||||||
return target.authRequirement
|
return target.authRequirement
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { normalizeLowercaseStringOrEmpty as normalizeApi } from "@openclaw/normalization-core/string-coerce";
|
||||||
import type { ModelCompatConfig } from "../config/types.models.js";
|
import type { ModelCompatConfig } from "../config/types.models.js";
|
||||||
|
|
||||||
type ModelTransportRoute = {
|
type ModelTransportRoute = {
|
||||||
@@ -5,10 +6,6 @@ type ModelTransportRoute = {
|
|||||||
baseUrl?: unknown;
|
baseUrl?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeApi(value: unknown): string {
|
|
||||||
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeBaseUrl(value: unknown): string {
|
function normalizeBaseUrl(value: unknown): string {
|
||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import pMap from "p-map";
|
import pMap from "p-map";
|
||||||
import { Type } from "typebox";
|
import { Type } from "typebox";
|
||||||
import { formatErrorMessage } from "../infra/errors.js";
|
import { formatErrorMessage } from "../infra/errors.js";
|
||||||
|
import { cancelUnreadResponseBody } from "../infra/http-body.js";
|
||||||
/**
|
/**
|
||||||
* Scans remote provider model catalogs for configured providers.
|
* Scans remote provider model catalogs for configured providers.
|
||||||
*/
|
*/
|
||||||
@@ -284,9 +285,7 @@ async function fetchOpenRouterModels(
|
|||||||
"OpenRouter model scan",
|
"OpenRouter model scan",
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
if (res && !res.bodyUsed) {
|
await cancelUnreadResponseBody(res);
|
||||||
await res.body?.cancel().catch(() => undefined);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
* store preserves typed columns for hot delivery state while retaining the
|
* store preserves typed columns for hot delivery state while retaining the
|
||||||
* normalized payload JSON for forward-compatible record hydration.
|
* normalized payload JSON for forward-compatible record hydration.
|
||||||
*/
|
*/
|
||||||
|
import { safeParseJson } from "@openclaw/normalization-core";
|
||||||
|
import { asFiniteNumber as normalizeFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { sql, type Insertable, type Selectable, type Updateable } from "kysely";
|
import { sql, type Insertable, type Selectable, type Updateable } from "kysely";
|
||||||
import { executeSqliteQuerySync, getNodeSqliteKysely } from "../infra/kysely-sync.js";
|
import { executeSqliteQuerySync, getNodeSqliteKysely } from "../infra/kysely-sync.js";
|
||||||
@@ -82,21 +84,13 @@ function parseJson(raw: string | null): unknown {
|
|||||||
if (!raw) {
|
if (!raw) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
try {
|
return safeParseJson(raw);
|
||||||
return JSON.parse(raw);
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function boolToSqlite(value: boolean | undefined): number | null {
|
function boolToSqlite(value: boolean | undefined): number | null {
|
||||||
return value === undefined ? null : value ? 1 : 0;
|
return value === undefined ? null : value ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeFiniteNumber(value: number | null): number | undefined {
|
|
||||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Rehydrates one sqlite row into the normalized subagent run record shape. */
|
/** Rehydrates one sqlite row into the normalized subagent run record shape. */
|
||||||
function rowToSubagentRunRecord(row: SubagentRunSqliteRow): SubagentRunRecord | null {
|
function rowToSubagentRunRecord(row: SubagentRunSqliteRow): SubagentRunRecord | null {
|
||||||
const payload = parseJson(row.payload_json);
|
const payload = parseJson(row.payload_json);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Persists runtime tool-schema quarantines in the shared SQLite-backed core
|
// Persists runtime tool-schema quarantines in the shared SQLite-backed core
|
||||||
// plugin-state store so health surfaces can see failures from any live
|
// plugin-state store so health surfaces can see failures from any live
|
||||||
// runtime process.
|
// runtime process.
|
||||||
|
import { hasNonEmptyString as isNonEmptyString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import {
|
import {
|
||||||
createRuntimeHealthRecordEnvelope,
|
createRuntimeHealthRecordEnvelope,
|
||||||
createRuntimeHealthStore,
|
createRuntimeHealthStore,
|
||||||
@@ -20,10 +21,6 @@ type PersistedRuntimeToolSchemaQuarantineRecord = RuntimeHealthRecordEnvelope &
|
|||||||
reason: string;
|
reason: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function isNonEmptyString(value: unknown): value is string {
|
|
||||||
return typeof value === "string" && value.trim().length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const quarantineStore = createRuntimeHealthStore<PersistedRuntimeToolSchemaQuarantineRecord>({
|
const quarantineStore = createRuntimeHealthStore<PersistedRuntimeToolSchemaQuarantineRecord>({
|
||||||
ownerId: "core:runtime-tool-quarantine-health",
|
ownerId: "core:runtime-tool-quarantine-health",
|
||||||
namespace: "schema-quarantines",
|
namespace: "schema-quarantines",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* Recovers flat or partial model/tool inputs into the structured cron job/patch shape.
|
* Recovers flat or partial model/tool inputs into the structured cron job/patch shape.
|
||||||
*/
|
*/
|
||||||
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
|
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
|
||||||
|
import { hasNonEmptyString as isNonEmptyString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { isRecord } from "../../utils.js";
|
import { isRecord } from "../../utils.js";
|
||||||
import { isStringOption } from "../../utils/string-readers.js";
|
import { isStringOption } from "../../utils/string-readers.js";
|
||||||
|
|
||||||
@@ -75,10 +76,6 @@ function isCronPayloadKind(value: unknown): value is (typeof CRON_PAYLOAD_KINDS)
|
|||||||
return value === "systemEvent" || value === "agentTurn" || value === "script";
|
return value === "systemEvent" || value === "agentTurn" || value === "script";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNonEmptyString(value: unknown): value is string {
|
|
||||||
return typeof value === "string" && value.trim().length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isStringArrayOrNull(value: unknown): boolean {
|
function isStringArrayOrNull(value: unknown): boolean {
|
||||||
return (
|
return (
|
||||||
value === null || (Array.isArray(value) && value.every((entry) => typeof entry === "string"))
|
value === null || (Array.isArray(value) && value.every((entry) => typeof entry === "string"))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
|
||||||
/** Reminder-context projection for cron tool job creation. */
|
/** Reminder-context projection for cron tool job creation. */
|
||||||
import { getRuntimeConfig } from "../../config/config.js";
|
import { getRuntimeConfig } from "../../config/config.js";
|
||||||
import { extractTextFromChatContent } from "../../shared/chat-content.js";
|
import { extractTextFromChatContent } from "../../shared/chat-content.js";
|
||||||
import { truncateUtf16Safe } from "../../utils.js";
|
|
||||||
import { REMINDER_CONTEXT_MESSAGES_MAX } from "./cron-tool-schema.js";
|
import { REMINDER_CONTEXT_MESSAGES_MAX } from "./cron-tool-schema.js";
|
||||||
import type { ChatMessage, GatewayToolCaller } from "./cron-tool.types.js";
|
import type { ChatMessage, GatewayToolCaller } from "./cron-tool.types.js";
|
||||||
import type { GatewayCallOptions } from "./gateway.js";
|
import type { GatewayCallOptions } from "./gateway.js";
|
||||||
@@ -20,11 +20,7 @@ export function stripExistingContext(text: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function truncateText(input: string, maxLen: number) {
|
function truncateText(input: string, maxLen: number) {
|
||||||
if (input.length <= maxLen) {
|
return truncateWithMarker(input, maxLen, { marker: "...", reserve: 3, trimEnd: true });
|
||||||
return input;
|
|
||||||
}
|
|
||||||
const truncated = truncateUtf16Safe(input, Math.max(0, maxLen - 3)).trimEnd();
|
|
||||||
return `${truncated}...`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractMessageText(message: ChatMessage): { role: string; text: string } | null {
|
function extractMessageText(message: ChatMessage): { role: string; text: string } | null {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* Manages live capture, manual import, summarization, and process-local transcript sessions.
|
* Manages live capture, manual import, summarization, and process-local transcript sessions.
|
||||||
*/
|
*/
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
|
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
|
||||||
import { Type } from "typebox";
|
import { Type } from "typebox";
|
||||||
import { resolveStateDir } from "../../config/paths.js";
|
import { resolveStateDir } from "../../config/paths.js";
|
||||||
@@ -59,12 +60,6 @@ function ownsTranscriptSession(
|
|||||||
return ctx.agentId === "main";
|
return ctx.agentId === "main";
|
||||||
}
|
}
|
||||||
|
|
||||||
function asParamsRecord(params: unknown): Record<string, unknown> {
|
|
||||||
return params && typeof params === "object" && !Array.isArray(params)
|
|
||||||
? (params as Record<string, unknown>)
|
|
||||||
: {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const TranscriptsSchema = Type.Object(
|
const TranscriptsSchema = Type.Object(
|
||||||
{
|
{
|
||||||
action: Type.String({
|
action: Type.String({
|
||||||
@@ -356,7 +351,7 @@ export function createTranscriptsTool(options?: {
|
|||||||
if (!config.enabled) {
|
if (!config.enabled) {
|
||||||
throw new Error("transcripts are disabled");
|
throw new Error("transcripts are disabled");
|
||||||
}
|
}
|
||||||
const params = asParamsRecord(rawParams);
|
const params = asOptionalRecord(rawParams) ?? {};
|
||||||
const action = readStringParam(params, "action", { required: true, trim: true });
|
const action = readStringParam(params, "action", { required: true, trim: true });
|
||||||
const store = createStore(ctx);
|
const store = createStore(ctx);
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
* re-wrapped here unconditionally, so no provider-controlled metadata can
|
* re-wrapped here unconditionally, so no provider-controlled metadata can
|
||||||
* spoof the trust marker and transport-specific extras never reach the model.
|
* spoof the trust marker and transport-specific extras never reach the model.
|
||||||
*/
|
*/
|
||||||
|
import { asFiniteNumber as readFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||||
import type { Static } from "typebox";
|
import type { Static } from "typebox";
|
||||||
@@ -119,10 +120,6 @@ function unwrapEnvelopes(value: string): string {
|
|||||||
return value.replace(ENVELOPE_OPEN_RE, "").replace(ENVELOPE_END_RE, "").trim();
|
return value.replace(ENVELOPE_OPEN_RE, "").replace(ENVELOPE_END_RE, "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function readFiniteNumber(value: unknown): number | undefined {
|
|
||||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// URLs are emitted canonicalized (percent-encoded), so whitespace or readable
|
// URLs are emitted canonicalized (percent-encoded), so whitespace or readable
|
||||||
// prose smuggled into a URL slot cannot ride outside the envelope as-is.
|
// prose smuggled into a URL slot cannot ride outside the envelope as-is.
|
||||||
function toHttpUrl(value: string): string | undefined {
|
function toHttpUrl(value: string): string | undefined {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import type { ReadableStream as NodeReadableStream } from "node:stream/web";
|
|||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { extractArchive } from "../../infra/archive.js";
|
import { extractArchive } from "../../infra/archive.js";
|
||||||
import { isTruthyEnvValue } from "../../infra/env.js";
|
import { isTruthyEnvValue } from "../../infra/env.js";
|
||||||
|
import { cancelUnreadResponseBody } from "../../infra/http-body.js";
|
||||||
import { fetchWithSsrFGuard } from "../../infra/net/fetch-guard.js";
|
import { fetchWithSsrFGuard } from "../../infra/net/fetch-guard.js";
|
||||||
import { APP_NAME, getBinDir } from "../config.js";
|
import { APP_NAME, getBinDir } from "../config.js";
|
||||||
import { readProviderJsonResponse } from "../provider-http-errors.js";
|
import { readProviderJsonResponse } from "../provider-http-errors.js";
|
||||||
@@ -36,12 +37,6 @@ const ARCHIVE_EXTRACT_TIMEOUT_MS = 60_000;
|
|||||||
const CONTENT_LENGTH_RE = /^\d+$/;
|
const CONTENT_LENGTH_RE = /^\d+$/;
|
||||||
const GITHUB_RELEASE_JSON_MAX_BYTES = 1024 * 1024;
|
const GITHUB_RELEASE_JSON_MAX_BYTES = 1024 * 1024;
|
||||||
|
|
||||||
async function cancelUnreadResponseBody(response: Response): Promise<void> {
|
|
||||||
if (!response.bodyUsed) {
|
|
||||||
await response.body?.cancel().catch(() => undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOfflineModeEnabled(): boolean {
|
function isOfflineModeEnabled(): boolean {
|
||||||
return isTruthyEnvValue(process.env.OPENCLAW_OFFLINE);
|
return isTruthyEnvValue(process.env.OPENCLAW_OFFLINE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// unintentionally breaking on newlines. Using [\s\S] keeps newlines inside
|
// unintentionally breaking on newlines. Using [\s\S] keeps newlines inside
|
||||||
// the chunk so messages are only split when they truly exceed the limit.
|
// the chunk so messages are only split when they truly exceed the limit.
|
||||||
|
|
||||||
import { resolveIntegerOption } from "@openclaw/normalization-core/number-coercion";
|
|
||||||
import {
|
import {
|
||||||
findFenceSpanAt,
|
findFenceSpanAt,
|
||||||
isSafeFenceBreak,
|
isSafeFenceBreak,
|
||||||
@@ -16,6 +15,7 @@ import { normalizeAccountId } from "../routing/session-key.js";
|
|||||||
import {
|
import {
|
||||||
avoidTrailingHighSurrogateBreak,
|
avoidTrailingHighSurrogateBreak,
|
||||||
chunkTextByBreakResolver,
|
chunkTextByBreakResolver,
|
||||||
|
normalizeChunkLimit,
|
||||||
} from "../shared/text-chunking.js";
|
} from "../shared/text-chunking.js";
|
||||||
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel-constants.js";
|
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel-constants.js";
|
||||||
|
|
||||||
@@ -33,11 +33,6 @@ export type ChunkMode = "length" | "newline";
|
|||||||
const DEFAULT_CHUNK_LIMIT = 4000;
|
const DEFAULT_CHUNK_LIMIT = 4000;
|
||||||
const DEFAULT_CHUNK_MODE: ChunkMode = "length";
|
const DEFAULT_CHUNK_MODE: ChunkMode = "length";
|
||||||
|
|
||||||
function normalizeChunkLimit(limit: number): number {
|
|
||||||
// String slicing truncates fractional indexes, so positive limits need an integer progress step.
|
|
||||||
return Number.isFinite(limit) && limit > 0 ? resolveIntegerOption(limit, 1, { min: 1 }) : limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProviderChunkConfig = {
|
type ProviderChunkConfig = {
|
||||||
textChunkLimit?: number;
|
textChunkLimit?: number;
|
||||||
streaming?: unknown;
|
streaming?: unknown;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
normalizeOptionalLowercaseString,
|
normalizeOptionalLowercaseString,
|
||||||
normalizeOptionalString,
|
normalizeOptionalString,
|
||||||
} from "@openclaw/normalization-core/string-coerce";
|
} from "@openclaw/normalization-core/string-coerce";
|
||||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
import { truncateUtf16Safe, truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
|
||||||
import { resolveAcpToolTerminalOutcome } from "../../acp/tool-status.js";
|
import { resolveAcpToolTerminalOutcome } from "../../acp/tool-status.js";
|
||||||
import { EmbeddedBlockChunker } from "../../agents/embedded-agent-block-chunker.js";
|
import { EmbeddedBlockChunker } from "../../agents/embedded-agent-block-chunker.js";
|
||||||
import { formatToolSummary, resolveToolDisplay } from "../../agents/tool-display.js";
|
import { formatToolSummary, resolveToolDisplay } from "../../agents/tool-display.js";
|
||||||
@@ -53,7 +53,7 @@ function truncateText(input: string, maxChars: number): string {
|
|||||||
if (maxChars <= 1) {
|
if (maxChars <= 1) {
|
||||||
return truncateUtf16Safe(input, maxChars);
|
return truncateUtf16Safe(input, maxChars);
|
||||||
}
|
}
|
||||||
return `${truncateUtf16Safe(input, maxChars - 1)}…`;
|
return truncateWithMarker(input, maxChars, { marker: "…", reserve: 1, trimEnd: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
function hashText(text: string): string {
|
function hashText(text: string): string {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { asFiniteNumber as readFiniteNumberValue } from "@openclaw/normalization-core/number-coercion";
|
||||||
|
import { asOptionalRecord as readRecordValue } from "@openclaw/normalization-core/record-coerce";
|
||||||
import {
|
import {
|
||||||
normalizeLowercaseStringOrEmpty,
|
normalizeLowercaseStringOrEmpty,
|
||||||
readStringValue,
|
readStringValue,
|
||||||
@@ -5,12 +7,6 @@ import {
|
|||||||
import { inferToolMetaFromArgs } from "../../agents/embedded-agent-utils.js";
|
import { inferToolMetaFromArgs } from "../../agents/embedded-agent-utils.js";
|
||||||
import type { GetReplyOptions } from "../types.js";
|
import type { GetReplyOptions } from "../types.js";
|
||||||
|
|
||||||
function readRecordValue(value: unknown): Record<string, unknown> | undefined {
|
|
||||||
return value && typeof value === "object" && !Array.isArray(value)
|
|
||||||
? (value as Record<string, unknown>)
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLI backends report a tool result as its raw content: a string, or the text
|
* CLI backends report a tool result as its raw content: a string, or the text
|
||||||
* blocks the harness streamed. Structured runners send a record instead, so the
|
* blocks the harness streamed. Structured runners send a record instead, so the
|
||||||
@@ -32,10 +28,6 @@ function readToolResultText(value: unknown): string | undefined {
|
|||||||
return text || undefined;
|
return text || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readFiniteNumberValue(value: unknown): number | undefined {
|
|
||||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readNullableNumberValue(value: unknown): number | null | undefined {
|
function readNullableNumberValue(value: unknown): number | null | undefined {
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { toAcpRuntimeErrorText } from "@openclaw/acp-core/runtime/error-text";
|
import { toAcpRuntimeErrorText } from "@openclaw/acp-core/runtime/error-text";
|
||||||
import type { AcpRuntimeSessionMode } from "@openclaw/acp-core/runtime/types";
|
import type { AcpRuntimeSessionMode } from "@openclaw/acp-core/runtime/types";
|
||||||
|
import type { Result } from "@openclaw/normalization-core/result";
|
||||||
import {
|
import {
|
||||||
normalizeOptionalLowercaseString,
|
normalizeOptionalLowercaseString,
|
||||||
normalizeOptionalString,
|
normalizeOptionalString,
|
||||||
@@ -183,7 +184,7 @@ function resolveDefaultSpawnThreadMode(params: HandleCommandsParams): AcpSpawnTh
|
|||||||
export function parseSpawnInput(
|
export function parseSpawnInput(
|
||||||
params: HandleCommandsParams,
|
params: HandleCommandsParams,
|
||||||
tokens: string[],
|
tokens: string[],
|
||||||
): { ok: true; value: ParsedSpawnInput } | { ok: false; error: string } {
|
): Result<ParsedSpawnInput, string> {
|
||||||
const normalizedTokens = tokens.map((token) => normalizeAcpOptionToken(token));
|
const normalizedTokens = tokens.map((token) => normalizeAcpOptionToken(token));
|
||||||
let mode: AcpRuntimeSessionMode = "persistent";
|
let mode: AcpRuntimeSessionMode = "persistent";
|
||||||
let thread = resolveDefaultSpawnThreadMode(params);
|
let thread = resolveDefaultSpawnThreadMode(params);
|
||||||
@@ -323,9 +324,7 @@ export function parseSpawnInput(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseSteerInput(
|
export function parseSteerInput(tokens: string[]): Result<ParsedSteerInput, string> {
|
||||||
tokens: string[],
|
|
||||||
): { ok: true; value: ParsedSteerInput } | { ok: false; error: string } {
|
|
||||||
const normalizedTokens = tokens.map((token) => normalizeAcpOptionToken(token));
|
const normalizedTokens = tokens.map((token) => normalizeAcpOptionToken(token));
|
||||||
let sessionToken: string | undefined;
|
let sessionToken: string | undefined;
|
||||||
const instructionTokens: string[] = [];
|
const instructionTokens: string[] = [];
|
||||||
@@ -372,7 +371,7 @@ export function parseSteerInput(
|
|||||||
export function parseSingleValueCommandInput(
|
export function parseSingleValueCommandInput(
|
||||||
tokens: string[],
|
tokens: string[],
|
||||||
usage: string,
|
usage: string,
|
||||||
): { ok: true; value: ParsedSingleValueCommandInput } | { ok: false; error: string } {
|
): Result<ParsedSingleValueCommandInput, string> {
|
||||||
const value = normalizeOptionalString(tokens[0]) ?? "";
|
const value = normalizeOptionalString(tokens[0]) ?? "";
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return { ok: false, error: usage };
|
return { ok: false, error: usage };
|
||||||
@@ -390,9 +389,7 @@ export function parseSingleValueCommandInput(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseSetCommandInput(
|
export function parseSetCommandInput(tokens: string[]): Result<ParsedSetCommandInput, string> {
|
||||||
tokens: string[],
|
|
||||||
): { ok: true; value: ParsedSetCommandInput } | { ok: false; error: string } {
|
|
||||||
const key = normalizeOptionalString(tokens[0]) ?? "";
|
const key = normalizeOptionalString(tokens[0]) ?? "";
|
||||||
const value = normalizeOptionalString(tokens[1]) ?? "";
|
const value = normalizeOptionalString(tokens[1]) ?? "";
|
||||||
if (!key || !value) {
|
if (!key || !value) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type FSWatcher, readFileSync, watch } from "node:fs";
|
import { type FSWatcher, readFileSync, watch } from "node:fs";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { isAbsolute, resolve } from "node:path";
|
import { isAbsolute, resolve } from "node:path";
|
||||||
|
import { isRecord as isPlainObject } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { createDedupeCache } from "../../infra/dedupe.js";
|
import { createDedupeCache } from "../../infra/dedupe.js";
|
||||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||||
import { DEFAULT_USAGE_BAR_TEMPLATE } from "./default-template.js";
|
import { DEFAULT_USAGE_BAR_TEMPLATE } from "./default-template.js";
|
||||||
@@ -31,10 +32,6 @@ function expandPath(p: string): string {
|
|||||||
return isAbsolute(p) ? p : resolve(p);
|
return isAbsolute(p) ? p : resolve(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasPieces(value: unknown): boolean {
|
function hasPieces(value: unknown): boolean {
|
||||||
return Array.isArray(value) && value.some(isPlainObject);
|
return Array.isArray(value) && value.some(isPlainObject);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
asSafeIntegerInRange,
|
asSafeIntegerInRange,
|
||||||
expectDefined,
|
expectDefined,
|
||||||
|
isRecord as isObject,
|
||||||
parseStrictInteger,
|
parseStrictInteger,
|
||||||
} from "@openclaw/normalization-core";
|
} from "@openclaw/normalization-core";
|
||||||
export type UsageBarTemplate = Record<string, unknown>;
|
export type UsageBarTemplate = Record<string, unknown>;
|
||||||
export type UsageContract = Record<string, unknown>;
|
export type UsageContract = Record<string, unknown>;
|
||||||
type Vocab = Record<string, unknown>;
|
type Vocab = Record<string, unknown>;
|
||||||
|
|
||||||
const isObject = (v: unknown): v is Record<string, unknown> =>
|
|
||||||
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
||||||
|
|
||||||
function toGlyphs(scale: unknown): string[] {
|
function toGlyphs(scale: unknown): string[] {
|
||||||
if (Array.isArray(scale)) {
|
if (Array.isArray(scale)) {
|
||||||
return scale.filter((g): g is string => typeof g === "string");
|
return scale.filter((g): g is string => typeof g === "string");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*
|
*
|
||||||
* Reads, writes, migrates, and normalizes direct-message policy and allowFrom fields.
|
* Reads, writes, migrates, and normalizes direct-message policy and allowFrom fields.
|
||||||
*/
|
*/
|
||||||
|
import { asNullableRecord as asObjectRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
|
import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,12 +53,6 @@ export function normalizeChannelDmPolicy(value: string | undefined): ChannelDmPo
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function asObjectRecord(value: unknown): DmAccessRecord | null {
|
|
||||||
return value && typeof value === "object" && !Array.isArray(value)
|
|
||||||
? (value as DmAccessRecord)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cloneDm(entry: DmAccessRecord): DmAccessRecord | null {
|
function cloneDm(entry: DmAccessRecord): DmAccessRecord | null {
|
||||||
const dm = asObjectRecord(entry.dm);
|
const dm = asObjectRecord(entry.dm);
|
||||||
return dm ? { ...dm } : null;
|
return dm ? { ...dm } : null;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { expectDefined } from "@openclaw/normalization-core";
|
import { expectDefined } from "@openclaw/normalization-core";
|
||||||
// Channel streaming config normalization and progress-draft formatting helpers.
|
// Channel streaming config normalization and progress-draft formatting helpers.
|
||||||
|
import { asNullableRecord as asObjectRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { normalizeTrimmedStringList } from "@openclaw/normalization-core/string-normalization";
|
import { normalizeTrimmedStringList } from "@openclaw/normalization-core/string-normalization";
|
||||||
import {
|
import {
|
||||||
@@ -40,12 +41,6 @@ export type { SlackChannelStreamingConfig } from "../config/types.slack.js";
|
|||||||
|
|
||||||
// Runtime reads are nested-only; doctor migrates legacy streaming spellings.
|
// Runtime reads are nested-only; doctor migrates legacy streaming spellings.
|
||||||
|
|
||||||
function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
|
||||||
return value && typeof value === "object" && !Array.isArray(value)
|
|
||||||
? (value as Record<string, unknown>)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function asInteger(value: unknown): number | undefined {
|
function asInteger(value: unknown): number | undefined {
|
||||||
return typeof value === "number" && Number.isInteger(value) ? value : undefined;
|
return typeof value === "number" && Number.isInteger(value) ? value : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { isRecord as isObjectRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import {
|
import {
|
||||||
normalizeLowercaseStringOrEmpty,
|
normalizeLowercaseStringOrEmpty,
|
||||||
normalizeOptionalString,
|
normalizeOptionalString,
|
||||||
@@ -401,10 +402,6 @@ function buildTtsConfigWithHydratedProvider(params: {
|
|||||||
return tts;
|
return tts;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
||||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function ttsProviderConfigHasApiKey(value: unknown): boolean {
|
function ttsProviderConfigHasApiKey(value: unknown): boolean {
|
||||||
return isObjectRecord(value) && "apiKey" in value;
|
return isObjectRecord(value) && "apiKey" in value;
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-11
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|||||||
import { canonicalizeBase64, estimateBase64DecodedBytes } from "@openclaw/media-core/base64";
|
import { canonicalizeBase64, estimateBase64DecodedBytes } from "@openclaw/media-core/base64";
|
||||||
import { parseMediaContentLength } from "@openclaw/media-core/content-length";
|
import { parseMediaContentLength } from "@openclaw/media-core/content-length";
|
||||||
import { toErrorObject } from "../infra/errors.js";
|
import { toErrorObject } from "../infra/errors.js";
|
||||||
|
import { cancelUnreadResponseBody } from "../infra/http-body.js";
|
||||||
import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
|
import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
|
||||||
import { normalizeHostname } from "../infra/net/hostname.js";
|
import { normalizeHostname } from "../infra/net/hostname.js";
|
||||||
import { resolveCliName } from "./cli-name.js";
|
import { resolveCliName } from "./cli-name.js";
|
||||||
@@ -81,12 +82,6 @@ type CameraClipPayload = {
|
|||||||
hasAudio: boolean;
|
hasAudio: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function cancelIgnoredResponseBody(response: Response | undefined): Promise<void> {
|
|
||||||
if (response?.bodyUsed !== true) {
|
|
||||||
await response?.body?.cancel().catch(() => undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Validate and normalize an unknown camera still-image payload. */
|
/** Validate and normalize an unknown camera still-image payload. */
|
||||||
export function parseCameraSnapPayload(value: unknown): CameraSnapPayload {
|
export function parseCameraSnapPayload(value: unknown): CameraSnapPayload {
|
||||||
const obj = asRecord(value);
|
const obj = asRecord(value);
|
||||||
@@ -170,13 +165,13 @@ async function writeUrlToFile(filePath: string, url: string, opts: { expectedHos
|
|||||||
const res = guarded.response;
|
const res = guarded.response;
|
||||||
const finalUrl = new URL(guarded.finalUrl);
|
const finalUrl = new URL(guarded.finalUrl);
|
||||||
if (normalizeHostname(finalUrl.hostname) !== expectedHost) {
|
if (normalizeHostname(finalUrl.hostname) !== expectedHost) {
|
||||||
await cancelIgnoredResponseBody(res);
|
await cancelUnreadResponseBody(res);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`writeUrlToFile: redirect host ${finalUrl.hostname} must match node host ${opts.expectedHost}`,
|
`writeUrlToFile: redirect host ${finalUrl.hostname} must match node host ${opts.expectedHost}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
await cancelIgnoredResponseBody(res);
|
await cancelUnreadResponseBody(res);
|
||||||
throw new Error(`failed to download ${url}: ${res.status} ${res.statusText}`);
|
throw new Error(`failed to download ${url}: ${res.status} ${res.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,11 +179,11 @@ async function writeUrlToFile(filePath: string, url: string, opts: { expectedHos
|
|||||||
try {
|
try {
|
||||||
contentLength = parseMediaContentLength(res.headers.get("content-length"));
|
contentLength = parseMediaContentLength(res.headers.get("content-length"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await cancelIgnoredResponseBody(res);
|
await cancelUnreadResponseBody(res);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
if (contentLength !== null && contentLength > MAX_CAMERA_URL_DOWNLOAD_BYTES) {
|
if (contentLength !== null && contentLength > MAX_CAMERA_URL_DOWNLOAD_BYTES) {
|
||||||
await cancelIgnoredResponseBody(res);
|
await cancelUnreadResponseBody(res);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`writeUrlToFile: content-length ${contentLength} exceeds max ${MAX_CAMERA_URL_DOWNLOAD_BYTES}`,
|
`writeUrlToFile: content-length ${contentLength} exceeds max ${MAX_CAMERA_URL_DOWNLOAD_BYTES}`,
|
||||||
);
|
);
|
||||||
@@ -196,7 +191,7 @@ async function writeUrlToFile(filePath: string, url: string, opts: { expectedHos
|
|||||||
|
|
||||||
const body = res.body;
|
const body = res.body;
|
||||||
if (!body) {
|
if (!body) {
|
||||||
await cancelIgnoredResponseBody(res);
|
await cancelUnreadResponseBody(res);
|
||||||
throw new Error(`failed to download ${url}: empty response body`);
|
throw new Error(`failed to download ${url}: empty response body`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
|
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
|
||||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { normalizeUniqueStringEntries } from "@openclaw/normalization-core/string-normalization";
|
import { normalizeUniqueStringEntries } from "@openclaw/normalization-core/string-normalization";
|
||||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
import { truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
|
||||||
import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js";
|
import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js";
|
||||||
import { isRich, theme } from "../../packages/terminal-core/src/theme.js";
|
import { isRich, theme } from "../../packages/terminal-core/src/theme.js";
|
||||||
import { formatCliCommand } from "../cli/command-format.js";
|
import { formatCliCommand } from "../cli/command-format.js";
|
||||||
@@ -24,7 +24,7 @@ const STATUS_VALUES = new Set<CommitmentStatus>([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
function truncate(value: string, maxChars: number): string {
|
function truncate(value: string, maxChars: number): string {
|
||||||
return value.length <= maxChars ? value : `${truncateUtf16Safe(value, maxChars - 1)}…`;
|
return truncateWithMarker(value, maxChars, { marker: "…", reserve: 1, trimEnd: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
function safe(value: string): string {
|
function safe(value: string): string {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { asOptionalRecord as readRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import {
|
import {
|
||||||
createAccountCronScheduledToolPolicy,
|
createAccountCronScheduledToolPolicy,
|
||||||
@@ -36,12 +37,6 @@ export function createScheduledToolPolicyMigrationCollector() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
||||||
return value && typeof value === "object" && !Array.isArray(value)
|
|
||||||
? (value as Record<string, unknown>)
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function usesToolRuntime(raw: Record<string, unknown>): boolean {
|
function usesToolRuntime(raw: Record<string, unknown>): boolean {
|
||||||
const payload = readRecord(raw.payload);
|
const payload = readRecord(raw.payload);
|
||||||
const trigger = readRecord(raw.trigger);
|
const trigger = readRecord(raw.trigger);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { isDeepStrictEqual } from "node:util";
|
import { isDeepStrictEqual } from "node:util";
|
||||||
import { normalizeConfiguredProviderCatalogModelId } from "@openclaw/model-catalog-core/provider-model-id-normalization";
|
import { normalizeConfiguredProviderCatalogModelId } from "@openclaw/model-catalog-core/provider-model-id-normalization";
|
||||||
|
import { normalizeLowercaseStringOrEmpty as normalizeString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { splitTrailingAuthProfile } from "../../../agents/model-ref-profile.js";
|
import { splitTrailingAuthProfile } from "../../../agents/model-ref-profile.js";
|
||||||
import { ensureRecord, getRecord } from "../../../config/legacy.shared.js";
|
import { ensureRecord, getRecord } from "../../../config/legacy.shared.js";
|
||||||
import { normalizeAgentModelRefForConfig } from "../../../config/model-input.js";
|
import { normalizeAgentModelRefForConfig } from "../../../config/model-input.js";
|
||||||
@@ -14,10 +15,6 @@ export function hasOwnDefinedProperty(record: Record<string, unknown>, key: stri
|
|||||||
return Object.hasOwn(record, key) && record[key] !== undefined;
|
return Object.hasOwn(record, key) && record[key] !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeString(value: unknown): string {
|
|
||||||
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function preferredClaudeSeparator(provider: string | undefined): "." | "-" {
|
function preferredClaudeSeparator(provider: string | undefined): "." | "-" {
|
||||||
return provider === "github-copilot" || provider === "copilot-proxy" ? "." : "-";
|
return provider === "github-copilot" || provider === "copilot-proxy" ? "." : "-";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,2 @@
|
|||||||
// Shared nullable record guard for doctor config walkers.
|
// Shared nullable record guard for doctor config walkers.
|
||||||
export function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
export { asNullableRecord as asObjectRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return value as Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/** CLI commands for listing, inspecting, and cancelling TaskFlow records. */
|
/** CLI commands for listing, inspecting, and cancelling TaskFlow records. */
|
||||||
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
|
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
|
||||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
import { truncateUtf16Safe, truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
|
||||||
import { truncateToVisibleWidth, visibleWidth } from "../../packages/terminal-core/src/ansi.js";
|
import { truncateToVisibleWidth, visibleWidth } from "../../packages/terminal-core/src/ansi.js";
|
||||||
import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js";
|
import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js";
|
||||||
import { isRich, theme } from "../../packages/terminal-core/src/theme.js";
|
import { isRich, theme } from "../../packages/terminal-core/src/theme.js";
|
||||||
@@ -38,7 +38,7 @@ function truncate(value: string, maxChars: number) {
|
|||||||
if (maxChars <= 1) {
|
if (maxChars <= 1) {
|
||||||
return truncateUtf16Safe(value, maxChars);
|
return truncateUtf16Safe(value, maxChars);
|
||||||
}
|
}
|
||||||
return `${truncateUtf16Safe(value, maxChars - 1)}…`;
|
return truncateWithMarker(value, maxChars, { marker: "…", reserve: 1, trimEnd: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeFlowDisplayText(value: string | undefined, maxChars?: number): string {
|
function safeFlowDisplayText(value: string | undefined, maxChars?: number): string {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/** Reads persisted generated catalogs without constructing a model registry. */
|
/** Reads persisted generated catalogs without constructing a model registry. */
|
||||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
|
import { normalizeOptionalString as readString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import type { ModelCatalogEntry, ModelInputType } from "../../agents/model-catalog.types.js";
|
import type { ModelCatalogEntry, ModelInputType } from "../../agents/model-catalog.types.js";
|
||||||
import {
|
import {
|
||||||
filterGeneratedPluginModelCatalogProviders,
|
filterGeneratedPluginModelCatalogProviders,
|
||||||
@@ -13,10 +14,6 @@ import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snaps
|
|||||||
const modelApis = new Set<string>(MODEL_APIS);
|
const modelApis = new Set<string>(MODEL_APIS);
|
||||||
const modelInputs = new Set<ModelInputType>(["text", "image", "audio", "video", "document"]);
|
const modelInputs = new Set<ModelInputType>(["text", "image", "audio", "video", "document"]);
|
||||||
|
|
||||||
function readString(value: unknown): string | undefined {
|
|
||||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readPositiveNumber(value: unknown): number | undefined {
|
function readPositiveNumber(value: unknown): number | undefined {
|
||||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* It selects active or requested sessions, renders recent trajectory events,
|
* It selects active or requested sessions, renders recent trajectory events,
|
||||||
* and can follow newly appended SQLite trajectory rows.
|
* and can follow newly appended SQLite trajectory rows.
|
||||||
*/
|
*/
|
||||||
|
import { normalizeOptionalString as toOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { readAcpSessionMeta } from "../acp/runtime/session-meta.js";
|
import { readAcpSessionMeta } from "../acp/runtime/session-meta.js";
|
||||||
import { getRuntimeConfig } from "../config/config.js";
|
import { getRuntimeConfig } from "../config/config.js";
|
||||||
import { listSessionEntriesReadOnly } from "../config/sessions/session-accessor.js";
|
import { listSessionEntriesReadOnly } from "../config/sessions/session-accessor.js";
|
||||||
@@ -79,10 +80,6 @@ function parseTailCount(value: string | number | undefined): number | null {
|
|||||||
return parseStrictNonNegativeInteger(value) ?? null;
|
return parseStrictNonNegativeInteger(value) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toOptionalString(value: unknown): string | undefined {
|
|
||||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTimestamp(ts: string): string {
|
function formatTimestamp(ts: string): string {
|
||||||
const date = new Date(ts);
|
const date = new Date(ts);
|
||||||
if (Number.isNaN(date.getTime())) {
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Gateway log-tail helpers for status diagnostics.
|
// Gateway log-tail helpers for status diagnostics.
|
||||||
// Summaries compact repeated auth/runtime failures while preserving enough context for operators.
|
// Summaries compact repeated auth/runtime failures while preserving enough context for operators.
|
||||||
|
|
||||||
|
import { safeParseJson } from "@openclaw/normalization-core";
|
||||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||||
import { classifyOAuthRefreshFailureReason } from "../../agents/auth-profiles/oauth-refresh-failure.js";
|
import { classifyOAuthRefreshFailureReason } from "../../agents/auth-profiles/oauth-refresh-failure.js";
|
||||||
@@ -108,15 +109,9 @@ export function summarizeLogTail(rawLines: string[], opts?: { maxLines?: number
|
|||||||
const block = consumeJsonBlock(lines, i);
|
const block = consumeJsonBlock(lines, i);
|
||||||
if (block) {
|
if (block) {
|
||||||
i = block.endIndex;
|
i = block.endIndex;
|
||||||
const parsed = (() => {
|
const parsed = (safeParseJson(block.json) ?? null) as {
|
||||||
try {
|
error?: { code?: string; message?: string };
|
||||||
return JSON.parse(block.json) as {
|
} | null;
|
||||||
error?: { code?: string; message?: string };
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
const code = normalizeOptionalString(parsed?.error?.code) ?? null;
|
const code = normalizeOptionalString(parsed?.error?.code) ?? null;
|
||||||
const msg = normalizeOptionalString(parsed?.error?.message) ?? null;
|
const msg = normalizeOptionalString(parsed?.error?.message) ?? null;
|
||||||
const refreshReason = classifyOAuthRefreshFailureReason(msg ?? "");
|
const refreshReason = classifyOAuthRefreshFailureReason(msg ?? "");
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
|
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
|
||||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
import { truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
|
||||||
import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js";
|
import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js";
|
||||||
import { isRich, theme } from "../../packages/terminal-core/src/theme.js";
|
import { isRich, theme } from "../../packages/terminal-core/src/theme.js";
|
||||||
import { formatCliCommand } from "../cli/command-format.js";
|
import { formatCliCommand } from "../cli/command-format.js";
|
||||||
@@ -206,7 +206,9 @@ function truncate(value: string, maxChars: number) {
|
|||||||
if (value.length <= maxChars) {
|
if (value.length <= maxChars) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
return maxChars <= 0 ? "" : `${truncateUtf16Safe(value, maxChars - 1)}…`;
|
return maxChars <= 0
|
||||||
|
? ""
|
||||||
|
: truncateWithMarker(value, maxChars, { marker: "…", reserve: 1, trimEnd: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortToken(value: string | undefined, maxChars = ID_PAD): string {
|
function shortToken(value: string | undefined, maxChars = ID_PAD): string {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
// Normalizes channel config compatibility fields during config loading.
|
// Normalizes channel config compatibility fields during config loading.
|
||||||
|
import { asNullableRecord as asObjectRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import {
|
import {
|
||||||
normalizeLegacyDmAliases,
|
normalizeLegacyDmAliases,
|
||||||
type CompatMutationResult,
|
type CompatMutationResult,
|
||||||
} from "../channels/plugins/dm-access.js";
|
} from "../channels/plugins/dm-access.js";
|
||||||
|
|
||||||
export { normalizeLegacyDmAliases };
|
export { normalizeLegacyDmAliases };
|
||||||
|
export { asObjectRecord };
|
||||||
export type { CompatMutationResult };
|
export type { CompatMutationResult };
|
||||||
|
|
||||||
/** Resolved streaming values a channel doctor supplies while migrating legacy aliases. */
|
/** Resolved streaming values a channel doctor supplies while migrating legacy aliases. */
|
||||||
@@ -41,13 +43,6 @@ export type RetiredChannelKeyRemoval = {
|
|||||||
pathPrefix: string;
|
pathPrefix: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Narrows unknown config JSON values to mutable object records. */
|
|
||||||
export function asObjectRecord(value: unknown): Record<string, unknown> | null {
|
|
||||||
return value && typeof value === "object" && !Array.isArray(value)
|
|
||||||
? (value as Record<string, unknown>)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseAliasStreamingMode(value: unknown): "off" | "partial" | "block" | "progress" | null {
|
function parseAliasStreamingMode(value: unknown): "off" | "partial" | "block" | "progress" | null {
|
||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Normalizes MCP config records into canonical runtime shape.
|
// Normalizes MCP config records into canonical runtime shape.
|
||||||
|
import { normalizeLowercaseStringOrEmpty as normalizeMcpString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { isRecord } from "../utils.js";
|
import { isRecord } from "../utils.js";
|
||||||
|
|
||||||
type ConfigMcpServers = Record<string, Record<string, unknown>>;
|
type ConfigMcpServers = Record<string, Record<string, unknown>>;
|
||||||
@@ -11,10 +12,6 @@ const CLI_MCP_TYPE_TO_OPENCLAW_TRANSPORT: Record<string, OpenClawMcpHttpTranspor
|
|||||||
stdio: "stdio",
|
stdio: "stdio",
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeMcpString(value: unknown): string {
|
|
||||||
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Maps CLI-native MCP type aliases to OpenClaw HTTP transport names. */
|
/** Maps CLI-native MCP type aliases to OpenClaw HTTP transport names. */
|
||||||
export function resolveOpenClawMcpTransportAlias(
|
export function resolveOpenClawMcpTransportAlias(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||||
|
import { asOptionalRecord as readRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import type { ProviderRouteOverridePresence } from "../plugin-sdk/provider-model-types.js";
|
import type { ProviderRouteOverridePresence } from "../plugin-sdk/provider-model-types.js";
|
||||||
import type { ModelDefinitionConfig, ModelProviderConfig } from "./types.models.js";
|
import type { ModelDefinitionConfig, ModelProviderConfig } from "./types.models.js";
|
||||||
import type { OpenClawConfig } from "./types.openclaw.js";
|
import type { OpenClawConfig } from "./types.openclaw.js";
|
||||||
@@ -36,12 +37,6 @@ function normalizeModelId(provider: string, modelId: string): string {
|
|||||||
: trimmed;
|
: trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
||||||
return value && typeof value === "object" && !Array.isArray(value)
|
|
||||||
? (value as Record<string, unknown>)
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasNonEmptyRecord(value: unknown): boolean {
|
function hasNonEmptyRecord(value: unknown): boolean {
|
||||||
const record = readRecord(value);
|
const record = readRecord(value);
|
||||||
return record !== undefined && Object.keys(record).length > 0;
|
return record !== undefined && Object.keys(record).length > 0;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { normalizeOptionalString as normalizeText } from "@openclaw/normalization-core/string-coerce";
|
||||||
import type { MsgContext } from "../../auto-reply/templating.js";
|
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||||
import { normalizeChatType } from "../../channels/chat-type.js";
|
import { normalizeChatType } from "../../channels/chat-type.js";
|
||||||
import { resolveConversationLabel } from "../../channels/conversation-label.js";
|
import { resolveConversationLabel } from "../../channels/conversation-label.js";
|
||||||
@@ -35,10 +36,6 @@ export type ConversationIdentity = {
|
|||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeText(value: unknown): string | undefined {
|
|
||||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeThreadId(value: unknown): string | undefined {
|
function normalizeThreadId(value: unknown): string | undefined {
|
||||||
if (typeof value === "number" && Number.isFinite(value)) {
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
return String(value);
|
return String(value);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { isDeepStrictEqual } from "node:util";
|
import { isDeepStrictEqual } from "node:util";
|
||||||
|
import { normalizeOptionalString as normalizeRunId } from "@openclaw/normalization-core/string-coerce";
|
||||||
import {
|
import {
|
||||||
normalizeDeliveryContext,
|
normalizeDeliveryContext,
|
||||||
type DeliveryContext,
|
type DeliveryContext,
|
||||||
@@ -17,10 +18,6 @@ type RestartRecoveryChannelAuthority = {
|
|||||||
sourceTurnId: string;
|
sourceTurnId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeRunId(value: unknown): string | undefined {
|
|
||||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Resolves only a complete durable channel claim; session-route fallbacks carry no authority. */
|
/** Resolves only a complete durable channel claim; session-route fallbacks carry no authority. */
|
||||||
export function resolveRestartRecoveryChannelAuthority(
|
export function resolveRestartRecoveryChannelAuthority(
|
||||||
entry: SessionEntry,
|
entry: SessionEntry,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { truncateWithMarker } from "@openclaw/normalization-core/utf16-slice";
|
||||||
/** Name, agent id, and payload text normalization helpers for cron service ops. */
|
/** Name, agent id, and payload text normalization helpers for cron service ops. */
|
||||||
import { normalizeOptionalAgentId } from "../../routing/session-key.js";
|
import { normalizeOptionalAgentId } from "../../routing/session-key.js";
|
||||||
import { truncateUtf16Safe } from "../../utils.js";
|
|
||||||
import type { CronPayload } from "../types.js";
|
import type { CronPayload } from "../types.js";
|
||||||
|
|
||||||
/** Normalizes a required cron job name and throws the public validation error when absent. */
|
/** Normalizes a required cron job name and throws the public validation error when absent. */
|
||||||
@@ -16,10 +16,7 @@ export function normalizeRequiredName(raw: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function truncateText(input: string, maxLen: number) {
|
function truncateText(input: string, maxLen: number) {
|
||||||
if (input.length <= maxLen) {
|
return truncateWithMarker(input, maxLen, { marker: "…", reserve: 1, trimEnd: true });
|
||||||
return input;
|
|
||||||
}
|
|
||||||
return `${truncateUtf16Safe(input, Math.max(0, maxLen - 1)).trimEnd()}…`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Normalizes optional cron agent ids through the canonical session-key agent id rules. */
|
/** Normalizes optional cron agent ids through the canonical session-key agent id rules. */
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
|
import { normalizeLowercaseStringOrEmpty as normalizeErrorSignal } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { isContextOverflowError } from "../agents/embedded-agent-helpers/context-overflow.js";
|
import { isContextOverflowError } from "../agents/embedded-agent-helpers/context-overflow.js";
|
||||||
import { STREAM_ERROR_FALLBACK_TEXT } from "../agents/stream-message-shared.js";
|
import { STREAM_ERROR_FALLBACK_TEXT } from "../agents/stream-message-shared.js";
|
||||||
import {
|
import {
|
||||||
@@ -88,10 +89,6 @@ const GATEWAY_ASSISTANT_ERROR_FALLBACK_TEXT = "The agent run failed before produ
|
|||||||
const GATEWAY_ASSISTANT_CONTEXT_OVERFLOW_FALLBACK_TEXT =
|
const GATEWAY_ASSISTANT_CONTEXT_OVERFLOW_FALLBACK_TEXT =
|
||||||
"Context overflow: this conversation is too large for the model. Try /compact, use /new to start a fresh session, or retry the command with a tighter output limit.";
|
"Context overflow: this conversation is too large for the model. Try /compact, use /new to start a fresh session, or retry the command with a tighter output limit.";
|
||||||
|
|
||||||
function normalizeErrorSignal(value: unknown): string {
|
|
||||||
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isContextOverflowErrorSignal(value: unknown): boolean {
|
function isContextOverflowErrorSignal(value: unknown): boolean {
|
||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// CLI backend live probe helpers run cron/MCP/image probes through the gateway
|
// CLI backend live probe helpers run cron/MCP/image probes through the gateway
|
||||||
// CLI backend and poll for externally visible live results.
|
// CLI backend and poll for externally visible live results.
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { asNullableRecord as asLoopbackSchemaRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { renderCatFacePngBase64 } from "../../test/helpers/live-image-probe.js";
|
import { renderCatFacePngBase64 } from "../../test/helpers/live-image-probe.js";
|
||||||
import { AUTOMATIONS_TOOL_NAME } from "../agents/tools/automations-tool-name.js";
|
import { AUTOMATIONS_TOOL_NAME } from "../agents/tools/automations-tool-name.js";
|
||||||
@@ -126,12 +127,6 @@ function parsePositiveInt(value: string | undefined, fallback: number, name: str
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function asLoopbackSchemaRecord(schema: unknown): Record<string, unknown> | null {
|
|
||||||
return schema && typeof schema === "object" && !Array.isArray(schema)
|
|
||||||
? (schema as Record<string, unknown>)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertLoopbackObjectSchemasHaveProperties(params: {
|
function assertLoopbackObjectSchemasHaveProperties(params: {
|
||||||
tools: LoopbackToolListEntry[];
|
tools: LoopbackToolListEntry[];
|
||||||
expectedSchemaProbeToolName?: string;
|
expectedSchemaProbeToolName?: string;
|
||||||
|
|||||||
+11
-17
@@ -1,6 +1,7 @@
|
|||||||
// Gateway webhook helpers for external hook dispatch into agents and wake flows.
|
// Gateway webhook helpers for external hook dispatch into agents and wake flows.
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import type { IncomingMessage } from "node:http";
|
import type { IncomingMessage } from "node:http";
|
||||||
|
import type { Result } from "@openclaw/normalization-core/result";
|
||||||
import {
|
import {
|
||||||
normalizeLowercaseStringOrEmpty,
|
normalizeLowercaseStringOrEmpty,
|
||||||
normalizeOptionalString,
|
normalizeOptionalString,
|
||||||
@@ -175,7 +176,7 @@ export function extractHookToken(req: IncomingMessage): string | undefined {
|
|||||||
export async function readJsonBody(
|
export async function readJsonBody(
|
||||||
req: IncomingMessage,
|
req: IncomingMessage,
|
||||||
maxBytes: number,
|
maxBytes: number,
|
||||||
): Promise<{ ok: true; value: unknown } | { ok: false; error: string }> {
|
): Promise<Result<unknown, string>> {
|
||||||
const result = await readJsonBodyWithLimit(req, { maxBytes, emptyObjectOnEmpty: true });
|
const result = await readJsonBodyWithLimit(req, { maxBytes, emptyObjectOnEmpty: true });
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
return result;
|
return result;
|
||||||
@@ -209,9 +210,7 @@ export function normalizeHookHeaders(req: IncomingMessage) {
|
|||||||
/** Validate a hook wake payload. */
|
/** Validate a hook wake payload. */
|
||||||
export function normalizeWakePayload(
|
export function normalizeWakePayload(
|
||||||
payload: Record<string, unknown>,
|
payload: Record<string, unknown>,
|
||||||
):
|
): Result<{ text: string; mode: "now" | "next-heartbeat" }, string> {
|
||||||
| { ok: true; value: { text: string; mode: "now" | "next-heartbeat" } }
|
|
||||||
| { ok: false; error: string } {
|
|
||||||
const normalizedText = normalizeOptionalString(payload.text) ?? "";
|
const normalizedText = normalizeOptionalString(payload.text) ?? "";
|
||||||
if (!normalizedText) {
|
if (!normalizedText) {
|
||||||
return { ok: false, error: "text required" };
|
return { ok: false, error: "text required" };
|
||||||
@@ -287,12 +286,10 @@ function normalizeHookAgentDelivery(params: {
|
|||||||
channel: unknown;
|
channel: unknown;
|
||||||
to: unknown;
|
to: unknown;
|
||||||
accountId: unknown;
|
accountId: unknown;
|
||||||
}):
|
}): Result<
|
||||||
| {
|
Pick<HookAgentPayload, "deliver" | "channel" | "to" | "accountId" | "delivery">,
|
||||||
ok: true;
|
string
|
||||||
value: Pick<HookAgentPayload, "deliver" | "channel" | "to" | "accountId" | "delivery">;
|
> {
|
||||||
}
|
|
||||||
| { ok: false; error: string } {
|
|
||||||
const deliver = resolveHookDeliver(params.deliver);
|
const deliver = resolveHookDeliver(params.deliver);
|
||||||
if (!deliver) {
|
if (!deliver) {
|
||||||
return {
|
return {
|
||||||
@@ -449,7 +446,7 @@ export function resolveHookSessionKey(params: {
|
|||||||
source: HookSessionKeySource;
|
source: HookSessionKeySource;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
idFactory?: () => string;
|
idFactory?: () => string;
|
||||||
}): { ok: true; value: string } | { ok: false; error: string } {
|
}): Result<string, string> {
|
||||||
const requested = resolveSessionKey(params.sessionKey);
|
const requested = resolveSessionKey(params.sessionKey);
|
||||||
if (requested) {
|
if (requested) {
|
||||||
if (
|
if (
|
||||||
@@ -526,12 +523,9 @@ export function normalizeHookDispatchSessionKey(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Validate and normalize a hook agent payload before policy/session resolution. */
|
/** Validate and normalize a hook agent payload before policy/session resolution. */
|
||||||
export function normalizeAgentPayload(payload: Record<string, unknown>):
|
export function normalizeAgentPayload(
|
||||||
| {
|
payload: Record<string, unknown>,
|
||||||
ok: true;
|
): Result<HookAgentPayload, string> {
|
||||||
value: HookAgentPayload;
|
|
||||||
}
|
|
||||||
| { ok: false; error: string } {
|
|
||||||
const message = normalizeOptionalString(payload.message) ?? "";
|
const message = normalizeOptionalString(payload.message) ?? "";
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return { ok: false, error: "message required" };
|
return { ok: false, error: "message required" };
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/** Connected node-hosted plugin tools available to agent tool resolution. */
|
/** Connected node-hosted plugin tools available to agent tool resolution. */
|
||||||
|
import { asOptionalRecord as normalizeRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import type { NodePluginToolDescriptor } from "../../packages/gateway-protocol/src/schema/nodes.js";
|
import type { NodePluginToolDescriptor } from "../../packages/gateway-protocol/src/schema/nodes.js";
|
||||||
import { NODE_MCP_TOOLS_CALL_COMMAND } from "../infra/node-commands.js";
|
import { NODE_MCP_TOOLS_CALL_COMMAND } from "../infra/node-commands.js";
|
||||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||||
@@ -44,12 +45,6 @@ function normalizeString(value: unknown): string {
|
|||||||
return typeof value === "string" ? value.trim() : "";
|
return typeof value === "string" ? value.trim() : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeRecord(value: unknown): Record<string, unknown> | undefined {
|
|
||||||
return value && typeof value === "object" && !Array.isArray(value)
|
|
||||||
? (value as Record<string, unknown>)
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultParameters(): Record<string, unknown> {
|
function defaultParameters(): Record<string, unknown> {
|
||||||
return { type: "object", properties: {}, additionalProperties: true };
|
return { type: "object", properties: {}, additionalProperties: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Transcript persistence and source-reply rewrites shared by chat send and abort.
|
// Transcript persistence and source-reply rewrites shared by chat send and abort.
|
||||||
|
import { asOptionalRecord as transcriptEventRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { getReplyPayloadMetadata } from "../../auto-reply/reply-payload.js";
|
import { getReplyPayloadMetadata } from "../../auto-reply/reply-payload.js";
|
||||||
import {
|
import {
|
||||||
findTranscriptEvent,
|
findTranscriptEvent,
|
||||||
@@ -67,12 +68,6 @@ export function assistantTranscriptScope(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function transcriptEventRecord(event: TranscriptEvent): Record<string, unknown> | undefined {
|
|
||||||
return event && typeof event === "object" && !Array.isArray(event)
|
|
||||||
? (event as Record<string, unknown>)
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function transcriptEventId(event: TranscriptEvent): string | undefined {
|
function transcriptEventId(event: TranscriptEvent): string | undefined {
|
||||||
const id = transcriptEventRecord(event)?.id;
|
const id = transcriptEventRecord(event)?.id;
|
||||||
return typeof id === "string" && id.trim().length > 0 ? id : undefined;
|
return typeof id === "string" && id.trim().length > 0 ? id : undefined;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Host directory browsing for the new-session folder picker. operator.admin
|
// Host directory browsing for the new-session folder picker. operator.admin
|
||||||
// only (see core-descriptors): listing arbitrary host paths carries the same
|
// only (see core-descriptors): listing arbitrary host paths carries the same
|
||||||
// trust as starting a session with an explicit cwd.
|
// trust as starting a session with an explicit cwd.
|
||||||
|
import { safeParseJson } from "@openclaw/normalization-core";
|
||||||
import {
|
import {
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
errorShape,
|
errorShape,
|
||||||
@@ -14,11 +15,7 @@ import type { GatewayRequestHandlers } from "./types.js";
|
|||||||
|
|
||||||
function parseNodePayload(payload: unknown, payloadJSON?: string | null): unknown {
|
function parseNodePayload(payload: unknown, payloadJSON?: string | null): unknown {
|
||||||
if (payloadJSON) {
|
if (payloadJSON) {
|
||||||
try {
|
return safeParseJson(payloadJSON);
|
||||||
return JSON.parse(payloadJSON) as unknown;
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Model list result building resolves visible model catalogs for an agent and
|
// Model list result building resolves visible model catalogs for an agent and
|
||||||
// strips runtime-only provider params before sending the browse API payload.
|
// strips runtime-only provider params before sending the browse API payload.
|
||||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||||
|
import { asPositiveSafeInteger as resolvePositiveSafeInteger } from "@openclaw/normalization-core/number-coercion";
|
||||||
import {
|
import {
|
||||||
resolveAgentEffectiveModelPrimary,
|
resolveAgentEffectiveModelPrimary,
|
||||||
resolveAgentWorkspaceDir,
|
resolveAgentWorkspaceDir,
|
||||||
@@ -79,10 +80,6 @@ function resolveModelsListView(params: Record<string, unknown>): ModelsListView
|
|||||||
return view === "configured" || view === "provider-config" || view === "all" ? view : "default";
|
return view === "configured" || view === "provider-config" || view === "all" ? view : "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePositiveSafeInteger(value: unknown): number | undefined {
|
|
||||||
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Project explicitly onto the public protocol shape. Concrete route, base URL,
|
// Project explicitly onto the public protocol shape. Concrete route, base URL,
|
||||||
// auth, and cost facts stay private; runtime intent is attached separately.
|
// auth, and cost facts stay private; runtime intent is attached separately.
|
||||||
function buildPublicModelProjection(entry: ModelCatalogEntry): ModelsListEntry {
|
function buildPublicModelProjection(entry: ModelCatalogEntry): ModelsListEntry {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { safeParseJson } from "@openclaw/normalization-core";
|
||||||
import {
|
import {
|
||||||
GATEWAY_CLIENT_CAPS,
|
GATEWAY_CLIENT_CAPS,
|
||||||
hasGatewayClientCap,
|
hasGatewayClientCap,
|
||||||
@@ -68,11 +69,7 @@ function parseNodePayload(payload: unknown, payloadJSON?: string | null): unknow
|
|||||||
if (!payloadJSON) {
|
if (!payloadJSON) {
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
try {
|
return safeParseJson(payloadJSON);
|
||||||
return JSON.parse(payloadJSON) as unknown;
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stageNodeTerminalUpload(
|
async function stageNodeTerminalUpload(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Gateway session lifecycle state projection.
|
// Gateway session lifecycle state projection.
|
||||||
// Converts agent run lifecycle events into session row/store status updates.
|
// Converts agent run lifecycle events into session row/store status updates.
|
||||||
|
import { normalizeOptionalString as normalizeLifecycleRunId } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||||
import type { SessionRunStatus } from "../../packages/gateway-protocol/src/schema/sessions-row.js";
|
import type { SessionRunStatus } from "../../packages/gateway-protocol/src/schema/sessions-row.js";
|
||||||
import { isAgentLifecycleYieldedWaiting } from "../agents/agent-lifecycle-parent-state.js";
|
import { isAgentLifecycleYieldedWaiting } from "../agents/agent-lifecycle-parent-state.js";
|
||||||
@@ -245,10 +246,6 @@ export function deriveGatewaySessionLifecycleProjectionPatch(params: {
|
|||||||
return patch;
|
return patch;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeLifecycleRunId(value: unknown): string | undefined {
|
|
||||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isRestartRecoveryLifecycleEvent(params: {
|
export function isRestartRecoveryLifecycleEvent(params: {
|
||||||
entry?: Pick<SessionEntry, "restartRecoveryRuns"> | null;
|
entry?: Pick<SessionEntry, "restartRecoveryRuns"> | null;
|
||||||
event: Pick<LifecycleEventLike, "runId" | "lifecycleGeneration" | "data">;
|
event: Pick<LifecycleEventLike, "runId" | "lifecycleGeneration" | "data">;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Apple Watch cannot use generic WebSockets on-device, so node events use bounded HTTPS polls.
|
// Apple Watch cannot use generic WebSockets on-device, so node events use bounded HTTPS polls.
|
||||||
import { randomBytes, randomUUID } from "node:crypto";
|
import { randomBytes, randomUUID } from "node:crypto";
|
||||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
|
import { isRecord as isStringRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import {
|
import {
|
||||||
GATEWAY_CLIENT_IDS,
|
GATEWAY_CLIENT_IDS,
|
||||||
GATEWAY_CLIENT_MODES,
|
GATEWAY_CLIENT_MODES,
|
||||||
@@ -174,10 +175,6 @@ function resolveWatchClientAddress(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isStringRecord(value: unknown): value is Record<string, unknown> {
|
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function trackResponseLifecycle(res: ServerResponse): ResponseLifecycle {
|
function trackResponseLifecycle(res: ServerResponse): ResponseLifecycle {
|
||||||
let aborted = false;
|
let aborted = false;
|
||||||
let settled = false;
|
let settled = false;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Hook workspace helpers resolve hook roots and workspace-local hook files.
|
// Hook workspace helpers resolve hook roots and workspace-local hook files.
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { safeParseJson } from "@openclaw/normalization-core";
|
||||||
import { normalizeTrimmedStringList } from "@openclaw/normalization-core/string-normalization";
|
import { normalizeTrimmedStringList } from "@openclaw/normalization-core/string-normalization";
|
||||||
import { MANIFEST_KEY } from "../compat/legacy-names.js";
|
import { MANIFEST_KEY } from "../compat/legacy-names.js";
|
||||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||||
@@ -44,11 +45,7 @@ function readHookPackageManifest(dir: string): HookPackageManifest | null {
|
|||||||
if (raw === null) {
|
if (raw === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
return (safeParseJson(raw) as HookPackageManifest | undefined) ?? null;
|
||||||
return JSON.parse(raw) as HookPackageManifest;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePackageHooks(manifest: HookPackageManifest): string[] {
|
function resolvePackageHooks(manifest: HookPackageManifest): string[] {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Resolves the LAN host OpenClaw should advertise to nearby devices.
|
// Resolves the LAN host OpenClaw should advertise to nearby devices.
|
||||||
import { isRfc1918Ipv4Address } from "@openclaw/net-policy/ip";
|
import { isRfc1918Ipv4Address } from "@openclaw/net-policy/ip";
|
||||||
|
import { normalizeLowercaseStringOrEmpty as normalizeInterfaceName } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { runCommandWithTimeout as defaultRunCommandWithTimeout } from "../process/exec.js";
|
import { runCommandWithTimeout as defaultRunCommandWithTimeout } from "../process/exec.js";
|
||||||
import {
|
import {
|
||||||
listExternalInterfaceAddresses,
|
listExternalInterfaceAddresses,
|
||||||
@@ -56,10 +57,6 @@ type RankedWindowsRouteRow = {
|
|||||||
order: number;
|
order: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeInterfaceName(name: unknown): string {
|
|
||||||
return typeof name === "string" ? name.trim().toLowerCase() : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeMetric(value: unknown): number {
|
function normalizeMetric(value: unknown): number {
|
||||||
if (typeof value === "number" && Number.isFinite(value)) {
|
if (typeof value === "number" && Number.isFinite(value)) {
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Shared owner-qualified ClawHub security verdict resolution.
|
// Shared owner-qualified ClawHub security verdict resolution.
|
||||||
|
import { asOptionalRecord as readObject } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import pLimit from "p-limit";
|
import pLimit from "p-limit";
|
||||||
import {
|
import {
|
||||||
@@ -76,12 +77,6 @@ function partitionCompatibleBatches(
|
|||||||
return batches.map((batch) => batch.items);
|
return batches.map((batch) => batch.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readObject(value: unknown): Record<string, unknown> | undefined {
|
|
||||||
return value && typeof value === "object" && !Array.isArray(value)
|
|
||||||
? (value as Record<string, unknown>)
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readOptionalStringField(value: unknown, field: string): string | undefined {
|
function readOptionalStringField(value: unknown, field: string): string | undefined {
|
||||||
return normalizeOptionalString(readObject(value)?.[field]);
|
return normalizeOptionalString(readObject(value)?.[field]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion";
|
import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion";
|
||||||
|
import { isRecord as isJsonObject } from "@openclaw/normalization-core/record-coerce";
|
||||||
import {
|
import {
|
||||||
normalizeLowercaseStringOrEmpty,
|
normalizeLowercaseStringOrEmpty,
|
||||||
normalizeOptionalString,
|
normalizeOptionalString,
|
||||||
@@ -852,10 +853,6 @@ function createClawHubBodyLimitError(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isJsonObject(value: unknown): value is Record<string, unknown> {
|
|
||||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function optionalStringField(
|
function optionalStringField(
|
||||||
source: Record<string, unknown>,
|
source: Record<string, unknown>,
|
||||||
field: string,
|
field: string,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
|
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
cancelUnreadResponseBody,
|
||||||
readResponseTextPrefix,
|
readResponseTextPrefix,
|
||||||
readResponseTextSnippet,
|
readResponseTextSnippet,
|
||||||
readResponseWithLimit,
|
readResponseWithLimit,
|
||||||
@@ -103,6 +104,37 @@ async function expectReadResponseWithLimitFailureCase(params: {
|
|||||||
).rejects.toThrow(params.expectedError);
|
).rejects.toThrow(params.expectedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe("cancelUnreadResponseBody", () => {
|
||||||
|
it("cancels unread bodies and ignores cancellation failures", async () => {
|
||||||
|
const cancel = vi.fn(() => {
|
||||||
|
throw new Error("already closed");
|
||||||
|
});
|
||||||
|
const response = new Response(makeStallingStream([], cancel));
|
||||||
|
|
||||||
|
await expect(cancelUnreadResponseBody(response)).resolves.toBeUndefined();
|
||||||
|
expect(cancel).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves consumed and absent bodies alone", async () => {
|
||||||
|
const cancel = vi.fn();
|
||||||
|
const response = new Response(
|
||||||
|
new ReadableStream<Uint8Array>({
|
||||||
|
start(controller) {
|
||||||
|
controller.enqueue(new TextEncoder().encode("done"));
|
||||||
|
controller.close();
|
||||||
|
},
|
||||||
|
cancel,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await response.text();
|
||||||
|
|
||||||
|
await cancelUnreadResponseBody(response);
|
||||||
|
await cancelUnreadResponseBody(undefined);
|
||||||
|
|
||||||
|
expect(cancel).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("readResponseWithLimit", () => {
|
describe("readResponseWithLimit", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ import { parseStrictNonNegativeInteger } from "./parse-finite-number.js";
|
|||||||
|
|
||||||
export { readChunkWithIdleTimeout } from "./http-response-body-timeout.js";
|
export { readChunkWithIdleTimeout } from "./http-response-body-timeout.js";
|
||||||
|
|
||||||
|
/** Cancels a response body only when no consumer has started reading it. */
|
||||||
|
export async function cancelUnreadResponseBody(response: Response | undefined): Promise<void> {
|
||||||
|
if (response && !response.bodyUsed) {
|
||||||
|
await response.body?.cancel().catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
export const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
||||||
export const DEFAULT_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
|
export const DEFAULT_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { isPassThroughRemoteMediaSource } from "@openclaw/media-core/media-source-url";
|
import { isPassThroughRemoteMediaSource } from "@openclaw/media-core/media-source-url";
|
||||||
|
import { hasNonEmptyString as isNonEmptyMediaSource } from "@openclaw/normalization-core/string-coerce";
|
||||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||||
import { resolveDeliveryQueueMediaDir } from "../../config/paths.js";
|
import { resolveDeliveryQueueMediaDir } from "../../config/paths.js";
|
||||||
import {
|
import {
|
||||||
@@ -38,10 +39,6 @@ function resolveArtifactExtension(source: string): string {
|
|||||||
return ARTIFACT_EXT_RE.test(extension) ? extension.toLowerCase() : "";
|
return ARTIFACT_EXT_RE.test(extension) ? extension.toLowerCase() : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNonEmptyMediaSource(source: unknown): source is string {
|
|
||||||
return typeof source === "string" && Boolean(source.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
function payloadMediaSources(payload: ReplyPayload): string[] {
|
function payloadMediaSources(payload: ReplyPayload): string[] {
|
||||||
const sources: string[] = [];
|
const sources: string[] = [];
|
||||||
if (isNonEmptyMediaSource(payload.mediaUrl)) {
|
if (isNonEmptyMediaSource(payload.mediaUrl)) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Message-action runner normalizes tool params, resolves channel/target/media,
|
// Message-action runner normalizes tool params, resolves channel/target/media,
|
||||||
// applies policies, and dispatches send/poll/plugin actions.
|
// applies policies, and dispatches send/poll/plugin actions.
|
||||||
|
import { asOptionalRecord as asResultRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import {
|
import {
|
||||||
normalizeOptionalLowercaseString,
|
normalizeOptionalLowercaseString,
|
||||||
normalizeOptionalString,
|
normalizeOptionalString,
|
||||||
@@ -284,12 +285,6 @@ export function getToolResult(
|
|||||||
return "toolResult" in result ? result.toolResult : undefined;
|
return "toolResult" in result ? result.toolResult : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function asResultRecord(value: unknown): Record<string, unknown> | undefined {
|
|
||||||
return value && typeof value === "object" && !Array.isArray(value)
|
|
||||||
? (value as Record<string, unknown>)
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function withSendNormalization(
|
function withSendNormalization(
|
||||||
result: MessageActionRunResult,
|
result: MessageActionRunResult,
|
||||||
normalization?: MessageActionNormalization,
|
normalization?: MessageActionNormalization,
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
|||||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||||
import { readProviderJsonResponse } from "../agents/provider-http-errors.js";
|
import { readProviderJsonResponse } from "../agents/provider-http-errors.js";
|
||||||
|
import { cancelUnreadResponseBody } from "./http-body.js";
|
||||||
import {
|
import {
|
||||||
buildUsageHttpErrorSnapshot,
|
buildUsageHttpErrorSnapshot,
|
||||||
discardUsageResponseBody,
|
|
||||||
fetchJson,
|
fetchJson,
|
||||||
parseUsageResetAt,
|
parseUsageResetAt,
|
||||||
readUsageJson,
|
readUsageJson,
|
||||||
@@ -159,7 +159,7 @@ async function fetchClaudeWebUsage(
|
|||||||
fetchFn,
|
fetchFn,
|
||||||
);
|
);
|
||||||
if (!orgRes.ok) {
|
if (!orgRes.ok) {
|
||||||
await discardUsageResponseBody(orgRes);
|
await cancelUnreadResponseBody(orgRes);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ async function fetchClaudeWebUsage(
|
|||||||
fetchFn,
|
fetchFn,
|
||||||
);
|
);
|
||||||
if (!usageRes.ok) {
|
if (!usageRes.ok) {
|
||||||
await discardUsageResponseBody(usageRes);
|
await cancelUnreadResponseBody(usageRes);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// Fetches Codex provider usage windows.
|
// Fetches Codex provider usage windows.
|
||||||
import { resolveProviderRequestHeaders } from "../agents/provider-request-config.js";
|
import { resolveProviderRequestHeaders } from "../agents/provider-request-config.js";
|
||||||
|
import { cancelUnreadResponseBody } from "./http-body.js";
|
||||||
import { parseStrictFiniteNumber } from "./parse-finite-number.js";
|
import { parseStrictFiniteNumber } from "./parse-finite-number.js";
|
||||||
import {
|
import {
|
||||||
buildUsageHttpErrorSnapshot,
|
buildUsageHttpErrorSnapshot,
|
||||||
discardUsageResponseBody,
|
|
||||||
fetchJson,
|
fetchJson,
|
||||||
readUsageJson,
|
readUsageJson,
|
||||||
} from "./provider-usage.fetch.shared.js";
|
} from "./provider-usage.fetch.shared.js";
|
||||||
@@ -89,7 +89,7 @@ export async function fetchCodexUsage(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
await discardUsageResponseBody(res);
|
await cancelUnreadResponseBody(res);
|
||||||
return buildUsageHttpErrorSnapshot({
|
return buildUsageHttpErrorSnapshot({
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
status: res.status,
|
status: res.status,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Fetches and normalizes DeepSeek provider usage records.
|
// Fetches and normalizes DeepSeek provider usage records.
|
||||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
|
import { cancelUnreadResponseBody } from "./http-body.js";
|
||||||
import {
|
import {
|
||||||
buildUsageHttpErrorSnapshot,
|
buildUsageHttpErrorSnapshot,
|
||||||
discardUsageResponseBody,
|
|
||||||
fetchJson,
|
fetchJson,
|
||||||
parseFiniteNumber,
|
parseFiniteNumber,
|
||||||
readUsageJson,
|
readUsageJson,
|
||||||
@@ -75,7 +75,7 @@ export async function fetchDeepSeekUsage(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
await discardUsageResponseBody(res);
|
await cancelUnreadResponseBody(res);
|
||||||
return buildUsageHttpErrorSnapshot({
|
return buildUsageHttpErrorSnapshot({
|
||||||
provider: "deepseek",
|
provider: "deepseek",
|
||||||
status: res.status,
|
status: res.status,
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { expectDefined } from "@openclaw/normalization-core";
|
|||||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
// Fetches Gemini provider usage windows.
|
// Fetches Gemini provider usage windows.
|
||||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||||
|
import { cancelUnreadResponseBody } from "./http-body.js";
|
||||||
import {
|
import {
|
||||||
buildUsageHttpErrorSnapshot,
|
buildUsageHttpErrorSnapshot,
|
||||||
discardUsageResponseBody,
|
|
||||||
fetchJson,
|
fetchJson,
|
||||||
readUsageJson,
|
readUsageJson,
|
||||||
} from "./provider-usage.fetch.shared.js";
|
} from "./provider-usage.fetch.shared.js";
|
||||||
@@ -36,7 +36,7 @@ export async function fetchGeminiUsage(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
await discardUsageResponseBody(res);
|
await cancelUnreadResponseBody(res);
|
||||||
return buildUsageHttpErrorSnapshot({
|
return buildUsageHttpErrorSnapshot({
|
||||||
provider,
|
provider,
|
||||||
status: res.status,
|
status: res.status,
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/st
|
|||||||
import { readProviderJsonResponse } from "../agents/provider-http-errors.js";
|
import { readProviderJsonResponse } from "../agents/provider-http-errors.js";
|
||||||
import { isRecord } from "../utils.js";
|
import { isRecord } from "../utils.js";
|
||||||
import { readTrimmedStringAlias } from "../utils/string-readers.js";
|
import { readTrimmedStringAlias } from "../utils/string-readers.js";
|
||||||
|
import { cancelUnreadResponseBody } from "./http-body.js";
|
||||||
import {
|
import {
|
||||||
buildUsageHttpErrorSnapshot,
|
buildUsageHttpErrorSnapshot,
|
||||||
discardUsageResponseBody,
|
|
||||||
fetchJson,
|
fetchJson,
|
||||||
parseFiniteNumber,
|
parseFiniteNumber,
|
||||||
} from "./provider-usage.fetch.shared.js";
|
} from "./provider-usage.fetch.shared.js";
|
||||||
@@ -544,7 +544,7 @@ export async function fetchMinimaxUsage(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
await discardUsageResponseBody(res);
|
await cancelUnreadResponseBody(res);
|
||||||
return buildUsageHttpErrorSnapshot({
|
return buildUsageHttpErrorSnapshot({
|
||||||
provider: "minimax",
|
provider: "minimax",
|
||||||
status: res.status,
|
status: res.status,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
|||||||
import {
|
import {
|
||||||
buildUsageErrorSnapshot,
|
buildUsageErrorSnapshot,
|
||||||
buildUsageHttpErrorSnapshot,
|
buildUsageHttpErrorSnapshot,
|
||||||
discardUsageResponseBody,
|
|
||||||
fetchJson,
|
fetchJson,
|
||||||
parseFiniteNumber,
|
parseFiniteNumber,
|
||||||
readUsageJson,
|
readUsageJson,
|
||||||
@@ -159,15 +158,6 @@ describe("provider usage fetch shared helpers", () => {
|
|||||||
expect(timeoutSpy).toHaveBeenCalledWith(MAX_TIMER_TIMEOUT_MS);
|
expect(timeoutSpy).toHaveBeenCalledWith(MAX_TIMER_TIMEOUT_MS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("cancels unread response bodies when discarding usage responses", async () => {
|
|
||||||
const response = new Response("not needed", { status: 429 });
|
|
||||||
const cancel = vi.spyOn(response.body!, "cancel").mockResolvedValue(undefined);
|
|
||||||
|
|
||||||
await discardUsageResponseBody(response);
|
|
||||||
|
|
||||||
expect(cancel).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("maps configured status codes to token expired", () => {
|
it("maps configured status codes to token expired", () => {
|
||||||
const snapshot = buildUsageHttpErrorSnapshot({
|
const snapshot = buildUsageHttpErrorSnapshot({
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
|
|||||||
@@ -23,12 +23,6 @@ export async function fetchJson(
|
|||||||
return await fetchFn(url, { ...init, signal });
|
return await fetchFn(url, { ...init, signal });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function discardUsageResponseBody(response: Response): Promise<void> {
|
|
||||||
if (!response.bodyUsed) {
|
|
||||||
await response.body?.cancel().catch(() => undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseFiniteNumber(value: unknown): number | undefined {
|
export function parseFiniteNumber(value: unknown): number | undefined {
|
||||||
return parseFiniteNumberish(value);
|
return parseFiniteNumberish(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
|
||||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||||
|
import { cancelUnreadResponseBody } from "./http-body.js";
|
||||||
import {
|
import {
|
||||||
buildUsageHttpErrorSnapshot,
|
buildUsageHttpErrorSnapshot,
|
||||||
discardUsageResponseBody,
|
|
||||||
fetchJson,
|
fetchJson,
|
||||||
parseUsageResetAt,
|
parseUsageResetAt,
|
||||||
readUsageJson,
|
readUsageJson,
|
||||||
@@ -80,7 +80,7 @@ export async function fetchZaiUsage(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
await discardUsageResponseBody(res);
|
await cancelUnreadResponseBody(res);
|
||||||
return buildUsageHttpErrorSnapshot({
|
return buildUsageHttpErrorSnapshot({
|
||||||
provider: "zai",
|
provider: "zai",
|
||||||
status: res.status,
|
status: res.status,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { DatabaseSync } from "node:sqlite";
|
import type { DatabaseSync } from "node:sqlite";
|
||||||
|
import { safeParseJson } from "@openclaw/normalization-core";
|
||||||
import { isRecord as isPlainRecord } from "@openclaw/normalization-core/record-coerce";
|
import { isRecord as isPlainRecord } from "@openclaw/normalization-core/record-coerce";
|
||||||
import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
|
import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
|
||||||
import {
|
import {
|
||||||
@@ -356,11 +357,7 @@ function parseRequiredJson(value: string | null): unknown {
|
|||||||
if (value === null) {
|
if (value === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
try {
|
return safeParseJson(value);
|
||||||
return JSON.parse(value) as unknown;
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeRestartSentinelRow(row: {
|
function decodeRestartSentinelRow(row: {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user