Files
openclaw/extensions/msteams/src/probe.ts
Sid Uppal cd90130877 msteams: implement Teams AI agent UX best practices (#51808)
Migrates the Teams extension from @microsoft/agents-hosting to the official Teams SDK (@microsoft/teams.apps + @microsoft/teams.api) and implements Microsoft's AI UX best practices for Teams agents.

- AI-generated label on all bot messages (Teams native badge + thumbs up/down)
- Streaming responses in 1:1 chats via Teams streaminfo protocol
- Welcome card with configurable prompt starters on bot install
- Feedback with reflective learning (negative feedback triggers background reflection)
- Typing indicators for personal + group chats (disabled for channels)
- Informative status updates (progress bar while LLM processes)
- JWT validation via Teams SDK createServiceTokenValidator
- User-Agent: teams.ts[apps]/<sdk-version> OpenClaw/<version> on outbound requests
- Fix copy-pasted image downloads (smba.trafficmanager.net auth allowlist)
- Pre-parse auth gate (reject unauthenticated requests before body parsing)
- Reflection dispatcher lifecycle fix (prevent leaked dispatchers)
- Colon-safe session filenames (Windows compatibility)
- Cooldown cache eviction (prevent unbounded memory growth)

Closes #51806
2026-03-23 22:03:39 -07:00

103 lines
2.9 KiB
TypeScript

import {
normalizeStringEntries,
type BaseProbeResult,
type MSTeamsConfig,
} from "../runtime-api.js";
import { formatUnknownError } from "./errors.js";
import { createMSTeamsTokenProvider, loadMSTeamsSdkWithAuth } from "./sdk.js";
import { readAccessToken } from "./token-response.js";
import { resolveMSTeamsCredentials } from "./token.js";
export type ProbeMSTeamsResult = BaseProbeResult<string> & {
appId?: string;
graph?: {
ok: boolean;
error?: string;
roles?: string[];
scopes?: string[];
};
};
function decodeJwtPayload(token: string): Record<string, unknown> | null {
const parts = token.split(".");
if (parts.length < 2) {
return null;
}
const payload = parts[1] ?? "";
const padded = payload.padEnd(payload.length + ((4 - (payload.length % 4)) % 4), "=");
const normalized = padded.replace(/-/g, "+").replace(/_/g, "/");
try {
const decoded = Buffer.from(normalized, "base64").toString("utf8");
const parsed = JSON.parse(decoded) as Record<string, unknown>;
return parsed && typeof parsed === "object" ? parsed : null;
} catch {
return null;
}
}
function readStringArray(value: unknown): string[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const out = normalizeStringEntries(value);
return out.length > 0 ? out : undefined;
}
function readScopes(value: unknown): string[] | undefined {
if (typeof value !== "string") {
return undefined;
}
const out = value
.split(/\s+/)
.map((entry) => entry.trim())
.filter(Boolean);
return out.length > 0 ? out : undefined;
}
export async function probeMSTeams(cfg?: MSTeamsConfig): Promise<ProbeMSTeamsResult> {
const creds = resolveMSTeamsCredentials(cfg);
if (!creds) {
return {
ok: false,
error: "missing credentials (appId, appPassword, tenantId)",
};
}
try {
const { app } = await loadMSTeamsSdkWithAuth(creds);
const tokenProvider = createMSTeamsTokenProvider(app);
const botTokenValue = await tokenProvider.getAccessToken("https://api.botframework.com");
if (!botTokenValue) {
throw new Error("Failed to acquire bot token");
}
let graph:
| {
ok: boolean;
error?: string;
roles?: string[];
scopes?: string[];
}
| undefined;
try {
const graphTokenValue = await tokenProvider.getAccessToken("https://graph.microsoft.com");
const accessToken = readAccessToken(graphTokenValue);
const payload = accessToken ? decodeJwtPayload(accessToken) : null;
graph = {
ok: true,
roles: readStringArray(payload?.roles),
scopes: readScopes(payload?.scp),
};
} catch (err) {
graph = { ok: false, error: formatUnknownError(err) };
}
return { ok: true, appId: creds.appId, ...(graph ? { graph } : {}) };
} catch (err) {
return {
ok: false,
appId: creds.appId,
error: formatUnknownError(err),
};
}
}