openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
120 lines (119 loc) • 10.3 kB
JavaScript
import { a as asOptionalRecord } from "./record-coerce-TIsquoVR.mjs";
import { b as redactToolPayloadText } from "./redact-DJ8j6ADa.mjs";
import { t as escapeHtml } from "./html-escape-BMD_QFeA.mjs";
import { n as HEARTBEAT_PROMPT } from "./heartbeat-BjLIgeQc.mjs";
import { t as extractAssistantPhaseText } from "./chat-message-content-C-d0FKNG.mjs";
import { l as sanitizeAssistantVisibleTextWithProfile } from "./assistant-visible-text-Bv6mP5pg.mjs";
import "./input-provenance-D1LHo5Qj.mjs";
import { t as stripInternalMetadataForDisplay } from "./display-text-sanitize-BtEixrp8.mjs";
import { n as isHeartbeatOkResponse, r as isHeartbeatUserMessage } from "./heartbeat-filter-CNihh_E4.mjs";
import { t as MarkdownItCallable } from "./markdown-it-DHkpg-if.mjs";
import { n as splitMediaOutput } from "./parse-output-DA26FHkG.mjs";
import { i as stripSuppressedControlReplyToken } from "./control-reply-text-w0dVwMAz.mjs";
import { t as stripUserEnvelopeForDisplay } from "./user-envelope-display-PUowpePW.mjs";
//#region src/gateway/control-ui-public-session-render.ts
const MAX_MESSAGES = 100;
const MAX_MESSAGE_CHARS = 32768;
const MAX_DOCUMENT_CHARS = 262144;
const markdown = new MarkdownItCallable({
html: false,
linkify: false,
breaks: true
});
markdown.validateLink = (value) => {
try {
const url = new URL(value);
return (url.protocol === "https:" || url.protocol === "http:") && !url.username && !url.password;
} catch {
return false;
}
};
markdown.renderer.rules.image = () => "<span class=\"omitted\">[Image omitted]</span>";
markdown.renderer.rules.link_open = (tokens, index, options, _env, renderer) => {
tokens[index]?.attrSet("rel", "noreferrer noopener nofollow");
return renderer.renderToken(tokens, index, options);
};
function publicMessageText(message) {
const entry = asOptionalRecord(message);
if (!entry || entry.role !== "user" && entry.role !== "assistant" || entry.display === false || entry.customType !== void 0 || entry.senderSession !== void 0 || entry.toolCallId !== void 0 || entry.tool_call_id !== void 0) return;
const provenance = asOptionalRecord(entry.provenance);
if (entry.provenance !== void 0 && provenance?.kind !== "external_user") return;
let text;
if (entry.role === "assistant") {
if (entry.phase !== void 0 && entry.phase !== "final_answer") return;
text = extractAssistantPhaseText(entry);
} else if (typeof entry.content === "string") text = entry.content;
else if (Array.isArray(entry.content)) text = entry.content.flatMap((value) => {
const block = asOptionalRecord(value);
return (block?.type === "text" || block?.type === "input_text") && typeof block.text === "string" ? [block.text] : [];
}).join("\n\n");
else if (typeof entry.text === "string") text = entry.text;
if (!text || text.includes("[Inter-session message]")) return;
text = entry.role === "user" ? stripUserEnvelopeForDisplay(text) : stripInternalMetadataForDisplay(text);
if (entry.role === "assistant") text = sanitizeAssistantVisibleTextWithProfile(text, "history", true);
const roleContent = {
role: entry.role,
content: text
};
if (isHeartbeatUserMessage(roleContent, HEARTBEAT_PROMPT) || isHeartbeatOkResponse(roleContent)) return;
if (entry.role === "assistant") text = stripSuppressedControlReplyToken(text);
text = splitMediaOutput(text, { extractAudioDirectives: false }).text;
text = redactToolPayloadText(text).trim();
return text ? {
role: entry.role,
text
} : void 0;
}
function renderPublicSessionDocument(params) {
const isLatest = params.isLatest !== false;
const title = escapeHtml(redactToolPayloadText(stripInternalMetadataForDisplay(params.title)).slice(0, 200).trim() || "Shared conversation");
let truncated = params.truncated || params.messages.length > MAX_MESSAGES;
let remaining = MAX_DOCUMENT_CHARS;
const rows = [];
for (const value of params.messages.slice(-100).toReversed()) {
const message = publicMessageText(value);
if (!message) continue;
if (remaining <= 0) {
truncated = true;
break;
}
const text = message.text.slice(0, Math.min(MAX_MESSAGE_CHARS, remaining));
const clipped = text.length < message.text.length;
truncated ||= clipped;
remaining -= text.length;
rows.push(`<article class="message ${message.role}" aria-label="${message.role === "user" ? "User" : "Assistant"} message"><h2>${message.role === "user" ? "User" : "OpenClaw"}</h2><div class="content">${markdown.render(text)}${clipped ? "<p class=\"omitted\">Message shortened for this public view.</p>" : ""}</div></article>`);
}
const description = "A public, read-only OpenClaw conversation. No login required.";
const canonicalMetadata = params.canonicalUrl ? `<link rel="canonical" href="${escapeHtml(params.canonicalUrl)}">
<meta property="og:url" content="${escapeHtml(params.canonicalUrl)}">` : "";
const navigation = params.olderUrl ? `<nav class="pagination" aria-label="Conversation pages"><a href="${escapeHtml(params.olderUrl)}" rel="prev">← Older messages</a></nav>` : "";
return `<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="referrer" content="no-referrer"><meta name="robots" content="noindex, nofollow">
${isLatest ? "<meta http-equiv=\"refresh\" content=\"15\">" : ""}
<title>${title} · OpenClaw</title>${canonicalMetadata}
<meta property="og:type" content="website"><meta property="og:site_name" content="OpenClaw">
<meta property="og:title" content="${title}"><meta property="og:description" content="${description}">
<meta property="og:image" content="${escapeHtml(params.cardUrl)}">
<meta name="twitter:card" content="summary_large_image">
<style>
:root{color-scheme:dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:
*{box-sizing:border-box}body{margin:0;border-top:3px solid
main{width:min(820px,calc(100% - 40px));margin:0 auto;padding:44px 0 60px}.brand{font-weight:750;letter-spacing:-.03em;font-size:19px;color:
h1{font-size:clamp(28px,5vw,42px);line-height:1.15;letter-spacing:-.045em;margin:40px 0 16px;overflow-wrap:anywhere}.intro{color:
.pagination{display:flex;justify-content:space-between;gap:18px;margin:24px 0;font-size:13px}.pagination a{padding:9px 0}.transcript{border-top:1px solid
@media(max-width:560px){main{width:calc(100% - 32px);padding-top:26px}.message{display:block;padding:23px 0}.message h2{margin:0 0 10px}.content h2{margin:24px 0 12px}.badge{font-size:11px}h1{margin-top:32px}footer{display:block}footer a{display:inline-block;margin-top:10px}}
</style></head><body><main>
<header><div class="masthead"><div class="brand"><span aria-hidden="true">✳</span>OpenClaw</div><span class="badge">Public · Read-only</span></div>
<h1>${title}</h1><p class="intro"><strong>Shared with everyone, no login required.</strong> This ${isLatest ? "live view" : "page"} includes conversation text. Tool output, files, images, reasoning, and interactive content are omitted.</p></header>
${!isLatest ? `<p class="page-label">Earlier conversation · <a href="${escapeHtml(params.latestUrl)}">Back to latest</a></p>` : ""}
${truncated ? "<aside class=\"notice\">Some messages or long text are omitted to keep this public page within its size limit.</aside>" : ""}
${navigation}
<section class="transcript" aria-label="Conversation">${rows.length ? rows.toReversed().join("\n") : `<p class="empty">${isLatest ? "No public conversation text yet. New messages will appear here as the conversation continues." : "No public conversation text on this page. Use the page links to continue reading."}</p>`}</section>
${navigation}
<footer><span>${isLatest ? "Live view · Refreshes every 15 seconds" : "Earlier conversation · Updates when you reload"}<br>Public access can be revoked by the session owner.</span><a href="${escapeHtml(params.latestUrl)}">${isLatest ? "Refresh now" : "Back to latest"}</a></footer>
</main></body></html>`;
}
//#endregion
export { renderPublicSessionDocument };