msteams: add typingIndicator config and prevent duplicate DM typing indicator (#60771)

* msteams: add typingIndicator config and avoid duplicate DM typing

* fix(msteams): validate typingIndicator config

* fix(msteams): stop streaming before Teams timeout

* fix(msteams): classify expired streams correctly

* fix(msteams): handle link text from html attachments

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
This commit is contained in:
Brad Groux
2026-04-04 04:34:24 -05:00
committed by GitHub
parent af4e9d19cf
commit fce81fccd8
9 changed files with 193 additions and 7 deletions

View File

@@ -63,6 +63,32 @@ function extractStatusCode(err: unknown): number | null {
return null;
}
function extractErrorCode(err: unknown): string | null {
if (!isRecord(err)) {
return null;
}
const direct = err.code;
if (typeof direct === "string" && direct.trim()) {
return direct;
}
const response = err.response;
if (!isRecord(response)) {
return null;
}
const body = response.body;
if (isRecord(body)) {
const error = body.error;
if (isRecord(error) && typeof error.code === "string" && error.code.trim()) {
return error.code;
}
}
return null;
}
function extractRetryAfterMs(err: unknown): number | null {
if (!isRecord(err)) {
return null;
@@ -129,6 +155,7 @@ export type MSTeamsSendErrorClassification = {
kind: MSTeamsSendErrorKind;
statusCode?: number;
retryAfterMs?: number;
errorCode?: string;
};
/**
@@ -142,9 +169,17 @@ export type MSTeamsSendErrorClassification = {
export function classifyMSTeamsSendError(err: unknown): MSTeamsSendErrorClassification {
const statusCode = extractStatusCode(err);
const retryAfterMs = extractRetryAfterMs(err);
const errorCode = extractErrorCode(err) ?? undefined;
if (statusCode === 401 || statusCode === 403) {
return { kind: "auth", statusCode };
if (statusCode === 401) {
return { kind: "auth", statusCode, errorCode };
}
if (statusCode === 403) {
if (errorCode === "ContentStreamNotAllowed") {
return { kind: "permanent", statusCode, errorCode };
}
return { kind: "auth", statusCode, errorCode };
}
if (statusCode === 429) {
@@ -152,6 +187,7 @@ export function classifyMSTeamsSendError(err: unknown): MSTeamsSendErrorClassifi
kind: "throttled",
statusCode,
retryAfterMs: retryAfterMs ?? undefined,
errorCode,
};
}
@@ -160,17 +196,19 @@ export function classifyMSTeamsSendError(err: unknown): MSTeamsSendErrorClassifi
kind: "transient",
statusCode,
retryAfterMs: retryAfterMs ?? undefined,
errorCode,
};
}
if (statusCode != null && statusCode >= 400) {
return { kind: "permanent", statusCode };
return { kind: "permanent", statusCode, errorCode };
}
return {
kind: "unknown",
statusCode: statusCode ?? undefined,
retryAfterMs: retryAfterMs ?? undefined,
errorCode,
};
}
@@ -195,6 +233,9 @@ export function formatMSTeamsSendErrorHint(
if (classification.kind === "auth") {
return "check msteams appId/appPassword/tenantId (or env vars MSTEAMS_APP_ID/MSTEAMS_APP_PASSWORD/MSTEAMS_TENANT_ID)";
}
if (classification.errorCode === "ContentStreamNotAllowed") {
return "Teams expired the content stream; stop streaming earlier and fall back to normal message delivery";
}
if (classification.kind === "throttled") {
return "Teams throttled the bot; backing off may help";
}