import { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback"; import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline"; import {
resolveSendableOutboundReplyParts,
resolveTextChunksWithFallback,
sendMediaWithLeadingCaption,
} from "openclaw/plugin-sdk/reply-payload"; import { resolveFeishuRuntimeAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { sendMediaFeishu } from "./media.js"; import type { MentionTarget } from "./mention-target.types.js"; import { buildMentionedCardContent } from "./mention.js"; import {
createReplyPrefixContext,
type ClawdbotConfig,
type OutboundIdentity,
type ReplyPayload,
type RuntimeEnv,
} from "./reply-dispatcher-runtime-api.js"; import { getFeishuRuntime } from "./runtime.js"; import { sendMessageFeishu, sendStructuredCardFeishu, type CardHeaderConfig } from "./send.js"; import { FeishuStreamingSession, mergeStreamingText } from "./streaming-card.js"; import { resolveReceiveIdType } from "./targets.js"; import { addTypingIndicator, removeTypingIndicator, type TypingIndicatorState } from "./typing.js";
/** Detect if text contains markdown elements that benefit from card rendering */ function shouldUseCard(text: string): boolean { return /```[\s\S]*?```/.test(text) || /\|.+\|[\r\n]+\|[-:| ]+\|/.test(text);
}
/** Maximum age (ms) for a message to receive a typing indicator reaction.
* Messages older than this are likely replays after context compaction (#30418). */ const TYPING_INDICATOR_MAX_AGE_MS = 2 * 60_000; const MS_EPOCH_MIN = 1_000_000_000_000; const STREAMING_START_FAILURE_BACKOFF_MS = 60_000; const streamingStartBackoffUntilByAccount = new Map<string, number>();
function isStreamingStartBackedOff(accountId: string, now = Date.now()): boolean { const backoffUntil = streamingStartBackoffUntilByAccount.get(accountId); if (backoffUntil === undefined) { returnfalse;
} if (backoffUntil <= now) {
streamingStartBackoffUntilByAccount.delete(accountId); returnfalse;
} returntrue;
}
function rememberStreamingStartFailure(accountId: string, now = Date.now()): number { const backoffUntil = now + STREAMING_START_FAILURE_BACKOFF_MS;
streamingStartBackoffUntilByAccount.set(accountId, backoffUntil); return backoffUntil;
}
export function clearFeishuStreamingStartBackoffForTests() {
streamingStartBackoffUntilByAccount.clear();
}
function normalizeEpochMs(timestamp: number | undefined): number | undefined { if (!Number.isFinite(timestamp) || timestamp === undefined || timestamp <= 0) { return undefined;
} // Defensive normalization: some payloads use seconds, others milliseconds. // Values below 1e12 are treated as epoch-seconds. return timestamp < MS_EPOCH_MIN ? timestamp * 1000 : timestamp;
}
const closeStreaming = async () => { if (streamingStartPromise) {
await streamingStartPromise;
}
await partialUpdateQueue; if (streaming?.isActive()) {
let text = buildCombinedStreamText(reasoningText, streamText); if (mentionTargets?.length) {
text = buildMentionedCardContent(mentionTargets, text);
} const finalNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
await streaming.close(text, { note: finalNote }); // Track the raw streamed text so the duplicate-final check in deliver() // can skip the redundant text delivery that arrives after onIdle closes // the streaming card. if (streamText) {
deliveredFinalTexts.add(streamText);
}
}
streaming = null;
streamingStartPromise = null;
streamText = "";
lastPartial = "";
reasoningText = "";
};
if (info?.kind === "block") { // Drop internal block chunks unless we can safely consume them as // streaming-card fallback content. if (!(streamingEnabled && useCard)) { return;
}
startStreaming(); if (streamingStartPromise) {
await streamingStartPromise;
}
}
if (info?.kind === "final" && streamingEnabled && useCard) {
startStreaming(); if (streamingStartPromise) {
await streamingStartPromise;
}
}
if (streaming?.isActive()) { if (info?.kind === "block") { // Some runtimes emit block payloads without onPartial/final callbacks. // Mirror block text into streamText so onIdle close still sends content.
queueStreamingUpdate(text, { mode: "delta" });
} if (info?.kind === "final") {
streamText = mergeStreamingText(streamText, text);
await closeStreaming();
deliveredFinalTexts.add(text);
} // Send media even when streaming handled the text if (hasMedia) {
await sendMediaReplies(payload);
} return;
}
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.