feat(msteams): add MS Teams provider skeleton
- Add Microsoft 365 Agents SDK packages (@microsoft/agents-hosting, @microsoft/agents-hosting-express, @microsoft/agents-hosting-extensions-teams) - Add MSTeamsConfig type and zod schema - Create src/msteams/ provider with monitor, token, send, probe - Wire provider into gateway (server-providers.ts, server.ts) - Add msteams to all provider type unions (hooks, queue, cron, etc.) - Update implementation guide with new SDK and progress
This commit is contained in:
@@ -87,6 +87,7 @@ export type AgentElevatedAllowFromConfig = {
|
||||
slack?: Array<string | number>;
|
||||
signal?: Array<string | number>;
|
||||
imessage?: Array<string | number>;
|
||||
msteams?: Array<string | number>;
|
||||
webchat?: Array<string | number>;
|
||||
};
|
||||
|
||||
@@ -214,7 +215,8 @@ export type HookMappingConfig = {
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
| "imessage"
|
||||
| "msteams";
|
||||
to?: string;
|
||||
/** Override model for this hook (provider/model or alias). */
|
||||
model?: string;
|
||||
@@ -569,6 +571,64 @@ export type SignalConfig = {
|
||||
accounts?: Record<string, SignalAccountConfig>;
|
||||
} & SignalAccountConfig;
|
||||
|
||||
export type MSTeamsWebhookConfig = {
|
||||
/** Port for the webhook server. Default: 3978. */
|
||||
port?: number;
|
||||
/** Path for the messages endpoint. Default: /api/messages. */
|
||||
path?: string;
|
||||
};
|
||||
|
||||
/** Reply style for MS Teams messages. */
|
||||
export type MSTeamsReplyStyle = "thread" | "top-level";
|
||||
|
||||
/** Channel-level config for MS Teams. */
|
||||
export type MSTeamsChannelConfig = {
|
||||
/** Require @mention to respond. Default: true. */
|
||||
requireMention?: boolean;
|
||||
/** Reply style: "thread" replies to the message, "top-level" posts a new message. */
|
||||
replyStyle?: MSTeamsReplyStyle;
|
||||
};
|
||||
|
||||
/** Team-level config for MS Teams. */
|
||||
export type MSTeamsTeamConfig = {
|
||||
/** Default requireMention for channels in this team. */
|
||||
requireMention?: boolean;
|
||||
/** Default reply style for channels in this team. */
|
||||
replyStyle?: MSTeamsReplyStyle;
|
||||
/** Per-channel overrides. Key is conversation ID (e.g., "19:...@thread.tacv2"). */
|
||||
channels?: Record<string, MSTeamsChannelConfig>;
|
||||
};
|
||||
|
||||
export type MSTeamsConfig = {
|
||||
/** If false, do not start the MS Teams provider. Default: true. */
|
||||
enabled?: boolean;
|
||||
/** Azure Bot App ID (from Azure Bot registration). */
|
||||
appId?: string;
|
||||
/** Azure Bot App Password / Client Secret. */
|
||||
appPassword?: string;
|
||||
/** Azure AD Tenant ID (for single-tenant bots). */
|
||||
tenantId?: string;
|
||||
/** Webhook server configuration. */
|
||||
webhook?: MSTeamsWebhookConfig;
|
||||
/** Direct message access policy (default: pairing). */
|
||||
dmPolicy?: DmPolicy;
|
||||
/** Allowlist for DM senders (AAD object IDs or UPNs). */
|
||||
allowFrom?: Array<string>;
|
||||
/** Outbound text chunk size (chars). Default: 4000. */
|
||||
textChunkLimit?: number;
|
||||
/**
|
||||
* Allowed host suffixes for inbound attachment downloads.
|
||||
* Use ["*"] to allow any host (not recommended).
|
||||
*/
|
||||
mediaAllowHosts?: Array<string>;
|
||||
/** Default: require @mention to respond in channels/groups. */
|
||||
requireMention?: boolean;
|
||||
/** Default reply style: "thread" replies to the message, "top-level" posts a new message. */
|
||||
replyStyle?: MSTeamsReplyStyle;
|
||||
/** Per-team config. Key is team ID (from the /team/ URL path segment). */
|
||||
teams?: Record<string, MSTeamsTeamConfig>;
|
||||
};
|
||||
|
||||
export type IMessageAccountConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
name?: string;
|
||||
@@ -631,6 +691,7 @@ export type QueueModeByProvider = {
|
||||
slack?: QueueMode;
|
||||
signal?: QueueMode;
|
||||
imessage?: QueueMode;
|
||||
msteams?: QueueMode;
|
||||
webchat?: QueueMode;
|
||||
};
|
||||
|
||||
@@ -875,13 +936,6 @@ export type GatewayTailscaleConfig = {
|
||||
export type GatewayRemoteConfig = {
|
||||
/** Remote Gateway WebSocket URL (ws:// or wss://). */
|
||||
url?: string;
|
||||
/**
|
||||
* Remote gateway over SSH, forwarding the gateway port to localhost.
|
||||
* Format: "user@host" or "user@host:port" (port defaults to 22).
|
||||
*/
|
||||
sshTarget?: string;
|
||||
/** Optional SSH identity file path. */
|
||||
sshIdentity?: string;
|
||||
/** Token for remote auth (when the gateway requires token auth). */
|
||||
token?: string;
|
||||
/** Password for remote auth (when the gateway requires password auth). */
|
||||
@@ -1126,7 +1180,7 @@ export type ClawdbotConfig = {
|
||||
every?: string;
|
||||
/** Heartbeat model override (provider/model). */
|
||||
model?: string;
|
||||
/** Delivery target (last|whatsapp|telegram|discord|signal|imessage|none). */
|
||||
/** Delivery target (last|whatsapp|telegram|discord|signal|imessage|msteams|none). */
|
||||
target?:
|
||||
| "last"
|
||||
| "whatsapp"
|
||||
@@ -1135,6 +1189,7 @@ export type ClawdbotConfig = {
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| "msteams"
|
||||
| "none";
|
||||
/** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). */
|
||||
to?: string;
|
||||
@@ -1225,6 +1280,7 @@ export type ClawdbotConfig = {
|
||||
slack?: SlackConfig;
|
||||
signal?: SignalConfig;
|
||||
imessage?: IMessageConfig;
|
||||
msteams?: MSTeamsConfig;
|
||||
cron?: CronConfig;
|
||||
hooks?: HooksConfig;
|
||||
bridge?: BridgeConfig;
|
||||
|
||||
@@ -109,6 +109,8 @@ const requireOpenAllowFrom = (params: {
|
||||
});
|
||||
};
|
||||
|
||||
const MSTeamsReplyStyleSchema = z.enum(["thread", "top-level"]);
|
||||
|
||||
const RetryConfigSchema = z
|
||||
.object({
|
||||
attempts: z.number().int().min(1).optional(),
|
||||
@@ -126,6 +128,7 @@ const QueueModeBySurfaceSchema = z
|
||||
slack: QueueModeSchema.optional(),
|
||||
signal: QueueModeSchema.optional(),
|
||||
imessage: QueueModeSchema.optional(),
|
||||
msteams: QueueModeSchema.optional(),
|
||||
webchat: QueueModeSchema.optional(),
|
||||
})
|
||||
.optional();
|
||||
@@ -455,6 +458,48 @@ const IMessageConfigSchema = IMessageAccountSchemaBase.extend({
|
||||
});
|
||||
});
|
||||
|
||||
const MSTeamsChannelSchema = z.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
||||
});
|
||||
|
||||
const MSTeamsTeamSchema = z.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
||||
channels: z.record(z.string(), MSTeamsChannelSchema.optional()).optional(),
|
||||
});
|
||||
|
||||
const MSTeamsConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
appId: z.string().optional(),
|
||||
appPassword: z.string().optional(),
|
||||
tenantId: z.string().optional(),
|
||||
webhook: z
|
||||
.object({
|
||||
port: z.number().int().positive().optional(),
|
||||
path: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
||||
allowFrom: z.array(z.string()).optional(),
|
||||
textChunkLimit: z.number().int().positive().optional(),
|
||||
mediaAllowHosts: z.array(z.string()).optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
||||
teams: z.record(z.string(), MSTeamsTeamSchema.optional()).optional(),
|
||||
})
|
||||
.superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
allowFrom: value.allowFrom,
|
||||
ctx,
|
||||
path: ["allowFrom"],
|
||||
message:
|
||||
'msteams.dmPolicy="open" requires msteams.allowFrom to include "*"',
|
||||
});
|
||||
});
|
||||
|
||||
const SessionSchema = z
|
||||
.object({
|
||||
scope: z.union([z.literal("per-sender"), z.literal("global")]).optional(),
|
||||
@@ -742,6 +787,7 @@ const HookMappingSchema = z
|
||||
z.literal("slack"),
|
||||
z.literal("signal"),
|
||||
z.literal("imessage"),
|
||||
z.literal("msteams"),
|
||||
])
|
||||
.optional(),
|
||||
to: z.string().optional(),
|
||||
@@ -1049,6 +1095,7 @@ export const ClawdbotSchema = z.object({
|
||||
slack: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
signal: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
imessage: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
msteams: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
webchat: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
})
|
||||
.optional(),
|
||||
@@ -1205,6 +1252,7 @@ export const ClawdbotSchema = z.object({
|
||||
slack: SlackConfigSchema.optional(),
|
||||
signal: SignalConfigSchema.optional(),
|
||||
imessage: IMessageConfigSchema.optional(),
|
||||
msteams: MSTeamsConfigSchema.optional(),
|
||||
bridge: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
@@ -1283,8 +1331,6 @@ export const ClawdbotSchema = z.object({
|
||||
remote: z
|
||||
.object({
|
||||
url: z.string().optional(),
|
||||
sshTarget: z.string().optional(),
|
||||
sshIdentity: z.string().optional(),
|
||||
token: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
})
|
||||
|
||||
@@ -160,7 +160,8 @@ function resolveDeliveryTarget(
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
| "imessage"
|
||||
| "msteams";
|
||||
to?: string;
|
||||
},
|
||||
) {
|
||||
|
||||
@@ -23,7 +23,8 @@ export type CronPayload =
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
| "imessage"
|
||||
| "msteams";
|
||||
to?: string;
|
||||
bestEffortDeliver?: boolean;
|
||||
};
|
||||
|
||||
@@ -25,7 +25,8 @@ export type HookMappingResolved = {
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
| "imessage"
|
||||
| "msteams";
|
||||
to?: string;
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
@@ -65,7 +66,8 @@ export type HookAction =
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
| "imessage"
|
||||
| "msteams";
|
||||
to?: string;
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
|
||||
@@ -39,7 +39,8 @@ type HookDispatchers = {
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
| "imessage"
|
||||
| "msteams";
|
||||
to?: string;
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
|
||||
@@ -88,6 +88,14 @@ export type IMessageRuntimeStatus = {
|
||||
dbPath?: string | null;
|
||||
};
|
||||
|
||||
export type MSTeamsRuntimeStatus = {
|
||||
running: boolean;
|
||||
lastStartAt?: number | null;
|
||||
lastStopAt?: number | null;
|
||||
lastError?: string | null;
|
||||
port?: number | null;
|
||||
};
|
||||
|
||||
export type ProviderRuntimeSnapshot = {
|
||||
whatsapp: WebProviderStatus;
|
||||
whatsappAccounts?: Record<string, WebProviderStatus>;
|
||||
@@ -101,6 +109,7 @@ export type ProviderRuntimeSnapshot = {
|
||||
signalAccounts?: Record<string, SignalRuntimeStatus>;
|
||||
imessage: IMessageRuntimeStatus;
|
||||
imessageAccounts?: Record<string, IMessageRuntimeStatus>;
|
||||
msteams: MSTeamsRuntimeStatus;
|
||||
};
|
||||
|
||||
type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
|
||||
@@ -113,12 +122,14 @@ type ProviderManagerOptions = {
|
||||
logSlack: SubsystemLogger;
|
||||
logSignal: SubsystemLogger;
|
||||
logIMessage: SubsystemLogger;
|
||||
logMSTeams: SubsystemLogger;
|
||||
whatsappRuntimeEnv: RuntimeEnv;
|
||||
telegramRuntimeEnv: RuntimeEnv;
|
||||
discordRuntimeEnv: RuntimeEnv;
|
||||
slackRuntimeEnv: RuntimeEnv;
|
||||
signalRuntimeEnv: RuntimeEnv;
|
||||
imessageRuntimeEnv: RuntimeEnv;
|
||||
msteamsRuntimeEnv: RuntimeEnv;
|
||||
};
|
||||
|
||||
export type ProviderManager = {
|
||||
@@ -136,6 +147,8 @@ export type ProviderManager = {
|
||||
stopSignalProvider: (accountId?: string) => Promise<void>;
|
||||
startIMessageProvider: (accountId?: string) => Promise<void>;
|
||||
stopIMessageProvider: (accountId?: string) => Promise<void>;
|
||||
startMSTeamsProvider: () => Promise<void>;
|
||||
stopMSTeamsProvider: () => Promise<void>;
|
||||
markWhatsAppLoggedOut: (cleared: boolean, accountId?: string) => void;
|
||||
};
|
||||
|
||||
@@ -150,12 +163,14 @@ export function createProviderManager(
|
||||
logSlack,
|
||||
logSignal,
|
||||
logIMessage,
|
||||
logMSTeams,
|
||||
whatsappRuntimeEnv,
|
||||
telegramRuntimeEnv,
|
||||
discordRuntimeEnv,
|
||||
slackRuntimeEnv,
|
||||
signalRuntimeEnv,
|
||||
imessageRuntimeEnv,
|
||||
msteamsRuntimeEnv,
|
||||
} = opts;
|
||||
|
||||
const whatsappAborts = new Map<string, AbortController>();
|
||||
@@ -164,7 +179,9 @@ export function createProviderManager(
|
||||
const slackAborts = new Map<string, AbortController>();
|
||||
const signalAborts = new Map<string, AbortController>();
|
||||
const imessageAborts = new Map<string, AbortController>();
|
||||
let msteamsAbort: AbortController | null = null;
|
||||
const whatsappTasks = new Map<string, Promise<unknown>>();
|
||||
let msteamsTask: Promise<unknown> | null = null;
|
||||
const telegramTasks = new Map<string, Promise<unknown>>();
|
||||
const discordTasks = new Map<string, Promise<unknown>>();
|
||||
const slackTasks = new Map<string, Promise<unknown>>();
|
||||
@@ -224,6 +241,13 @@ export function createProviderManager(
|
||||
cliPath: null,
|
||||
dbPath: null,
|
||||
});
|
||||
let msteamsRuntime: MSTeamsRuntimeStatus = {
|
||||
running: false,
|
||||
lastStartAt: null,
|
||||
lastStopAt: null,
|
||||
lastError: null,
|
||||
port: null,
|
||||
};
|
||||
|
||||
const updateWhatsAppStatus = (accountId: string, next: WebProviderStatus) => {
|
||||
whatsappRuntimes.set(accountId, next);
|
||||
@@ -1026,6 +1050,83 @@ export function createProviderManager(
|
||||
);
|
||||
};
|
||||
|
||||
const startMSTeamsProvider = async () => {
|
||||
if (msteamsTask) return;
|
||||
const cfg = loadConfig();
|
||||
if (!cfg.msteams) {
|
||||
msteamsRuntime = {
|
||||
...msteamsRuntime,
|
||||
running: false,
|
||||
lastError: "not configured",
|
||||
};
|
||||
if (shouldLogVerbose()) {
|
||||
logMSTeams.debug("msteams provider not configured (no msteams config)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (cfg.msteams?.enabled === false) {
|
||||
msteamsRuntime = {
|
||||
...msteamsRuntime,
|
||||
running: false,
|
||||
lastError: "disabled",
|
||||
};
|
||||
if (shouldLogVerbose()) {
|
||||
logMSTeams.debug("msteams provider disabled (msteams.enabled=false)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
const { monitorMSTeamsProvider } = await import("../msteams/index.js");
|
||||
const port = cfg.msteams?.webhook?.port ?? 3978;
|
||||
logMSTeams.info(`starting provider (port ${port})`);
|
||||
msteamsAbort = new AbortController();
|
||||
msteamsRuntime = {
|
||||
...msteamsRuntime,
|
||||
running: true,
|
||||
lastStartAt: Date.now(),
|
||||
lastError: null,
|
||||
port,
|
||||
};
|
||||
const task = monitorMSTeamsProvider({
|
||||
cfg,
|
||||
runtime: msteamsRuntimeEnv,
|
||||
abortSignal: msteamsAbort.signal,
|
||||
})
|
||||
.catch((err) => {
|
||||
msteamsRuntime = {
|
||||
...msteamsRuntime,
|
||||
lastError: formatError(err),
|
||||
};
|
||||
logMSTeams.error(`provider exited: ${formatError(err)}`);
|
||||
})
|
||||
.finally(() => {
|
||||
msteamsAbort = null;
|
||||
msteamsTask = null;
|
||||
msteamsRuntime = {
|
||||
...msteamsRuntime,
|
||||
running: false,
|
||||
lastStopAt: Date.now(),
|
||||
};
|
||||
});
|
||||
msteamsTask = task;
|
||||
};
|
||||
|
||||
const stopMSTeamsProvider = async () => {
|
||||
if (!msteamsAbort && !msteamsTask) return;
|
||||
msteamsAbort?.abort();
|
||||
try {
|
||||
await msteamsTask;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
msteamsAbort = null;
|
||||
msteamsTask = null;
|
||||
msteamsRuntime = {
|
||||
...msteamsRuntime,
|
||||
running: false,
|
||||
lastStopAt: Date.now(),
|
||||
};
|
||||
};
|
||||
|
||||
const startProviders = async () => {
|
||||
await startWhatsAppProvider();
|
||||
await startDiscordProvider();
|
||||
@@ -1033,6 +1134,7 @@ export function createProviderManager(
|
||||
await startTelegramProvider();
|
||||
await startSignalProvider();
|
||||
await startIMessageProvider();
|
||||
await startMSTeamsProvider();
|
||||
};
|
||||
|
||||
const markWhatsAppLoggedOut = (cleared: boolean, accountId?: string) => {
|
||||
@@ -1180,6 +1282,7 @@ export function createProviderManager(
|
||||
signalAccounts,
|
||||
imessage,
|
||||
imessageAccounts,
|
||||
msteams: { ...msteamsRuntime },
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1198,6 +1301,8 @@ export function createProviderManager(
|
||||
stopSignalProvider,
|
||||
startIMessageProvider,
|
||||
stopIMessageProvider,
|
||||
startMSTeamsProvider,
|
||||
stopMSTeamsProvider,
|
||||
markWhatsAppLoggedOut,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -183,6 +183,7 @@ const logDiscord = logProviders.child("discord");
|
||||
const logSlack = logProviders.child("slack");
|
||||
const logSignal = logProviders.child("signal");
|
||||
const logIMessage = logProviders.child("imessage");
|
||||
const logMSTeams = logProviders.child("msteams");
|
||||
const canvasRuntime = runtimeForLogger(logCanvas);
|
||||
const whatsappRuntimeEnv = runtimeForLogger(logWhatsApp);
|
||||
const telegramRuntimeEnv = runtimeForLogger(logTelegram);
|
||||
@@ -190,6 +191,7 @@ const discordRuntimeEnv = runtimeForLogger(logDiscord);
|
||||
const slackRuntimeEnv = runtimeForLogger(logSlack);
|
||||
const signalRuntimeEnv = runtimeForLogger(logSignal);
|
||||
const imessageRuntimeEnv = runtimeForLogger(logIMessage);
|
||||
const msteamsRuntimeEnv = runtimeForLogger(logMSTeams);
|
||||
|
||||
type GatewayModelChoice = ModelCatalogEntry;
|
||||
|
||||
@@ -501,7 +503,8 @@ export async function startGatewayServer(
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
| "imessage"
|
||||
| "msteams";
|
||||
to?: string;
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
@@ -756,12 +759,14 @@ export async function startGatewayServer(
|
||||
logSlack,
|
||||
logSignal,
|
||||
logIMessage,
|
||||
logMSTeams,
|
||||
whatsappRuntimeEnv,
|
||||
telegramRuntimeEnv,
|
||||
discordRuntimeEnv,
|
||||
slackRuntimeEnv,
|
||||
signalRuntimeEnv,
|
||||
imessageRuntimeEnv,
|
||||
msteamsRuntimeEnv,
|
||||
});
|
||||
const {
|
||||
getRuntimeSnapshot,
|
||||
@@ -772,12 +777,14 @@ export async function startGatewayServer(
|
||||
startSlackProvider,
|
||||
startSignalProvider,
|
||||
startIMessageProvider,
|
||||
startMSTeamsProvider,
|
||||
stopWhatsAppProvider,
|
||||
stopTelegramProvider,
|
||||
stopDiscordProvider,
|
||||
stopSlackProvider,
|
||||
stopSignalProvider,
|
||||
stopIMessageProvider,
|
||||
stopMSTeamsProvider,
|
||||
markWhatsAppLoggedOut,
|
||||
} = providerManager;
|
||||
|
||||
|
||||
4
src/msteams/index.ts
Normal file
4
src/msteams/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { monitorMSTeamsProvider } from "./monitor.js";
|
||||
export { probeMSTeams } from "./probe.js";
|
||||
export { sendMessageMSTeams } from "./send.js";
|
||||
export { type MSTeamsCredentials, resolveMSTeamsCredentials } from "./token.js";
|
||||
111
src/msteams/monitor.ts
Normal file
111
src/msteams/monitor.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { ClawdbotConfig } from "../config/types.js";
|
||||
import { getChildLogger } from "../logging.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { resolveMSTeamsCredentials } from "./token.js";
|
||||
|
||||
const log = getChildLogger({ name: "msteams:monitor" });
|
||||
|
||||
export type MonitorMSTeamsOpts = {
|
||||
cfg: ClawdbotConfig;
|
||||
runtime?: RuntimeEnv;
|
||||
abortSignal?: AbortSignal;
|
||||
};
|
||||
|
||||
export type MonitorMSTeamsResult = {
|
||||
app: unknown;
|
||||
shutdown: () => Promise<void>;
|
||||
};
|
||||
|
||||
export async function monitorMSTeamsProvider(
|
||||
opts: MonitorMSTeamsOpts,
|
||||
): Promise<MonitorMSTeamsResult> {
|
||||
const msteamsCfg = opts.cfg.msteams;
|
||||
if (!msteamsCfg?.enabled) {
|
||||
log.debug("msteams provider disabled");
|
||||
return { app: null, shutdown: async () => {} };
|
||||
}
|
||||
|
||||
const creds = resolveMSTeamsCredentials(msteamsCfg);
|
||||
if (!creds) {
|
||||
log.error("msteams credentials not configured");
|
||||
return { app: null, shutdown: async () => {} };
|
||||
}
|
||||
|
||||
const port = msteamsCfg.webhook?.port ?? 3978;
|
||||
const path = msteamsCfg.webhook?.path ?? "/msteams/messages";
|
||||
|
||||
log.info(`starting msteams provider on port ${port}${path}`);
|
||||
|
||||
// Dynamic import to avoid loading SDK when provider is disabled
|
||||
const agentsHosting = await import("@microsoft/agents-hosting");
|
||||
const { startServer } = await import("@microsoft/agents-hosting-express");
|
||||
|
||||
const { ActivityHandler } = agentsHosting;
|
||||
type TurnContext = InstanceType<typeof agentsHosting.TurnContext>;
|
||||
|
||||
// Create activity handler using fluent API
|
||||
const handler = new ActivityHandler()
|
||||
.onMessage(async (context: TurnContext, next: () => Promise<void>) => {
|
||||
const text = context.activity?.text?.trim() ?? "";
|
||||
const from = context.activity?.from;
|
||||
const conversation = context.activity?.conversation;
|
||||
|
||||
log.debug("received message", {
|
||||
text: text.slice(0, 100),
|
||||
from: from?.id,
|
||||
conversation: conversation?.id,
|
||||
});
|
||||
|
||||
// TODO: Implement full message handling
|
||||
// - Route to agent based on config
|
||||
// - Process commands
|
||||
// - Send reply via context.sendActivity()
|
||||
|
||||
// Echo for now as a test
|
||||
await context.sendActivity(`Received: ${text}`);
|
||||
await next();
|
||||
})
|
||||
.onMembersAdded(async (context: TurnContext, next: () => Promise<void>) => {
|
||||
const membersAdded = context.activity?.membersAdded ?? [];
|
||||
for (const member of membersAdded) {
|
||||
if (member.id !== context.activity?.recipient?.id) {
|
||||
log.debug("member added", { member: member.id });
|
||||
await context.sendActivity("Hello! I'm Clawdbot.");
|
||||
}
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
// Auth configuration using the new SDK format
|
||||
const authConfig = {
|
||||
clientId: creds.appId,
|
||||
clientSecret: creds.appPassword,
|
||||
tenantId: creds.tenantId,
|
||||
};
|
||||
|
||||
// Set env vars that startServer reads (it uses loadAuthConfigFromEnv internally)
|
||||
process.env.clientId = creds.appId;
|
||||
process.env.clientSecret = creds.appPassword;
|
||||
process.env.tenantId = creds.tenantId;
|
||||
process.env.PORT = String(port);
|
||||
|
||||
// Start the server
|
||||
const expressApp = startServer(handler, authConfig);
|
||||
|
||||
log.info(`msteams provider started on port ${port}`);
|
||||
|
||||
const shutdown = async () => {
|
||||
log.info("shutting down msteams provider");
|
||||
// Express app doesn't have a direct close method
|
||||
// The server is managed by startServer internally
|
||||
};
|
||||
|
||||
// Handle abort signal
|
||||
if (opts.abortSignal) {
|
||||
opts.abortSignal.addEventListener("abort", () => {
|
||||
void shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
return { app: expressApp, shutdown };
|
||||
}
|
||||
23
src/msteams/probe.ts
Normal file
23
src/msteams/probe.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { MSTeamsConfig } from "../config/types.js";
|
||||
import { resolveMSTeamsCredentials } from "./token.js";
|
||||
|
||||
export type ProbeMSTeamsResult = {
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
appId?: string;
|
||||
};
|
||||
|
||||
export async function probeMSTeams(
|
||||
cfg?: MSTeamsConfig,
|
||||
): Promise<ProbeMSTeamsResult> {
|
||||
const creds = resolveMSTeamsCredentials(cfg);
|
||||
if (!creds) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "missing credentials (appId, appPassword, tenantId)",
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Validate credentials by attempting to get a token
|
||||
return { ok: true, appId: creds.appId };
|
||||
}
|
||||
25
src/msteams/send.ts
Normal file
25
src/msteams/send.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { MSTeamsConfig } from "../config/types.js";
|
||||
import { getChildLogger } from "../logging.js";
|
||||
|
||||
const log = getChildLogger({ name: "msteams:send" });
|
||||
|
||||
export type SendMSTeamsMessageParams = {
|
||||
cfg: MSTeamsConfig;
|
||||
conversationId: string;
|
||||
text: string;
|
||||
serviceUrl: string;
|
||||
};
|
||||
|
||||
export type SendMSTeamsMessageResult = {
|
||||
ok: boolean;
|
||||
messageId?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export async function sendMessageMSTeams(
|
||||
_params: SendMSTeamsMessageParams,
|
||||
): Promise<SendMSTeamsMessageResult> {
|
||||
// TODO: Implement using CloudAdapter.continueConversationAsync
|
||||
log.warn("sendMessageMSTeams not yet implemented");
|
||||
return { ok: false, error: "not implemented" };
|
||||
}
|
||||
23
src/msteams/token.ts
Normal file
23
src/msteams/token.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { MSTeamsConfig } from "../config/types.js";
|
||||
|
||||
export type MSTeamsCredentials = {
|
||||
appId: string;
|
||||
appPassword: string;
|
||||
tenantId: string;
|
||||
};
|
||||
|
||||
export function resolveMSTeamsCredentials(
|
||||
cfg?: MSTeamsConfig,
|
||||
): MSTeamsCredentials | undefined {
|
||||
const appId = cfg?.appId?.trim() || process.env.MSTEAMS_APP_ID?.trim();
|
||||
const appPassword =
|
||||
cfg?.appPassword?.trim() || process.env.MSTEAMS_APP_PASSWORD?.trim();
|
||||
const tenantId =
|
||||
cfg?.tenantId?.trim() || process.env.MSTEAMS_TENANT_ID?.trim();
|
||||
|
||||
if (!appId || !appPassword || !tenantId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { appId, appPassword, tenantId };
|
||||
}
|
||||
Reference in New Issue
Block a user