mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-14 08:15:39 +00:00
docs: document utility helpers
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import { normalizeOptionalAccountId } from "../routing/account-id.js";
|
||||
|
||||
/**
|
||||
* Compatibility wrapper for account-id normalization.
|
||||
*
|
||||
* Runtime code imports this utility when it needs the older utils path while
|
||||
* the canonical normalization logic lives in routing/account-id.
|
||||
*/
|
||||
/** Normalize an optional account id, returning undefined for blank/invalid input. */
|
||||
export function normalizeAccountId(value?: string): string | undefined {
|
||||
return normalizeOptionalAccountId(value);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,12 @@ import {
|
||||
} from "./message-channel-constants.js";
|
||||
import { normalizeMessageChannel } from "./message-channel-normalize.js";
|
||||
|
||||
/**
|
||||
* Message channel and Gateway client classification helpers.
|
||||
*
|
||||
* This module keeps channel normalization, client identity checks, and markdown
|
||||
* capability lookup in one place for send/render decisions.
|
||||
*/
|
||||
export { GATEWAY_CLIENT_NAMES, GATEWAY_CLIENT_MODES };
|
||||
export type { GatewayClientName, GatewayClientMode };
|
||||
export { normalizeGatewayClientName, normalizeGatewayClientMode };
|
||||
@@ -40,24 +46,29 @@ type GatewayClientInfoLike = {
|
||||
id?: string | null;
|
||||
};
|
||||
|
||||
/** Return whether a Gateway client is the CLI transport. */
|
||||
export function isGatewayCliClient(client?: GatewayClientInfoLike | null): boolean {
|
||||
return normalizeGatewayClientMode(client?.mode) === GATEWAY_CLIENT_MODES.CLI;
|
||||
}
|
||||
|
||||
/** Return whether a client is one of the operator UI clients. */
|
||||
export function isOperatorUiClient(client?: GatewayClientInfoLike | null): boolean {
|
||||
const clientId = normalizeGatewayClientName(client?.id);
|
||||
return clientId === GATEWAY_CLIENT_NAMES.CONTROL_UI || clientId === GATEWAY_CLIENT_NAMES.TUI;
|
||||
}
|
||||
|
||||
/** Return whether a client is the browser Control UI. */
|
||||
export function isBrowserOperatorUiClient(client?: GatewayClientInfoLike | null): boolean {
|
||||
const clientId = normalizeGatewayClientName(client?.id);
|
||||
return clientId === GATEWAY_CLIENT_NAMES.CONTROL_UI;
|
||||
}
|
||||
|
||||
/** Return whether a raw channel id resolves to OpenClaw's internal channel. */
|
||||
export function isInternalMessageChannel(raw?: string | null): raw is InternalMessageChannel {
|
||||
return normalizeMessageChannel(raw) === INTERNAL_MESSAGE_CHANNEL;
|
||||
}
|
||||
|
||||
/** Return whether a Gateway client is the public webchat surface. */
|
||||
export function isWebchatClient(client?: GatewayClientInfoLike | null): boolean {
|
||||
const mode = normalizeGatewayClientMode(client?.mode);
|
||||
if (mode === GATEWAY_CLIENT_MODES.WEBCHAT) {
|
||||
@@ -66,6 +77,7 @@ export function isWebchatClient(client?: GatewayClientInfoLike | null): boolean
|
||||
return normalizeGatewayClientName(client?.id) === GATEWAY_CLIENT_NAMES.WEBCHAT_UI;
|
||||
}
|
||||
|
||||
/** Resolve whether a channel can receive markdown without plain-text downgrade. */
|
||||
export function isMarkdownCapableMessageChannel(raw?: string | null): boolean {
|
||||
const channel = normalizeMessageChannel(raw);
|
||||
if (!channel) {
|
||||
@@ -80,6 +92,7 @@ export function isMarkdownCapableMessageChannel(raw?: string | null): boolean {
|
||||
if (builtInMeta) {
|
||||
return builtInMeta.markdownCapable === true;
|
||||
}
|
||||
// Catalog metadata covers bundled channels whose runtime plugin is not loaded yet.
|
||||
const catalogMeta = listBundledChannelCatalogEntries().find(
|
||||
(entry) => entry.id === builtInChannel,
|
||||
);
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
/**
|
||||
* Shared queue overflow, debounce, and collection helpers.
|
||||
*
|
||||
* Queue owners use these helpers to cap pending work, summarize dropped items,
|
||||
* debounce drains, and force individual collection when cross-channel ordering matters.
|
||||
*/
|
||||
/** Mutable summary state for a capped queue. */
|
||||
export type QueueSummaryState = {
|
||||
dropPolicy: "summarize" | "old" | "new";
|
||||
droppedCount: number;
|
||||
summaryLines: string[];
|
||||
};
|
||||
|
||||
/** Queue overflow strategy. */
|
||||
export type QueueDropPolicy = QueueSummaryState["dropPolicy"];
|
||||
|
||||
/** Generic capped queue state with shared overflow summary fields. */
|
||||
export type QueueState<T> = QueueSummaryState & {
|
||||
items: T[];
|
||||
cap: number;
|
||||
};
|
||||
|
||||
/** Clear accumulated overflow summary state after it has been emitted. */
|
||||
export function clearQueueSummaryState(state: QueueSummaryState): void {
|
||||
state.droppedCount = 0;
|
||||
state.summaryLines = [];
|
||||
}
|
||||
|
||||
/** Build a summary prompt preview without mutating the source queue state. */
|
||||
export function previewQueueSummaryPrompt(params: {
|
||||
state: QueueSummaryState;
|
||||
noun: string;
|
||||
@@ -32,6 +43,7 @@ export function previewQueueSummaryPrompt(params: {
|
||||
});
|
||||
}
|
||||
|
||||
/** Apply runtime queue settings while preserving previous values for omitted fields. */
|
||||
export function applyQueueRuntimeSettings<TMode extends string>(params: {
|
||||
target: {
|
||||
mode: TMode;
|
||||
@@ -58,6 +70,7 @@ export function applyQueueRuntimeSettings<TMode extends string>(params: {
|
||||
params.target.dropPolicy = params.settings.dropPolicy ?? params.target.dropPolicy;
|
||||
}
|
||||
|
||||
/** Trim queue summary text to a bounded single-line preview. */
|
||||
export function elideQueueText(text: string, limit = 140): string {
|
||||
if (text.length <= limit) {
|
||||
return text;
|
||||
@@ -65,11 +78,13 @@ export function elideQueueText(text: string, limit = 140): string {
|
||||
return `${text.slice(0, Math.max(0, limit - 1)).trimEnd()}…`;
|
||||
}
|
||||
|
||||
/** Normalize whitespace and elide one dropped item for queue summaries. */
|
||||
export function buildQueueSummaryLine(text: string, limit = 160): string {
|
||||
const cleaned = text.replace(/\s+/g, " ").trim();
|
||||
return elideQueueText(cleaned, limit);
|
||||
}
|
||||
|
||||
/** Run optional duplicate detection before an item enters a queue. */
|
||||
export function shouldSkipQueueItem<T>(params: {
|
||||
item: T;
|
||||
items: T[];
|
||||
@@ -81,6 +96,7 @@ export function shouldSkipQueueItem<T>(params: {
|
||||
return params.dedupe(params.item, params.items);
|
||||
}
|
||||
|
||||
/** Apply overflow policy before enqueueing another item. */
|
||||
export function applyQueueDropPolicy<T>(params: {
|
||||
queue: QueueState<T>;
|
||||
summarize: (item: T) => string;
|
||||
@@ -102,6 +118,7 @@ export function applyQueueDropPolicy<T>(params: {
|
||||
params.queue.droppedCount += 1;
|
||||
params.queue.summaryLines.push(buildQueueSummaryLine(params.summarize(item)));
|
||||
}
|
||||
// Summary memory is bounded independently from the item cap to avoid prompt blowups.
|
||||
const limit = Math.max(0, params.summaryLimit ?? cap);
|
||||
while (params.queue.summaryLines.length > limit) {
|
||||
params.queue.summaryLines.shift();
|
||||
@@ -110,11 +127,13 @@ export function applyQueueDropPolicy<T>(params: {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Wait until the queue has been quiet for its debounce window. */
|
||||
export function waitForQueueDebounce(queue: {
|
||||
debounceMs: number;
|
||||
lastEnqueuedAt: number;
|
||||
}): Promise<void> {
|
||||
if (process.env.OPENCLAW_TEST_FAST === "1") {
|
||||
// Tests use this escape hatch so debounce logic does not slow deterministic queue specs.
|
||||
return Promise.resolve();
|
||||
}
|
||||
const debounceMs = Math.max(0, queue.debounceMs);
|
||||
@@ -134,6 +153,7 @@ export function waitForQueueDebounce(queue: {
|
||||
});
|
||||
}
|
||||
|
||||
/** Mark one queue as draining unless another drain is already active. */
|
||||
export function beginQueueDrain<T extends { draining: boolean }>(
|
||||
map: Map<string, T>,
|
||||
key: string,
|
||||
@@ -146,6 +166,7 @@ export function beginQueueDrain<T extends { draining: boolean }>(
|
||||
return queue;
|
||||
}
|
||||
|
||||
/** Run and remove the next queued item, returning false when empty. */
|
||||
export async function drainNextQueueItem<T>(
|
||||
items: T[],
|
||||
run: (item: T) => Promise<void>,
|
||||
@@ -159,6 +180,7 @@ export async function drainNextQueueItem<T>(
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Drain one item when collect mode requires individual processing. */
|
||||
export async function drainCollectItemIfNeeded<T>(params: {
|
||||
forceIndividualCollect: boolean;
|
||||
isCrossChannel: boolean;
|
||||
@@ -170,12 +192,14 @@ export async function drainCollectItemIfNeeded<T>(params: {
|
||||
return "skipped";
|
||||
}
|
||||
if (params.isCrossChannel) {
|
||||
// Once cross-channel items appear, future collection stays individual to preserve ordering.
|
||||
params.setForceIndividualCollect?.(true);
|
||||
}
|
||||
const drained = await drainNextQueueItem(params.items, params.run);
|
||||
return drained ? "drained" : "empty";
|
||||
}
|
||||
|
||||
/** Drain one collect step using mutable queue collection state. */
|
||||
export async function drainCollectQueueStep<T>(params: {
|
||||
collectState: { forceIndividualCollect: boolean };
|
||||
isCrossChannel: boolean;
|
||||
@@ -193,6 +217,7 @@ export async function drainCollectQueueStep<T>(params: {
|
||||
});
|
||||
}
|
||||
|
||||
/** Build and consume the queue overflow summary prompt. */
|
||||
export function buildQueueSummaryPrompt(params: {
|
||||
state: QueueSummaryState;
|
||||
noun: string;
|
||||
@@ -216,6 +241,7 @@ export function buildQueueSummaryPrompt(params: {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/** Render a collect prompt from queued items and optional overflow summary. */
|
||||
export function buildCollectPrompt<T>(params: {
|
||||
title: string;
|
||||
items: T[];
|
||||
@@ -232,6 +258,7 @@ export function buildCollectPrompt<T>(params: {
|
||||
return blocks.join("\n\n");
|
||||
}
|
||||
|
||||
/** Return true when queued items span keys or explicitly mark cross-channel state. */
|
||||
export function hasCrossChannelItems<T>(
|
||||
items: T[],
|
||||
resolveKey: (item: T) => { key?: string; cross?: boolean },
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Defensive JSON stringify helper for diagnostics.
|
||||
*
|
||||
* The replacer handles values common in runtime logs that JSON.stringify would
|
||||
* otherwise reject or erase, and returns null for circular structures.
|
||||
*/
|
||||
/** Safely stringify diagnostic values, preserving bigint/errors/functions in readable form. */
|
||||
export function safeJsonStringify(value: unknown): string | null {
|
||||
try {
|
||||
return JSON.stringify(value, (_key, val) => {
|
||||
@@ -11,6 +18,7 @@ export function safeJsonStringify(value: unknown): string | null {
|
||||
return { name: val.name, message: val.message, stack: val.stack };
|
||||
}
|
||||
if (val instanceof Uint8Array) {
|
||||
// Binary payloads are base64 encoded so diagnostic JSON remains valid UTF-8 text.
|
||||
return { type: "Uint8Array", data: Buffer.from(val).toString("base64") };
|
||||
}
|
||||
return val;
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
/**
|
||||
* Compatibility export for timeout-wrapped operations.
|
||||
*
|
||||
* The implementation lives in infra/fs-safe; this keeps older utils imports on
|
||||
* the same public helper without duplicating timeout behavior.
|
||||
*/
|
||||
export { withTimeout } from "../infra/fs-safe.js";
|
||||
|
||||
Reference in New Issue
Block a user