import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; import {
createChannelReplyPipeline,
logTypingFailure,
resolveChannelMediaMaxBytes,
type OpenClawConfig,
type MSTeamsReplyStyle,
type RuntimeEnv,
} from "../runtime-api.js"; import type { MSTeamsAccessTokenProvider } from "./attachments/types.js"; import type { StoredConversationReference } from "./conversation-store.js"; import {
classifyMSTeamsSendError,
formatMSTeamsSendErrorHint,
formatUnknownError,
} from "./errors.js"; import {
buildConversationReference,
type MSTeamsAdapter,
type MSTeamsRenderedMessage,
renderReplyPayloadsToMessages,
sendMSTeamsMessages,
} from "./messenger.js"; import type { MSTeamsMonitorLogger } from "./monitor-types.js"; import { createTeamsReplyStreamController } from "./reply-stream-controller.js"; import { withRevokedProxyFallback } from "./revoked-context.js"; import { getMSTeamsRuntime } from "./runtime.js"; import type { MSTeamsTurnContext } from "./sdk-types.js";
export { pickInformativeStatusText } from "./reply-stream-controller.js";
/** * Keepalive cadence for the typing indicator while the bot is running * (including long tool chains). Bot Framework 1:1 TurnContext proxies * expire after ~30s of inactivity; sending a typing activity every 8s * keeps the proxy alive so the post-tool reply can still land via the * turn context. Sits in the middle of the 5-10s range recommended in * #59731.
*/ const TYPING_KEEPALIVE_INTERVAL_MS = 8_000;
/** * TTL ceiling for the typing keepalive loop. The default in * createTypingCallbacks is 60s, which is too short for the Teams long tool * chains described in #59731 (60s+ total runs are common). Give tool * chains up to 10 minutes before auto-stopping the keepalive.
*/ const TYPING_KEEPALIVE_MAX_DURATION_MS = 10 * 60_000;
// Forward reference: sendTypingIndicator is built before the stream // controller exists, but the keepalive tick needs to check stream state so // we don't overlay "..." typing on the visible streaming card. The ref is // wired once the stream controller is constructed below. const streamActiveRef: { current: () => boolean } = { current: () => false };
const sendTypingIndicator = isTypingSupported
? async () => { // While the streaming card is actively being updated the user // already sees a live indicator in the stream — don't overlay a // plain "..." typing on top of it. Between segments (tool chain) // the stream is finalized, so typing indicators are appropriate // and they are what keep the TurnContext alive. See #59731. if (streamActiveRef.current()) { return;
}
await rawSendTypingIndicator();
}
: async () => {};
const queueDeliveryFailureSystemEvent = (failure: {
failed: number;
total: number;
error: unknown;
}) => { const classification = classifyMSTeamsSendError(failure.error); const errorText = formatUnknownError(failure.error); const failedAll = failure.failed >= failure.total; const summary = failedAll
? "the previous reply was not delivered"
: `${failure.failed} of ${failure.total} message blocks were not delivered`; const sentences = [
`Microsoft Teams delivery failed: ${summary}.`,
`The user may not have received ${failedAll ? "that reply" : "the full reply"}.`,
`Error: ${errorText}.`,
classification.statusCode != null ? `Status: ${classification.statusCode}.` : undefined,
classification.kind === "transient" || classification.kind === "throttled"
? "Retrying later may succeed."
: undefined,
].filter(Boolean);
core.system.enqueueSystemEvent(sentences.join(" "), {
sessionKey: params.sessionKey,
contextKey: `msteams:delivery-failure:${params.conversationRef.conversation?.id ?? "unknown"}`,
});
};
const flushPendingMessages = async () => { if (pendingMessages.length === 0) { return;
} const toSend = pendingMessages.splice(0); const total = toSend.length;
let ids: string[]; try {
ids = await sendMessages(toSend);
} catch (batchError) {
ids = [];
let failed = 0;
let lastFailedError: unknown = batchError; for (const msg of toSend) { try { const msgIds = await sendMessages([msg]);
ids.push(...msgIds);
} catch (msgError) {
failed += 1;
lastFailedError = msgError;
params.log.debug?.("individual message send failed, continuing with remaining blocks");
}
} if (failed > 0) {
params.log.warn?.(`failed to deliver ${failed} of ${total} message blocks`, {
failed,
total,
});
queueDeliveryFailureSystemEvent({
failed,
total,
error: lastFailedError,
});
}
} if (ids.length > 0) {
params.onSentMessageIds?.(ids);
}
};
const {
dispatcher,
replyOptions,
markDispatchIdle: baseMarkDispatchIdle,
} = core.channel.reply.createReplyDispatcherWithTyping({
...replyPipeline,
humanDelay: core.channel.reply.resolveHumanDelayConfig(params.cfg, params.agentId),
onReplyStart: async () => {
await streamController.onReplyStart(); // Always start the typing keepalive loop when typing is enabled and // supported by this conversation type. The sendTypingIndicator gate // skips actual sends while the stream card is visually active, so // during the first text segment the user only sees the streaming UI. // Once the stream finalizes (between segments / during tool chains), // the loop starts sending typing activities and keeps the Bot Framework // TurnContext alive so the post-tool reply can still land. See #59731. if (typingIndicatorEnabled) {
await typingCallbacks?.onReplyStart?.();
}
},
typingCallbacks,
deliver: async (payload) => { const preparedPayload = streamController.preparePayload(payload); if (!preparedPayload) { 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.