Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/Java/Openclaw/ui/src/ui/chat/   (KI Agentensystem Version 22©)  Datei vom 26.3.2026 mit Größe 13 kB image not shown  

Quelle  message-normalizer.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

/**
 * Message normalization utilities for chat rendering.
 */

import { stripInboundMetadata } from "../../../../src/auto-reply/reply/strip-inbound-meta.js";
import { extractCanvasShortcodes } from "../../../../src/chat/canvas-render.js";
import {
  isToolCallContentType,
  isToolResultContentType,
  resolveToolBlockArgs,
} from "../../../../src/chat/tool-content.js";
import { mediaKindFromMime } from "../../../../src/media/constants.js";
import { splitMediaFromOutput } from "../../../../src/media/parse.js";
import { parseInlineDirectives } from "../../../../src/utils/directive-tags.js";
import type { NormalizedMessage, MessageContentItem } from "../types/chat-types.ts";

function coerceCanvasPreview(
  value: unknown,
):
  | Extract<NonNullable<NormalizedMessage["content"][number]>, { type: "canvas" }>["preview"]
  | null {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return null;
  }
  const preview = value as Record<string, unknown>;
  if (preview.kind !== "canvas" || preview.surface === "tool_card") {
    return null;
  }
  const render = preview.render === "url" ? "url" : null;
  if (!render) {
    return null;
  }
  return {
    kind: "canvas",
    surface: "assistant_message",
    render,
    ...(typeof preview.title === "string" ? { title: preview.title } : {}),
    ...(typeof preview.preferredHeight === "number"
      ? { preferredHeight: preview.preferredHeight }
      : {}),
    ...(typeof preview.url === "string" ? { url: preview.url } : {}),
    ...(typeof preview.viewId === "string" ? { viewId: preview.viewId } : {}),
    ...(typeof preview.className === "string" ? { className: preview.className } : {}),
    ...(typeof preview.style === "string" ? { style: preview.style } : {}),
  };
}

function isRenderableAssistantAttachment(url: string): boolean {
  const trimmed = url.trim();
  return (
    /^https?:\/\//i.test(trimmed) ||
    /^data:(?:image|audio|video)\//i.test(trimmed) ||
    /^\/(?:__openclaw__|media)\//.test(trimmed) ||
    trimmed.startsWith("file://") ||
    trimmed.startsWith("~") ||
    trimmed.startsWith("/") ||
    /^[a-zA-Z]:[\\/]/.test(trimmed)
  );
}

function shouldPreserveRelativeAssistantAttachment(url: string): boolean {
  const trimmed = url.trim();
  if (!trimmed) {
    return false;
  }
  return (
    !/^https?:\/\//i.test(trimmed) &&
    !/^data:(?:image|audio|video)\//i.test(trimmed) &&
    !/^\/(?:__openclaw__|media)\//.test(trimmed) &&
    !trimmed.startsWith("file://") &&
    !trimmed.startsWith("~") &&
    !trimmed.startsWith("/") &&
    !/^[a-zA-Z]:[\\/]/.test(trimmed)
  );
}

const MIME_BY_EXT: Record<string, string> = {
  png: "image/png",
  jpg: "image/jpeg",
  jpeg: "image/jpeg",
  webp: "image/webp",
  gif: "image/gif",
  heic: "image/heic",
  heif: "image/heif",
  ogg: "audio/ogg",
  oga: "audio/ogg",
  mp3: "audio/mpeg",
  wav: "audio/wav",
  flac: "audio/flac",
  aac: "audio/aac",
  opus: "audio/opus",
  m4a: "audio/mp4",
  mp4: "video/mp4",
  mov: "video/quicktime",
  pdf: "application/pdf",
  txt: "text/plain",
  md: "text/markdown",
  csv: "text/csv",
  json: "application/json",
  zip: "application/zip",
};

function getFileExtension(url: string): string | undefined {
  const trimmed = url.trim();
  if (!trimmed) {
    return undefined;
  }
  const source = (() => {
    try {
      if (/^https?:\/\//i.test(trimmed)) {
        return new URL(trimmed).pathname;
      }
    } catch {}
    return trimmed;
  })();
  const fileName = source.split(/[\\/]/).pop() ?? source;
  const match = /\.([a-zA-Z0-9]+)$/.exec(fileName);
  return match?.[1]?.toLowerCase();
}

function mimeTypeFromUrl(url: string): string | undefined {
  const ext = getFileExtension(url);
  return ext ? MIME_BY_EXT[ext] : undefined;
}

function inferAttachmentKind(url: string): {
  kind: "image" | "audio" | "video" | "document";
  mimeType?: string;
  label: string;
} {
  const mimeType = mimeTypeFromUrl(url);
  const kind = mediaKindFromMime(mimeType) ?? "document";
  const label = (() => {
    try {
      if (/^https?:\/\//i.test(url)) {
        const parsed = new URL(url);
        const name = parsed.pathname.split("/").pop()?.trim();
        return name || parsed.hostname || url;
      }
    } catch {}
    const name = url.split(/[\\/]/).pop()?.trim();
    return name || url;
  })();
  return { kind, mimeType, label };
}

function mergeAdjacentTextItems(items: MessageContentItem[]): MessageContentItem[] {
  const merged: MessageContentItem[] = [];
  for (const item of items) {
    const previous = merged[merged.length - 1];
    if (item.type === "text" && previous?.type === "text") {
      previous.text = [previous.text, item.text].filter((value) => value !== undefined).join("\n");
      continue;
    }
    merged.push(item);
  }
  return merged.filter((item) => item.type !== "text" || Boolean(item.text?.trim()));
}

function expandTextContent(text: string): {
  content: MessageContentItem[];
  audioAsVoice: boolean;
  replyTarget: NormalizedMessage["replyTarget"];
} {
  const extracted = extractCanvasShortcodes(text);
  const parsed = splitMediaFromOutput(extracted.text);
  const parts: MessageContentItem[] = [];
  let audioAsVoice = parsed.audioAsVoice === true;
  let replyTarget: NormalizedMessage["replyTarget"] = null;
  const segments = parsed.segments ?? [{ type: "text" as const, text: parsed.text }];

  for (const segment of segments) {
    if (segment.type === "media") {
      if (!isRenderableAssistantAttachment(segment.url)) {
        if (shouldPreserveRelativeAssistantAttachment(segment.url)) {
          parts.push({ type: "text", text: `MEDIA:${segment.url}` });
        }
        continue;
      }
      const inferred = inferAttachmentKind(segment.url);
      parts.push({
        type: "attachment",
        attachment: {
          url: segment.url,
          kind: inferred.kind,
          label: inferred.label,
          mimeType: inferred.mimeType,
        },
      });
      continue;
    }

    const directives = parseInlineDirectives(segment.text, {
      stripAudioTag: true,
      stripReplyTags: true,
    });
    audioAsVoice = audioAsVoice || directives.audioAsVoice;
    if (directives.replyToExplicitId) {
      replyTarget = { kind: "id", id: directives.replyToExplicitId };
    } else if (directives.replyToCurrent && replyTarget === null) {
      replyTarget = { kind: "current" };
    }
    if (directives.text) {
      parts.push({ type: "text", text: directives.text });
    }
  }
  for (const preview of extracted.previews) {
    parts.push({ type: "canvas", preview, rawText: null });
  }

  const content = mergeAdjacentTextItems(
    parts.map((item) => {
      if (item.type === "attachment" && item.attachment.kind === "audio" && audioAsVoice) {
        return Object.assign({}, item, { attachment: { ...item.attachment, isVoiceNote: true } });
      }
      return item;
    }),
  );

  return {
    content:
      content.length > 0
        ? content
        : (parsed.mediaUrls ?? []).some((url) => shouldPreserveRelativeAssistantAttachment(url))
          ? (parsed.mediaUrls ?? [])
              .filter((url) => shouldPreserveRelativeAssistantAttachment(url))
              .map((url) => ({ type: "text" as const, text: `MEDIA:${url}` }))
          : replyTarget === null && !audioAsVoice && parsed.text.trim().length > 0
            ? [{ type: "text", text: parsed.text }]
            : [],
    audioAsVoice,
    replyTarget,
  };
}

/**
 * Normalize a raw message object into a consistent structure.
 */
export function normalizeMessage(message: unknown): NormalizedMessage {
  const m = message as Record<string, unknown>;
  let role = typeof m.role === "string" ? m.role : "unknown";

  // Detect tool messages by common gateway shapes.
  // Some tool events come through as assistant role with tool_* items in the content array.
  const hasToolId = typeof m.toolCallId === "string" || typeof m.tool_call_id === "string";

  const contentRaw = m.content;
  const contentItems = Array.isArray(contentRaw) ? contentRaw : null;
  const hasToolContent =
    Array.isArray(contentItems) &&
    contentItems.some((item) => {
      const x = item as Record<string, unknown>;
      return isToolResultContentType(x.type) || isToolCallContentType(x.type);
    });

  const hasToolName = typeof m.toolName === "string" || typeof m.tool_name === "string";

  if (hasToolId || hasToolContent || hasToolName) {
    role = "toolResult";
  }
  const isAssistantMessage = role === "assistant";

  // Extract content
  let content: MessageContentItem[] = [];
  let audioAsVoice = false;
  let replyTarget: NormalizedMessage["replyTarget"] = null;

  if (typeof m.content === "string") {
    if (isAssistantMessage) {
      const expanded = expandTextContent(m.content);
      content = expanded.content;
      audioAsVoice = expanded.audioAsVoice;
      replyTarget = expanded.replyTarget;
    } else {
      content = [{ type: "text", text: m.content }];
    }
  } else if (Array.isArray(m.content)) {
    content = m.content.flatMap((item: Record<string, unknown>) => {
      if (
        item.type === "attachment" &&
        item.attachment &&
        typeof item.attachment === "object" &&
        !Array.isArray(item.attachment)
      ) {
        const attachment = item.attachment as {
          url?: unknown;
          kind?: unknown;
          label?: unknown;
          mimeType?: unknown;
          isVoiceNote?: unknown;
        };
        if (
          typeof attachment.url !== "string" ||
          (attachment.kind !== "image" &&
            attachment.kind !== "audio" &&
            attachment.kind !== "video" &&
            attachment.kind !== "document") ||
          typeof attachment.label !== "string"
        ) {
          return [];
        }
        return [
          {
            type: "attachment" as const,
            attachment: {
              url: attachment.url,
              kind: attachment.kind,
              label: attachment.label,
              ...(typeof attachment.mimeType === "string" ? { mimeType: attachment.mimeType } : {}),
              ...(attachment.isVoiceNote === true ? { isVoiceNote: true } : {}),
            },
          },
        ];
      }
      if (
        item.type === "canvas" &&
        item.preview &&
        typeof item.preview === "object" &&
        !Array.isArray(item.preview)
      ) {
        const preview = coerceCanvasPreview(item.preview);
        if (!preview) {
          return [];
        }
        return [
          {
            type: "canvas" as const,
            preview,
            rawText: typeof item.rawText === "string" ? item.rawText : null,
          },
        ];
      }
      if (item.type === "text" && typeof item.text === "string" && isAssistantMessage) {
        const expanded = expandTextContent(item.text);
        audioAsVoice = audioAsVoice || expanded.audioAsVoice;
        if (expanded.replyTarget?.kind === "id") {
          replyTarget = expanded.replyTarget;
        } else if (expanded.replyTarget?.kind === "current" && replyTarget === null) {
          replyTarget = expanded.replyTarget;
        }
        return expanded.content;
      }
      return [
        {
          type:
            (item.type as Extract<
              MessageContentItem,
              { type: "text" | "tool_call" | "tool_result" }
            >["type"]) || "text",
          text: item.text as string | undefined,
          name: item.name as string | undefined,
          args: resolveToolBlockArgs(item),
        },
      ];
    });
  } else if (typeof m.text === "string") {
    if (isAssistantMessage) {
      const expanded = expandTextContent(m.text);
      content = expanded.content;
      audioAsVoice = expanded.audioAsVoice;
      replyTarget = expanded.replyTarget;
    } else {
      content = [{ type: "text", text: m.text }];
    }
  }

  const timestamp = typeof m.timestamp === "number" ? m.timestamp : Date.now();
  const id = typeof m.id === "string" ? m.id : undefined;
  const senderLabel =
    typeof m.senderLabel === "string" && m.senderLabel.trim() ? m.senderLabel.trim() : null;

  // Strip AI-injected metadata prefix blocks from user messages before display.
  if (role === "user" || role === "User") {
    content = content.map((item) => {
      if (item.type === "text" && typeof item.text === "string") {
        return { ...item, text: stripInboundMetadata(item.text) };
      }
      return item;
    });
  }

  return {
    role,
    content,
    timestamp,
    id,
    senderLabel,
    ...(audioAsVoice ? { audioAsVoice: true } : {}),
    ...(replyTarget ? { replyTarget } : {}),
  };
}

/**
 * Normalize role for grouping purposes.
 */
export function normalizeRoleForGrouping(role: string): string {
  const lower = role.toLowerCase();
  // Preserve original casing when it's already a core role.
  if (role === "user" || role === "User") {
    return role;
  }
  if (role === "assistant") {
    return "assistant";
  }
  if (role === "system") {
    return "system";
  }
  // Keep tool-related roles distinct so the UI can style/toggle them.
  if (
    lower === "toolresult" ||
    lower === "tool_result" ||
    lower === "tool" ||
    lower === "function"
  ) {
    return "tool";
  }
  return role;
}

/**
 * Check if a message is a tool result message based on its role.
 */
export function isToolResultMessage(message: unknown): boolean {
  const m = message as Record<string, unknown>;
  const role = typeof m.role === "string" ? m.role.toLowerCase() : "";
  return role === "toolresult" || role === "tool_result";
}

¤ Dauer der Verarbeitung: 0.21 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.