refactor(msteams): extract sdk + storage helpers
This commit is contained in:
@@ -1,16 +1,15 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import lockfile from "proper-lockfile";
|
import lockfile from "proper-lockfile";
|
||||||
|
|
||||||
import { resolveStateDir } from "../config/paths.js";
|
|
||||||
import type {
|
import type {
|
||||||
MSTeamsConversationStore,
|
MSTeamsConversationStore,
|
||||||
MSTeamsConversationStoreEntry,
|
MSTeamsConversationStoreEntry,
|
||||||
StoredConversationReference,
|
StoredConversationReference,
|
||||||
} from "./conversation-store.js";
|
} from "./conversation-store.js";
|
||||||
|
import { resolveMSTeamsStorePath } from "./storage.js";
|
||||||
|
|
||||||
type ConversationStoreData = {
|
type ConversationStoreData = {
|
||||||
version: 1;
|
version: 1;
|
||||||
@@ -34,16 +33,6 @@ const STORE_LOCK_OPTIONS = {
|
|||||||
stale: 30_000,
|
stale: 30_000,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function resolveStorePath(
|
|
||||||
env: NodeJS.ProcessEnv = process.env,
|
|
||||||
homedir?: () => string,
|
|
||||||
): string {
|
|
||||||
const stateDir = homedir
|
|
||||||
? resolveStateDir(env, homedir)
|
|
||||||
: resolveStateDir(env);
|
|
||||||
return path.join(stateDir, STORE_FILENAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeParseJson<T>(raw: string): T | null {
|
function safeParseJson<T>(raw: string): T | null {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(raw) as T;
|
return JSON.parse(raw) as T;
|
||||||
@@ -167,11 +156,17 @@ export function createMSTeamsConversationStoreFs(params?: {
|
|||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
homedir?: () => string;
|
homedir?: () => string;
|
||||||
ttlMs?: number;
|
ttlMs?: number;
|
||||||
|
stateDir?: string;
|
||||||
|
storePath?: string;
|
||||||
}): MSTeamsConversationStore {
|
}): MSTeamsConversationStore {
|
||||||
const env = params?.env ?? process.env;
|
|
||||||
const homedir = params?.homedir ?? os.homedir;
|
|
||||||
const ttlMs = params?.ttlMs ?? CONVERSATION_TTL_MS;
|
const ttlMs = params?.ttlMs ?? CONVERSATION_TTL_MS;
|
||||||
const filePath = resolveStorePath(env, homedir);
|
const filePath = resolveMSTeamsStorePath({
|
||||||
|
filename: STORE_FILENAME,
|
||||||
|
env: params?.env,
|
||||||
|
homedir: params?.homedir,
|
||||||
|
stateDir: params?.stateDir,
|
||||||
|
storePath: params?.storePath,
|
||||||
|
});
|
||||||
|
|
||||||
const empty: ConversationStoreData = { version: 1, conversations: {} };
|
const empty: ConversationStoreData = { version: 1, conversations: {} };
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
||||||
import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js";
|
import { dispatchReplyFromConfig } from "../auto-reply/reply/dispatch-from-config.js";
|
||||||
import { createReplyDispatcherWithTyping } from "../auto-reply/reply/reply-dispatcher.js";
|
|
||||||
import type { ClawdbotConfig } from "../config/types.js";
|
import type { ClawdbotConfig } from "../config/types.js";
|
||||||
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
|
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
|
||||||
import { enqueueSystemEvent } from "../infra/system-events.js";
|
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||||
@@ -23,11 +22,7 @@ import type {
|
|||||||
MSTeamsConversationStore,
|
MSTeamsConversationStore,
|
||||||
StoredConversationReference,
|
StoredConversationReference,
|
||||||
} from "./conversation-store.js";
|
} from "./conversation-store.js";
|
||||||
import {
|
import { formatUnknownError } from "./errors.js";
|
||||||
classifyMSTeamsSendError,
|
|
||||||
formatMSTeamsSendErrorHint,
|
|
||||||
formatUnknownError,
|
|
||||||
} from "./errors.js";
|
|
||||||
import {
|
import {
|
||||||
extractMSTeamsConversationMessageId,
|
extractMSTeamsConversationMessageId,
|
||||||
normalizeMSTeamsConversationId,
|
normalizeMSTeamsConversationId,
|
||||||
@@ -35,24 +30,16 @@ import {
|
|||||||
stripMSTeamsMentionTags,
|
stripMSTeamsMentionTags,
|
||||||
wasMSTeamsBotMentioned,
|
wasMSTeamsBotMentioned,
|
||||||
} from "./inbound.js";
|
} from "./inbound.js";
|
||||||
import {
|
import type { MSTeamsAdapter } from "./messenger.js";
|
||||||
type MSTeamsAdapter,
|
import type { MSTeamsMonitorLogger } from "./monitor-types.js";
|
||||||
renderReplyPayloadsToMessages,
|
|
||||||
sendMSTeamsMessages,
|
|
||||||
} from "./messenger.js";
|
|
||||||
import {
|
import {
|
||||||
resolveMSTeamsReplyPolicy,
|
resolveMSTeamsReplyPolicy,
|
||||||
resolveMSTeamsRouteConfig,
|
resolveMSTeamsRouteConfig,
|
||||||
} from "./policy.js";
|
} from "./policy.js";
|
||||||
import { extractMSTeamsPollVote, type MSTeamsPollStore } from "./polls.js";
|
import { extractMSTeamsPollVote, type MSTeamsPollStore } from "./polls.js";
|
||||||
|
import { createMSTeamsReplyDispatcher } from "./reply-dispatcher.js";
|
||||||
import type { MSTeamsTurnContext } from "./sdk-types.js";
|
import type { MSTeamsTurnContext } from "./sdk-types.js";
|
||||||
|
|
||||||
export type MSTeamsMonitorLogger = {
|
|
||||||
debug: (message: string, meta?: Record<string, unknown>) => void;
|
|
||||||
info: (message: string, meta?: Record<string, unknown>) => void;
|
|
||||||
error: (message: string, meta?: Record<string, unknown>) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MSTeamsAccessTokenProvider = {
|
export type MSTeamsAccessTokenProvider = {
|
||||||
getAccessToken: (scope: string) => Promise<string>;
|
getAccessToken: (scope: string) => Promise<string>;
|
||||||
};
|
};
|
||||||
@@ -456,59 +443,18 @@ function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send typing indicator
|
|
||||||
const sendTypingIndicator = async () => {
|
|
||||||
try {
|
|
||||||
await context.sendActivities([{ type: "typing" }]);
|
|
||||||
} catch {
|
|
||||||
// Typing indicator is best-effort.
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create reply dispatcher
|
// Create reply dispatcher
|
||||||
const { dispatcher, replyOptions, markDispatchIdle } =
|
const { dispatcher, replyOptions, markDispatchIdle } =
|
||||||
createReplyDispatcherWithTyping({
|
createMSTeamsReplyDispatcher({
|
||||||
responsePrefix: cfg.messages?.responsePrefix,
|
cfg,
|
||||||
deliver: async (payload) => {
|
runtime,
|
||||||
const messages = renderReplyPayloadsToMessages([payload], {
|
log,
|
||||||
textChunkLimit: textLimit,
|
adapter,
|
||||||
chunkText: true,
|
appId,
|
||||||
mediaMode: "split",
|
conversationRef,
|
||||||
});
|
context,
|
||||||
await sendMSTeamsMessages({
|
replyStyle,
|
||||||
replyStyle,
|
textLimit,
|
||||||
adapter,
|
|
||||||
appId,
|
|
||||||
conversationRef,
|
|
||||||
context,
|
|
||||||
messages,
|
|
||||||
// Enable default retry/backoff for throttling/transient failures.
|
|
||||||
retry: {},
|
|
||||||
onRetry: (event) => {
|
|
||||||
log.debug("retrying send", {
|
|
||||||
replyStyle,
|
|
||||||
...event,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (err, info) => {
|
|
||||||
const errMsg = formatUnknownError(err);
|
|
||||||
const classification = classifyMSTeamsSendError(err);
|
|
||||||
const hint = formatMSTeamsSendErrorHint(classification);
|
|
||||||
runtime.error?.(
|
|
||||||
danger(
|
|
||||||
`msteams ${info.kind} reply failed: ${errMsg}${hint ? ` (${hint})` : ""}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
log.error("reply failed", {
|
|
||||||
kind: info.kind,
|
|
||||||
error: errMsg,
|
|
||||||
classification,
|
|
||||||
hint,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onReplyStart: sendTypingIndicator,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dispatch to agent
|
// Dispatch to agent
|
||||||
|
|||||||
5
src/msteams/monitor-types.ts
Normal file
5
src/msteams/monitor-types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type MSTeamsMonitorLogger = {
|
||||||
|
debug: (message: string, meta?: Record<string, unknown>) => void;
|
||||||
|
info: (message: string, meta?: Record<string, unknown>) => void;
|
||||||
|
error: (message: string, meta?: Record<string, unknown>) => void;
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import { formatUnknownError } from "./errors.js";
|
|||||||
import type { MSTeamsAdapter } from "./messenger.js";
|
import type { MSTeamsAdapter } from "./messenger.js";
|
||||||
import { registerMSTeamsHandlers } from "./monitor-handler.js";
|
import { registerMSTeamsHandlers } from "./monitor-handler.js";
|
||||||
import { createMSTeamsPollStoreFs, type MSTeamsPollStore } from "./polls.js";
|
import { createMSTeamsPollStoreFs, type MSTeamsPollStore } from "./polls.js";
|
||||||
|
import { createMSTeamsAdapter, loadMSTeamsSdkWithAuth } from "./sdk.js";
|
||||||
import { resolveMSTeamsCredentials } from "./token.js";
|
import { resolveMSTeamsCredentials } from "./token.js";
|
||||||
|
|
||||||
const log = getChildLogger({ name: "msteams" });
|
const log = getChildLogger({ name: "msteams" });
|
||||||
@@ -65,25 +66,14 @@ export async function monitorMSTeamsProvider(
|
|||||||
log.info(`starting provider (port ${port})`);
|
log.info(`starting provider (port ${port})`);
|
||||||
|
|
||||||
// Dynamic import to avoid loading SDK when provider is disabled
|
// Dynamic import to avoid loading SDK when provider is disabled
|
||||||
const agentsHosting = await import("@microsoft/agents-hosting");
|
|
||||||
const express = await import("express");
|
const express = await import("express");
|
||||||
|
|
||||||
const {
|
const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
|
||||||
ActivityHandler,
|
const { ActivityHandler, MsalTokenProvider, authorizeJWT } = sdk;
|
||||||
CloudAdapter,
|
|
||||||
MsalTokenProvider,
|
|
||||||
authorizeJWT,
|
|
||||||
getAuthConfigWithDefaults,
|
|
||||||
} = agentsHosting;
|
|
||||||
|
|
||||||
// Auth configuration - create early so adapter is available for deliverReplies
|
// Auth configuration - create early so adapter is available for deliverReplies
|
||||||
const authConfig = getAuthConfigWithDefaults({
|
|
||||||
clientId: creds.appId,
|
|
||||||
clientSecret: creds.appPassword,
|
|
||||||
tenantId: creds.tenantId,
|
|
||||||
});
|
|
||||||
const tokenProvider = new MsalTokenProvider(authConfig);
|
const tokenProvider = new MsalTokenProvider(authConfig);
|
||||||
const adapter = new CloudAdapter(authConfig);
|
const adapter = createMSTeamsAdapter(authConfig, sdk);
|
||||||
|
|
||||||
const handler = registerMSTeamsHandlers(new ActivityHandler(), {
|
const handler = registerMSTeamsHandlers(new ActivityHandler(), {
|
||||||
cfg,
|
cfg,
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
import { createMSTeamsPollStoreMemory } from "./polls-store-memory.js";
|
|
||||||
|
|
||||||
describe("msteams poll memory store", () => {
|
|
||||||
it("stores polls and records normalized votes", async () => {
|
|
||||||
const store = createMSTeamsPollStoreMemory();
|
|
||||||
await store.createPoll({
|
|
||||||
id: "poll-1",
|
|
||||||
question: "Lunch?",
|
|
||||||
options: ["Pizza", "Sushi"],
|
|
||||||
maxSelections: 1,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
votes: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const poll = await store.recordVote({
|
|
||||||
pollId: "poll-1",
|
|
||||||
voterId: "user-1",
|
|
||||||
selections: ["0", "1"],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(poll?.votes["user-1"]).toEqual(["0"]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
42
src/msteams/polls-store.test.ts
Normal file
42
src/msteams/polls-store.test.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { createMSTeamsPollStoreFs } from "./polls.js";
|
||||||
|
import { createMSTeamsPollStoreMemory } from "./polls-store-memory.js";
|
||||||
|
|
||||||
|
const createFsStore = async () => {
|
||||||
|
const stateDir = await fs.promises.mkdtemp(
|
||||||
|
path.join(os.tmpdir(), "clawdbot-msteams-polls-"),
|
||||||
|
);
|
||||||
|
return createMSTeamsPollStoreFs({ stateDir });
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMemoryStore = () => createMSTeamsPollStoreMemory();
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
{ name: "memory", createStore: createMemoryStore },
|
||||||
|
{ name: "fs", createStore: createFsStore },
|
||||||
|
])("$name poll store", ({ createStore }) => {
|
||||||
|
it("stores polls and records normalized votes", async () => {
|
||||||
|
const store = await createStore();
|
||||||
|
await store.createPoll({
|
||||||
|
id: "poll-1",
|
||||||
|
question: "Lunch?",
|
||||||
|
options: ["Pizza", "Sushi"],
|
||||||
|
maxSelections: 1,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
votes: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const poll = await store.recordVote({
|
||||||
|
pollId: "poll-1",
|
||||||
|
voterId: "user-1",
|
||||||
|
selections: ["0", "1"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(poll?.votes["user-1"]).toEqual(["0"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,7 +4,7 @@ import path from "node:path";
|
|||||||
|
|
||||||
import lockfile from "proper-lockfile";
|
import lockfile from "proper-lockfile";
|
||||||
|
|
||||||
import { resolveStateDir } from "../config/paths.js";
|
import { resolveMSTeamsStorePath } from "./storage.js";
|
||||||
|
|
||||||
export type MSTeamsPollVote = {
|
export type MSTeamsPollVote = {
|
||||||
pollId: string;
|
pollId: string;
|
||||||
@@ -239,19 +239,6 @@ export type MSTeamsPollStoreFsOptions = {
|
|||||||
storePath?: string;
|
storePath?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveStorePath(params?: MSTeamsPollStoreFsOptions): string {
|
|
||||||
if (params?.storePath) {
|
|
||||||
return params.storePath;
|
|
||||||
}
|
|
||||||
if (params?.stateDir) {
|
|
||||||
return path.join(params.stateDir, STORE_FILENAME);
|
|
||||||
}
|
|
||||||
const stateDir = params?.homedir
|
|
||||||
? resolveStateDir(params.env ?? process.env, params.homedir)
|
|
||||||
: resolveStateDir(params?.env ?? process.env);
|
|
||||||
return path.join(stateDir, STORE_FILENAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeParseJson<T>(raw: string): T | null {
|
function safeParseJson<T>(raw: string): T | null {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(raw) as T;
|
return JSON.parse(raw) as T;
|
||||||
@@ -364,7 +351,13 @@ export function normalizeMSTeamsPollSelections(
|
|||||||
export function createMSTeamsPollStoreFs(
|
export function createMSTeamsPollStoreFs(
|
||||||
params?: MSTeamsPollStoreFsOptions,
|
params?: MSTeamsPollStoreFsOptions,
|
||||||
): MSTeamsPollStore {
|
): MSTeamsPollStore {
|
||||||
const filePath = resolveStorePath(params);
|
const filePath = resolveMSTeamsStorePath({
|
||||||
|
filename: STORE_FILENAME,
|
||||||
|
env: params?.env,
|
||||||
|
homedir: params?.homedir,
|
||||||
|
stateDir: params?.stateDir,
|
||||||
|
storePath: params?.storePath,
|
||||||
|
});
|
||||||
const empty: PollStoreData = { version: 1, polls: {} };
|
const empty: PollStoreData = { version: 1, polls: {} };
|
||||||
|
|
||||||
const readStore = async (): Promise<PollStoreData> => {
|
const readStore = async (): Promise<PollStoreData> => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { MSTeamsConfig } from "../config/types.js";
|
import type { MSTeamsConfig } from "../config/types.js";
|
||||||
import { formatUnknownError } from "./errors.js";
|
import { formatUnknownError } from "./errors.js";
|
||||||
|
import { loadMSTeamsSdkWithAuth } from "./sdk.js";
|
||||||
import { resolveMSTeamsCredentials } from "./token.js";
|
import { resolveMSTeamsCredentials } from "./token.js";
|
||||||
|
|
||||||
export type ProbeMSTeamsResult = {
|
export type ProbeMSTeamsResult = {
|
||||||
@@ -20,16 +21,8 @@ export async function probeMSTeams(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { MsalTokenProvider, getAuthConfigWithDefaults } = await import(
|
const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
|
||||||
"@microsoft/agents-hosting"
|
const tokenProvider = new sdk.MsalTokenProvider(authConfig);
|
||||||
);
|
|
||||||
const authConfig = getAuthConfigWithDefaults({
|
|
||||||
clientId: creds.appId,
|
|
||||||
clientSecret: creds.appPassword,
|
|
||||||
tenantId: creds.tenantId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const tokenProvider = new MsalTokenProvider(authConfig);
|
|
||||||
await tokenProvider.getAccessToken("https://api.botframework.com/.default");
|
await tokenProvider.getAccessToken("https://api.botframework.com/.default");
|
||||||
return { ok: true, appId: creds.appId };
|
return { ok: true, appId: creds.appId };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
81
src/msteams/reply-dispatcher.ts
Normal file
81
src/msteams/reply-dispatcher.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { createReplyDispatcherWithTyping } from "../auto-reply/reply/reply-dispatcher.js";
|
||||||
|
import type { ClawdbotConfig, MSTeamsReplyStyle } from "../config/types.js";
|
||||||
|
import { danger } from "../globals.js";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import type { StoredConversationReference } from "./conversation-store.js";
|
||||||
|
import {
|
||||||
|
classifyMSTeamsSendError,
|
||||||
|
formatMSTeamsSendErrorHint,
|
||||||
|
formatUnknownError,
|
||||||
|
} from "./errors.js";
|
||||||
|
import {
|
||||||
|
type MSTeamsAdapter,
|
||||||
|
renderReplyPayloadsToMessages,
|
||||||
|
sendMSTeamsMessages,
|
||||||
|
} from "./messenger.js";
|
||||||
|
import type { MSTeamsMonitorLogger } from "./monitor-types.js";
|
||||||
|
import type { MSTeamsTurnContext } from "./sdk-types.js";
|
||||||
|
|
||||||
|
export function createMSTeamsReplyDispatcher(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
runtime: RuntimeEnv;
|
||||||
|
log: MSTeamsMonitorLogger;
|
||||||
|
adapter: MSTeamsAdapter;
|
||||||
|
appId: string;
|
||||||
|
conversationRef: StoredConversationReference;
|
||||||
|
context: MSTeamsTurnContext;
|
||||||
|
replyStyle: MSTeamsReplyStyle;
|
||||||
|
textLimit: number;
|
||||||
|
}) {
|
||||||
|
const sendTypingIndicator = async () => {
|
||||||
|
try {
|
||||||
|
await params.context.sendActivities([{ type: "typing" }]);
|
||||||
|
} catch {
|
||||||
|
// Typing indicator is best-effort.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return createReplyDispatcherWithTyping({
|
||||||
|
responsePrefix: params.cfg.messages?.responsePrefix,
|
||||||
|
deliver: async (payload) => {
|
||||||
|
const messages = renderReplyPayloadsToMessages([payload], {
|
||||||
|
textChunkLimit: params.textLimit,
|
||||||
|
chunkText: true,
|
||||||
|
mediaMode: "split",
|
||||||
|
});
|
||||||
|
await sendMSTeamsMessages({
|
||||||
|
replyStyle: params.replyStyle,
|
||||||
|
adapter: params.adapter,
|
||||||
|
appId: params.appId,
|
||||||
|
conversationRef: params.conversationRef,
|
||||||
|
context: params.context,
|
||||||
|
messages,
|
||||||
|
// Enable default retry/backoff for throttling/transient failures.
|
||||||
|
retry: {},
|
||||||
|
onRetry: (event) => {
|
||||||
|
params.log.debug("retrying send", {
|
||||||
|
replyStyle: params.replyStyle,
|
||||||
|
...event,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (err, info) => {
|
||||||
|
const errMsg = formatUnknownError(err);
|
||||||
|
const classification = classifyMSTeamsSendError(err);
|
||||||
|
const hint = formatMSTeamsSendErrorHint(classification);
|
||||||
|
params.runtime.error?.(
|
||||||
|
danger(
|
||||||
|
`msteams ${info.kind} reply failed: ${errMsg}${hint ? ` (${hint})` : ""}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
params.log.error("reply failed", {
|
||||||
|
kind: info.kind,
|
||||||
|
error: errMsg,
|
||||||
|
classification,
|
||||||
|
hint,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onReplyStart: sendTypingIndicator,
|
||||||
|
});
|
||||||
|
}
|
||||||
34
src/msteams/sdk.ts
Normal file
34
src/msteams/sdk.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { MSTeamsAdapter } from "./messenger.js";
|
||||||
|
import type { MSTeamsCredentials } from "./token.js";
|
||||||
|
|
||||||
|
export type MSTeamsSdk = Awaited<
|
||||||
|
ReturnType<typeof import("@microsoft/agents-hosting")>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export async function loadMSTeamsSdk(): Promise<MSTeamsSdk> {
|
||||||
|
return await import("@microsoft/agents-hosting");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMSTeamsAuthConfig(
|
||||||
|
creds: MSTeamsCredentials,
|
||||||
|
sdk: MSTeamsSdk,
|
||||||
|
) {
|
||||||
|
return sdk.getAuthConfigWithDefaults({
|
||||||
|
clientId: creds.appId,
|
||||||
|
clientSecret: creds.appPassword,
|
||||||
|
tenantId: creds.tenantId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMSTeamsAdapter(
|
||||||
|
authConfig: unknown,
|
||||||
|
sdk: MSTeamsSdk,
|
||||||
|
): MSTeamsAdapter {
|
||||||
|
return new sdk.CloudAdapter(authConfig) as unknown as MSTeamsAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadMSTeamsSdkWithAuth(creds: MSTeamsCredentials) {
|
||||||
|
const sdk = await loadMSTeamsSdk();
|
||||||
|
const authConfig = buildMSTeamsAuthConfig(creds, sdk);
|
||||||
|
return { sdk, authConfig };
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
sendMSTeamsMessages,
|
sendMSTeamsMessages,
|
||||||
} from "./messenger.js";
|
} from "./messenger.js";
|
||||||
import { buildMSTeamsPollCard } from "./polls.js";
|
import { buildMSTeamsPollCard } from "./polls.js";
|
||||||
|
import { createMSTeamsAdapter, loadMSTeamsSdkWithAuth } from "./sdk.js";
|
||||||
import { resolveMSTeamsCredentials } from "./token.js";
|
import { resolveMSTeamsCredentials } from "./token.js";
|
||||||
|
|
||||||
let _log: ReturnType<typeof getChildLoggerFn> | undefined;
|
let _log: ReturnType<typeof getChildLoggerFn> | undefined;
|
||||||
@@ -156,17 +157,8 @@ async function resolveMSTeamsSendContext(params: {
|
|||||||
const { conversationId, ref } = found;
|
const { conversationId, ref } = found;
|
||||||
const log = await getLog();
|
const log = await getLog();
|
||||||
|
|
||||||
// Dynamic import to avoid loading SDK when not needed
|
const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
|
||||||
const agentsHosting = await import("@microsoft/agents-hosting");
|
const adapter = createMSTeamsAdapter(authConfig, sdk);
|
||||||
const { CloudAdapter, getAuthConfigWithDefaults } = agentsHosting;
|
|
||||||
|
|
||||||
const authConfig = getAuthConfigWithDefaults({
|
|
||||||
clientId: creds.appId,
|
|
||||||
clientSecret: creds.appPassword,
|
|
||||||
tenantId: creds.tenantId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const adapter = new CloudAdapter(authConfig);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appId: creds.appId,
|
appId: creds.appId,
|
||||||
|
|||||||
24
src/msteams/storage.ts
Normal file
24
src/msteams/storage.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { resolveStateDir } from "../config/paths.js";
|
||||||
|
|
||||||
|
export type MSTeamsStorePathOptions = {
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
|
homedir?: () => string;
|
||||||
|
stateDir?: string;
|
||||||
|
storePath?: string;
|
||||||
|
filename: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveMSTeamsStorePath(
|
||||||
|
params: MSTeamsStorePathOptions,
|
||||||
|
): string {
|
||||||
|
if (params.storePath) return params.storePath;
|
||||||
|
if (params.stateDir) return path.join(params.stateDir, params.filename);
|
||||||
|
|
||||||
|
const env = params.env ?? process.env;
|
||||||
|
const stateDir = params.homedir
|
||||||
|
? resolveStateDir(env, params.homedir)
|
||||||
|
: resolveStateDir(env);
|
||||||
|
return path.join(stateDir, params.filename);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user