@openclaw/msteams 2026.2.14 → 2026.2.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.2.17
4
+
5
+ ### Changes
6
+
7
+ - Version alignment with core OpenClaw release numbers.
8
+
9
+ ## 2026.2.16
10
+
11
+ ### Changes
12
+
13
+ - Version alignment with core OpenClaw release numbers.
14
+
15
+ ## 2026.2.15
16
+
17
+ ### Changes
18
+
19
+ - Version alignment with core OpenClaw release numbers.
20
+
3
21
  ## 2026.2.14
4
22
 
5
23
  ### Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/msteams",
3
- "version": "2026.2.14",
3
+ "version": "2026.2.17",
4
4
  "description": "OpenClaw Microsoft Teams channel plugin",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -1,8 +1,3 @@
1
- import type {
2
- MSTeamsAccessTokenProvider,
3
- MSTeamsAttachmentLike,
4
- MSTeamsInboundMedia,
5
- } from "./types.js";
6
1
  import { getMSTeamsRuntime } from "../runtime.js";
7
2
  import {
8
3
  extractInlineImageCandidates,
@@ -14,6 +9,11 @@ import {
14
9
  resolveAuthAllowedHosts,
15
10
  resolveAllowedHosts,
16
11
  } from "./shared.js";
12
+ import type {
13
+ MSTeamsAccessTokenProvider,
14
+ MSTeamsAttachmentLike,
15
+ MSTeamsInboundMedia,
16
+ } from "./types.js";
17
17
 
18
18
  type DownloadCandidate = {
19
19
  url: string;
@@ -1,9 +1,3 @@
1
- import type {
2
- MSTeamsAccessTokenProvider,
3
- MSTeamsAttachmentLike,
4
- MSTeamsGraphMediaResult,
5
- MSTeamsInboundMedia,
6
- } from "./types.js";
7
1
  import { getMSTeamsRuntime } from "../runtime.js";
8
2
  import { downloadMSTeamsAttachments } from "./download.js";
9
3
  import {
@@ -13,6 +7,12 @@ import {
13
7
  normalizeContentType,
14
8
  resolveAllowedHosts,
15
9
  } from "./shared.js";
10
+ import type {
11
+ MSTeamsAccessTokenProvider,
12
+ MSTeamsAttachmentLike,
13
+ MSTeamsGraphMediaResult,
14
+ MSTeamsInboundMedia,
15
+ } from "./types.js";
16
16
 
17
17
  type GraphHostedContent = {
18
18
  id?: string | null;
@@ -1,4 +1,3 @@
1
- import type { MSTeamsAttachmentLike, MSTeamsHtmlAttachmentSummary } from "./types.js";
2
1
  import {
3
2
  ATTACHMENT_TAG_RE,
4
3
  extractHtmlFromAttachment,
@@ -7,6 +6,7 @@ import {
7
6
  isLikelyImageAttachment,
8
7
  safeHostForUrl,
9
8
  } from "./shared.js";
9
+ import type { MSTeamsAttachmentLike, MSTeamsHtmlAttachmentSummary } from "./types.js";
10
10
 
11
11
  export function summarizeMSTeamsHtmlAttachments(
12
12
  attachments: MSTeamsAttachmentLike[] | undefined,
@@ -10,11 +10,12 @@ const saveMediaBufferMock = vi.fn(async () => ({
10
10
 
11
11
  const runtimeStub = {
12
12
  media: {
13
- detectMime: (...args: unknown[]) => detectMimeMock(...args),
13
+ detectMime: detectMimeMock as unknown as PluginRuntime["media"]["detectMime"],
14
14
  },
15
15
  channel: {
16
16
  media: {
17
- saveMediaBuffer: (...args: unknown[]) => saveMediaBufferMock(...args),
17
+ saveMediaBuffer:
18
+ saveMediaBufferMock as unknown as PluginRuntime["channel"]["media"]["saveMediaBuffer"],
18
19
  },
19
20
  },
20
21
  } as unknown as PluginRuntime;
@@ -1,8 +1,16 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
2
  import { describe, expect, it } from "vitest";
3
3
  import { msteamsPlugin } from "./channel.js";
4
4
 
5
5
  describe("msteams directory", () => {
6
+ const runtimeEnv: RuntimeEnv = {
7
+ log: () => {},
8
+ error: () => {},
9
+ exit: (code: number): never => {
10
+ throw new Error(`exit ${code}`);
11
+ },
12
+ };
13
+
6
14
  it("lists peers and groups from config", async () => {
7
15
  const cfg = {
8
16
  channels: {
@@ -26,7 +34,12 @@ describe("msteams directory", () => {
26
34
  expect(msteamsPlugin.directory?.listGroups).toBeTruthy();
27
35
 
28
36
  await expect(
29
- msteamsPlugin.directory!.listPeers({ cfg, query: undefined, limit: undefined }),
37
+ msteamsPlugin.directory!.listPeers!({
38
+ cfg,
39
+ query: undefined,
40
+ limit: undefined,
41
+ runtime: runtimeEnv,
42
+ }),
30
43
  ).resolves.toEqual(
31
44
  expect.arrayContaining([
32
45
  { kind: "user", id: "user:alice" },
@@ -37,7 +50,12 @@ describe("msteams directory", () => {
37
50
  );
38
51
 
39
52
  await expect(
40
- msteamsPlugin.directory!.listGroups({ cfg, query: undefined, limit: undefined }),
53
+ msteamsPlugin.directory!.listGroups!({
54
+ cfg,
55
+ query: undefined,
56
+ limit: undefined,
57
+ runtime: runtimeEnv,
58
+ }),
41
59
  ).resolves.toEqual(
42
60
  expect.arrayContaining([
43
61
  { kind: "group", id: "conversation:chan1" },
package/src/channel.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { ChannelMessageActionName, ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk";
2
2
  import {
3
+ buildBaseChannelStatusSummary,
3
4
  buildChannelConfigSchema,
5
+ createDefaultChannelRuntimeState,
4
6
  DEFAULT_ACCOUNT_ID,
5
7
  MSTeamsConfigSchema,
6
8
  PAIRING_APPROVED_MESSAGE,
@@ -415,20 +417,9 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
415
417
  },
416
418
  outbound: msteamsOutbound,
417
419
  status: {
418
- defaultRuntime: {
419
- accountId: DEFAULT_ACCOUNT_ID,
420
- running: false,
421
- lastStartAt: null,
422
- lastStopAt: null,
423
- lastError: null,
424
- port: null,
425
- },
420
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }),
426
421
  buildChannelSummary: ({ snapshot }) => ({
427
- configured: snapshot.configured ?? false,
428
- running: snapshot.running ?? false,
429
- lastStartAt: snapshot.lastStartAt ?? null,
430
- lastStopAt: snapshot.lastStopAt ?? null,
431
- lastError: snapshot.lastError ?? null,
422
+ ...buildBaseChannelStatusSummary(snapshot),
432
423
  port: snapshot.port ?? null,
433
424
  probe: snapshot.probe,
434
425
  lastProbeAt: snapshot.lastProbeAt ?? null,
@@ -1,28 +1,15 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
2
1
  import fs from "node:fs";
3
2
  import os from "node:os";
4
3
  import path from "node:path";
5
4
  import { beforeEach, describe, expect, it } from "vitest";
6
- import type { StoredConversationReference } from "./conversation-store.js";
7
5
  import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
6
+ import type { StoredConversationReference } from "./conversation-store.js";
8
7
  import { setMSTeamsRuntime } from "./runtime.js";
9
-
10
- const runtimeStub = {
11
- state: {
12
- resolveStateDir: (env: NodeJS.ProcessEnv = process.env, homedir?: () => string) => {
13
- const override = env.OPENCLAW_STATE_DIR?.trim() || env.OPENCLAW_STATE_DIR?.trim();
14
- if (override) {
15
- return override;
16
- }
17
- const resolvedHome = homedir ? homedir() : os.homedir();
18
- return path.join(resolvedHome, ".openclaw");
19
- },
20
- },
21
- } as unknown as PluginRuntime;
8
+ import { msteamsRuntimeStub } from "./test-runtime.js";
22
9
 
23
10
  describe("msteams conversation store (fs)", () => {
24
11
  beforeEach(() => {
25
- setMSTeamsRuntime(runtimeStub);
12
+ setMSTeamsRuntime(msteamsRuntimeStub);
26
13
  });
27
14
 
28
15
  it("filters and prunes expired entries (but keeps legacy ones)", async () => {
@@ -125,6 +125,7 @@ describe("msteams messenger", () => {
125
125
 
126
126
  const adapter: MSTeamsAdapter = {
127
127
  continueConversation: async () => {},
128
+ process: async () => {},
128
129
  };
129
130
 
130
131
  const ids = await sendMSTeamsMessages({
@@ -154,6 +155,7 @@ describe("msteams messenger", () => {
154
155
  },
155
156
  });
156
157
  },
158
+ process: async () => {},
157
159
  };
158
160
 
159
161
  const ids = await sendMSTeamsMessages({
@@ -191,6 +193,7 @@ describe("msteams messenger", () => {
191
193
 
192
194
  const adapter: MSTeamsAdapter = {
193
195
  continueConversation: async () => {},
196
+ process: async () => {},
194
197
  };
195
198
 
196
199
  const ids = await sendMSTeamsMessages({
@@ -250,6 +253,7 @@ describe("msteams messenger", () => {
250
253
 
251
254
  const adapter: MSTeamsAdapter = {
252
255
  continueConversation: async () => {},
256
+ process: async () => {},
253
257
  };
254
258
 
255
259
  const ids = await sendMSTeamsMessages({
@@ -277,6 +281,7 @@ describe("msteams messenger", () => {
277
281
 
278
282
  const adapter: MSTeamsAdapter = {
279
283
  continueConversation: async () => {},
284
+ process: async () => {},
280
285
  };
281
286
 
282
287
  await expect(
@@ -310,6 +315,7 @@ describe("msteams messenger", () => {
310
315
  },
311
316
  });
312
317
  },
318
+ process: async () => {},
313
319
  };
314
320
 
315
321
  const ids = await sendMSTeamsMessages({
@@ -1,4 +1,3 @@
1
- import type { MSTeamsTurnContext } from "../sdk-types.js";
2
1
  import {
3
2
  buildMSTeamsGraphMessageUrls,
4
3
  downloadMSTeamsAttachments,
@@ -8,6 +7,7 @@ import {
8
7
  type MSTeamsHtmlAttachmentSummary,
9
8
  type MSTeamsInboundMedia,
10
9
  } from "../attachments.js";
10
+ import type { MSTeamsTurnContext } from "../sdk-types.js";
11
11
 
12
12
  type MSTeamsLogger = {
13
13
  debug?: (message: string, meta?: Record<string, unknown>) => void;
@@ -9,15 +9,13 @@ import {
9
9
  formatAllowlistMatchMeta,
10
10
  type HistoryEntry,
11
11
  } from "openclaw/plugin-sdk";
12
- import type { StoredConversationReference } from "../conversation-store.js";
13
- import type { MSTeamsMessageHandlerDeps } from "../monitor-handler.js";
14
- import type { MSTeamsTurnContext } from "../sdk-types.js";
15
12
  import {
16
13
  buildMSTeamsAttachmentPlaceholder,
17
14
  buildMSTeamsMediaPayload,
18
15
  type MSTeamsAttachmentLike,
19
16
  summarizeMSTeamsHtmlAttachments,
20
17
  } from "../attachments.js";
18
+ import type { StoredConversationReference } from "../conversation-store.js";
21
19
  import { formatUnknownError } from "../errors.js";
22
20
  import {
23
21
  extractMSTeamsConversationMessageId,
@@ -26,6 +24,7 @@ import {
26
24
  stripMSTeamsMentionTags,
27
25
  wasMSTeamsBotMentioned,
28
26
  } from "../inbound.js";
27
+ import type { MSTeamsMessageHandlerDeps } from "../monitor-handler.js";
29
28
  import {
30
29
  isMSTeamsGroupAllowed,
31
30
  resolveMSTeamsAllowlistMatch,
@@ -35,6 +34,7 @@ import {
35
34
  import { extractMSTeamsPollVote } from "../polls.js";
36
35
  import { createMSTeamsReplyDispatcher } from "../reply-dispatcher.js";
37
36
  import { getMSTeamsRuntime } from "../runtime.js";
37
+ import type { MSTeamsTurnContext } from "../sdk-types.js";
38
38
  import { recordMSTeamsSentMessage, wasMSTeamsMessageSent } from "../sent-message-cache.js";
39
39
  import { resolveMSTeamsInboundMedia } from "./inbound-media.js";
40
40
 
@@ -1,12 +1,12 @@
1
1
  import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
2
  import type { MSTeamsConversationStore } from "./conversation-store.js";
3
+ import { buildFileInfoCard, parseFileConsentInvoke, uploadToConsentUrl } from "./file-consent.js";
3
4
  import type { MSTeamsAdapter } from "./messenger.js";
5
+ import { createMSTeamsMessageHandler } from "./monitor-handler/message-handler.js";
4
6
  import type { MSTeamsMonitorLogger } from "./monitor-types.js";
7
+ import { getPendingUpload, removePendingUpload } from "./pending-uploads.js";
5
8
  import type { MSTeamsPollStore } from "./polls.js";
6
9
  import type { MSTeamsTurnContext } from "./sdk-types.js";
7
- import { buildFileInfoCard, parseFileConsentInvoke, uploadToConsentUrl } from "./file-consent.js";
8
- import { createMSTeamsMessageHandler } from "./monitor-handler/message-handler.js";
9
- import { getPendingUpload, removePendingUpload } from "./pending-uploads.js";
10
10
 
11
11
  export type MSTeamsAccessTokenProvider = {
12
12
  getAccessToken: (scope: string) => Promise<string>;
package/src/monitor.ts CHANGED
@@ -6,10 +6,10 @@ import {
6
6
  type OpenClawConfig,
7
7
  type RuntimeEnv,
8
8
  } from "openclaw/plugin-sdk";
9
- import type { MSTeamsConversationStore } from "./conversation-store.js";
10
- import type { MSTeamsAdapter } from "./messenger.js";
11
9
  import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
10
+ import type { MSTeamsConversationStore } from "./conversation-store.js";
12
11
  import { formatUnknownError } from "./errors.js";
12
+ import type { MSTeamsAdapter } from "./messenger.js";
13
13
  import { registerMSTeamsHandlers, type MSTeamsActivityHandler } from "./monitor-handler.js";
14
14
  import { createMSTeamsPollStoreFs, type MSTeamsPollStore } from "./polls.js";
15
15
  import {
package/src/onboarding.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  addWildcardAllowFrom,
11
11
  DEFAULT_ACCOUNT_ID,
12
12
  formatDocsLink,
13
+ mergeAllowFromEntries,
13
14
  promptChannelAccessConfig,
14
15
  } from "openclaw/plugin-sdk";
15
16
  import {
@@ -63,6 +64,32 @@ function looksLikeGuid(value: string): boolean {
63
64
  return /^[0-9a-fA-F-]{16,}$/.test(value);
64
65
  }
65
66
 
67
+ async function promptMSTeamsCredentials(prompter: WizardPrompter): Promise<{
68
+ appId: string;
69
+ appPassword: string;
70
+ tenantId: string;
71
+ }> {
72
+ const appId = String(
73
+ await prompter.text({
74
+ message: "Enter MS Teams App ID",
75
+ validate: (value) => (value?.trim() ? undefined : "Required"),
76
+ }),
77
+ ).trim();
78
+ const appPassword = String(
79
+ await prompter.text({
80
+ message: "Enter MS Teams App Password",
81
+ validate: (value) => (value?.trim() ? undefined : "Required"),
82
+ }),
83
+ ).trim();
84
+ const tenantId = String(
85
+ await prompter.text({
86
+ message: "Enter MS Teams Tenant ID",
87
+ validate: (value) => (value?.trim() ? undefined : "Required"),
88
+ }),
89
+ ).trim();
90
+ return { appId, appPassword, tenantId };
91
+ }
92
+
66
93
  async function promptMSTeamsAllowFrom(params: {
67
94
  cfg: OpenClawConfig;
68
95
  prompter: WizardPrompter;
@@ -107,9 +134,7 @@ async function promptMSTeamsAllowFrom(params: {
107
134
  );
108
135
  continue;
109
136
  }
110
- const unique = [
111
- ...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids]),
112
- ];
137
+ const unique = mergeAllowFromEntries(existing, ids);
113
138
  return setMSTeamsAllowFrom(params.cfg, unique);
114
139
  }
115
140
 
@@ -123,7 +148,7 @@ async function promptMSTeamsAllowFrom(params: {
123
148
  }
124
149
 
125
150
  const ids = resolved.map((item) => item.id as string);
126
- const unique = [...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids])];
151
+ const unique = mergeAllowFromEntries(existing, ids);
127
152
  return setMSTeamsAllowFrom(params.cfg, unique);
128
153
  }
129
154
  }
@@ -251,24 +276,7 @@ export const msteamsOnboardingAdapter: ChannelOnboardingAdapter = {
251
276
  },
252
277
  };
253
278
  } else {
254
- appId = String(
255
- await prompter.text({
256
- message: "Enter MS Teams App ID",
257
- validate: (value) => (value?.trim() ? undefined : "Required"),
258
- }),
259
- ).trim();
260
- appPassword = String(
261
- await prompter.text({
262
- message: "Enter MS Teams App Password",
263
- validate: (value) => (value?.trim() ? undefined : "Required"),
264
- }),
265
- ).trim();
266
- tenantId = String(
267
- await prompter.text({
268
- message: "Enter MS Teams Tenant ID",
269
- validate: (value) => (value?.trim() ? undefined : "Required"),
270
- }),
271
- ).trim();
279
+ ({ appId, appPassword, tenantId } = await promptMSTeamsCredentials(prompter));
272
280
  }
273
281
  } else if (hasConfigCreds) {
274
282
  const keep = await prompter.confirm({
@@ -276,44 +284,10 @@ export const msteamsOnboardingAdapter: ChannelOnboardingAdapter = {
276
284
  initialValue: true,
277
285
  });
278
286
  if (!keep) {
279
- appId = String(
280
- await prompter.text({
281
- message: "Enter MS Teams App ID",
282
- validate: (value) => (value?.trim() ? undefined : "Required"),
283
- }),
284
- ).trim();
285
- appPassword = String(
286
- await prompter.text({
287
- message: "Enter MS Teams App Password",
288
- validate: (value) => (value?.trim() ? undefined : "Required"),
289
- }),
290
- ).trim();
291
- tenantId = String(
292
- await prompter.text({
293
- message: "Enter MS Teams Tenant ID",
294
- validate: (value) => (value?.trim() ? undefined : "Required"),
295
- }),
296
- ).trim();
287
+ ({ appId, appPassword, tenantId } = await promptMSTeamsCredentials(prompter));
297
288
  }
298
289
  } else {
299
- appId = String(
300
- await prompter.text({
301
- message: "Enter MS Teams App ID",
302
- validate: (value) => (value?.trim() ? undefined : "Required"),
303
- }),
304
- ).trim();
305
- appPassword = String(
306
- await prompter.text({
307
- message: "Enter MS Teams App Password",
308
- validate: (value) => (value?.trim() ? undefined : "Required"),
309
- }),
310
- ).trim();
311
- tenantId = String(
312
- await prompter.text({
313
- message: "Enter MS Teams Tenant ID",
314
- validate: (value) => (value?.trim() ? undefined : "Required"),
315
- }),
316
- ).trim();
290
+ ({ appId, appPassword, tenantId } = await promptMSTeamsCredentials(prompter));
317
291
  }
318
292
 
319
293
  if (appId && appPassword && tenantId) {
package/src/policy.ts CHANGED
@@ -11,6 +11,7 @@ import type {
11
11
  import {
12
12
  buildChannelKeyCandidates,
13
13
  normalizeChannelSlug,
14
+ resolveAllowlistMatchSimple,
14
15
  resolveToolsBySender,
15
16
  resolveChannelEntryMatchWithFallback,
16
17
  resolveNestedAllowlistDecision,
@@ -209,24 +210,7 @@ export function resolveMSTeamsAllowlistMatch(params: {
209
210
  senderId: string;
210
211
  senderName?: string | null;
211
212
  }): MSTeamsAllowlistMatch {
212
- const allowFrom = params.allowFrom
213
- .map((entry) => String(entry).trim().toLowerCase())
214
- .filter(Boolean);
215
- if (allowFrom.length === 0) {
216
- return { allowed: false };
217
- }
218
- if (allowFrom.includes("*")) {
219
- return { allowed: true, matchKey: "*", matchSource: "wildcard" };
220
- }
221
- const senderId = params.senderId.toLowerCase();
222
- if (allowFrom.includes(senderId)) {
223
- return { allowed: true, matchKey: senderId, matchSource: "id" };
224
- }
225
- const senderName = params.senderName?.toLowerCase();
226
- if (senderName && allowFrom.includes(senderName)) {
227
- return { allowed: true, matchKey: senderName, matchSource: "name" };
228
- }
229
- return { allowed: false };
213
+ return resolveAllowlistMatchSimple(params);
230
214
  }
231
215
 
232
216
  export function resolveMSTeamsReplyPolicy(params: {
package/src/polls.test.ts CHANGED
@@ -1,27 +1,14 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
2
1
  import fs from "node:fs";
3
2
  import os from "node:os";
4
3
  import path from "node:path";
5
4
  import { beforeEach, describe, expect, it } from "vitest";
6
5
  import { buildMSTeamsPollCard, createMSTeamsPollStoreFs, extractMSTeamsPollVote } from "./polls.js";
7
6
  import { setMSTeamsRuntime } from "./runtime.js";
8
-
9
- const runtimeStub = {
10
- state: {
11
- resolveStateDir: (env: NodeJS.ProcessEnv = process.env, homedir?: () => string) => {
12
- const override = env.OPENCLAW_STATE_DIR?.trim() || env.OPENCLAW_STATE_DIR?.trim();
13
- if (override) {
14
- return override;
15
- }
16
- const resolvedHome = homedir ? homedir() : os.homedir();
17
- return path.join(resolvedHome, ".openclaw");
18
- },
19
- },
20
- } as unknown as PluginRuntime;
7
+ import { msteamsRuntimeStub } from "./test-runtime.js";
21
8
 
22
9
  describe("msteams polls", () => {
23
10
  beforeEach(() => {
24
- setMSTeamsRuntime(runtimeStub);
11
+ setMSTeamsRuntime(msteamsRuntimeStub);
25
12
  });
26
13
 
27
14
  it("builds poll cards with fallback text", () => {
package/src/probe.ts CHANGED
@@ -1,11 +1,9 @@
1
- import type { MSTeamsConfig } from "openclaw/plugin-sdk";
1
+ import type { BaseProbeResult, MSTeamsConfig } from "openclaw/plugin-sdk";
2
2
  import { formatUnknownError } from "./errors.js";
3
3
  import { loadMSTeamsSdkWithAuth } from "./sdk.js";
4
4
  import { resolveMSTeamsCredentials } from "./token.js";
5
5
 
6
- export type ProbeMSTeamsResult = {
7
- ok: boolean;
8
- error?: string;
6
+ export type ProbeMSTeamsResult = BaseProbeResult<string> & {
9
7
  appId?: string;
10
8
  graph?: {
11
9
  ok: boolean;
@@ -9,8 +9,6 @@ import {
9
9
  } from "openclaw/plugin-sdk";
10
10
  import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
11
11
  import type { StoredConversationReference } from "./conversation-store.js";
12
- import type { MSTeamsMonitorLogger } from "./monitor-types.js";
13
- import type { MSTeamsTurnContext } from "./sdk-types.js";
14
12
  import {
15
13
  classifyMSTeamsSendError,
16
14
  formatMSTeamsSendErrorHint,
@@ -21,7 +19,9 @@ import {
21
19
  renderReplyPayloadsToMessages,
22
20
  sendMSTeamsMessages,
23
21
  } from "./messenger.js";
22
+ import type { MSTeamsMonitorLogger } from "./monitor-types.js";
24
23
  import { getMSTeamsRuntime } from "./runtime.js";
24
+ import type { MSTeamsTurnContext } from "./sdk-types.js";
25
25
 
26
26
  export function createMSTeamsReplyDispatcher(params: {
27
27
  cfg: OpenClawConfig;
@@ -4,12 +4,12 @@ import {
4
4
  type PluginRuntime,
5
5
  } from "openclaw/plugin-sdk";
6
6
  import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
7
+ import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
7
8
  import type {
8
9
  MSTeamsConversationStore,
9
10
  StoredConversationReference,
10
11
  } from "./conversation-store.js";
11
12
  import type { MSTeamsAdapter } from "./messenger.js";
12
- import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
13
13
  import { getMSTeamsRuntime } from "./runtime.js";
14
14
  import { createMSTeamsAdapter, loadMSTeamsSdkWithAuth } from "./sdk.js";
15
15
  import { resolveMSTeamsCredentials } from "./token.js";
package/src/send.ts CHANGED
@@ -374,6 +374,45 @@ async function sendTextWithMedia(
374
374
  };
375
375
  }
376
376
 
377
+ type ProactiveActivityParams = {
378
+ adapter: MSTeamsProactiveContext["adapter"];
379
+ appId: string;
380
+ ref: MSTeamsProactiveContext["ref"];
381
+ activity: Record<string, unknown>;
382
+ errorPrefix: string;
383
+ };
384
+
385
+ async function sendProactiveActivity({
386
+ adapter,
387
+ appId,
388
+ ref,
389
+ activity,
390
+ errorPrefix,
391
+ }: ProactiveActivityParams): Promise<string> {
392
+ const baseRef = buildConversationReference(ref);
393
+ const proactiveRef = {
394
+ ...baseRef,
395
+ activityId: undefined,
396
+ };
397
+
398
+ let messageId = "unknown";
399
+ try {
400
+ await adapter.continueConversation(appId, proactiveRef, async (ctx) => {
401
+ const response = await ctx.sendActivity(activity);
402
+ messageId = extractMessageId(response) ?? "unknown";
403
+ });
404
+ return messageId;
405
+ } catch (err) {
406
+ const classification = classifyMSTeamsSendError(err);
407
+ const hint = formatMSTeamsSendErrorHint(classification);
408
+ const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : "";
409
+ throw new Error(
410
+ `${errorPrefix} failed${status}: ${formatUnknownError(err)}${hint ? ` (${hint})` : ""}`,
411
+ { cause: err },
412
+ );
413
+ }
414
+ }
415
+
377
416
  /**
378
417
  * Send a poll (Adaptive Card) to a Teams conversation or user.
379
418
  */
@@ -409,27 +448,13 @@ export async function sendPollMSTeams(
409
448
  };
410
449
 
411
450
  // Send poll via proactive conversation (Adaptive Cards require direct activity send)
412
- const baseRef = buildConversationReference(ref);
413
- const proactiveRef = {
414
- ...baseRef,
415
- activityId: undefined,
416
- };
417
-
418
- let messageId = "unknown";
419
- try {
420
- await adapter.continueConversation(appId, proactiveRef, async (ctx) => {
421
- const response = await ctx.sendActivity(activity);
422
- messageId = extractMessageId(response) ?? "unknown";
423
- });
424
- } catch (err) {
425
- const classification = classifyMSTeamsSendError(err);
426
- const hint = formatMSTeamsSendErrorHint(classification);
427
- const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : "";
428
- throw new Error(
429
- `msteams poll send failed${status}: ${formatUnknownError(err)}${hint ? ` (${hint})` : ""}`,
430
- { cause: err },
431
- );
432
- }
451
+ const messageId = await sendProactiveActivity({
452
+ adapter,
453
+ appId,
454
+ ref,
455
+ activity,
456
+ errorPrefix: "msteams poll send",
457
+ });
433
458
 
434
459
  log.info("sent poll", { conversationId, pollId: pollCard.pollId, messageId });
435
460
 
@@ -469,27 +494,13 @@ export async function sendAdaptiveCardMSTeams(
469
494
  };
470
495
 
471
496
  // Send card via proactive conversation
472
- const baseRef = buildConversationReference(ref);
473
- const proactiveRef = {
474
- ...baseRef,
475
- activityId: undefined,
476
- };
477
-
478
- let messageId = "unknown";
479
- try {
480
- await adapter.continueConversation(appId, proactiveRef, async (ctx) => {
481
- const response = await ctx.sendActivity(activity);
482
- messageId = extractMessageId(response) ?? "unknown";
483
- });
484
- } catch (err) {
485
- const classification = classifyMSTeamsSendError(err);
486
- const hint = formatMSTeamsSendErrorHint(classification);
487
- const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : "";
488
- throw new Error(
489
- `msteams card send failed${status}: ${formatUnknownError(err)}${hint ? ` (${hint})` : ""}`,
490
- { cause: err },
491
- );
492
- }
497
+ const messageId = await sendProactiveActivity({
498
+ adapter,
499
+ appId,
500
+ ref,
501
+ activity,
502
+ errorPrefix: "msteams card send",
503
+ });
493
504
 
494
505
  log.info("sent adaptive card", { conversationId, messageId });
495
506
 
package/src/store-fs.ts CHANGED
@@ -1,7 +1,5 @@
1
- import crypto from "node:crypto";
2
1
  import fs from "node:fs";
3
- import path from "node:path";
4
- import { safeParseJson } from "openclaw/plugin-sdk";
2
+ import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk";
5
3
  import { withFileLock as withPathLock } from "./file-lock.js";
6
4
 
7
5
  const STORE_LOCK_OPTIONS = {
@@ -19,31 +17,11 @@ export async function readJsonFile<T>(
19
17
  filePath: string,
20
18
  fallback: T,
21
19
  ): Promise<{ value: T; exists: boolean }> {
22
- try {
23
- const raw = await fs.promises.readFile(filePath, "utf-8");
24
- const parsed = safeParseJson<T>(raw);
25
- if (parsed == null) {
26
- return { value: fallback, exists: true };
27
- }
28
- return { value: parsed, exists: true };
29
- } catch (err) {
30
- const code = (err as { code?: string }).code;
31
- if (code === "ENOENT") {
32
- return { value: fallback, exists: false };
33
- }
34
- return { value: fallback, exists: false };
35
- }
20
+ return await readJsonFileWithFallback(filePath, fallback);
36
21
  }
37
22
 
38
23
  export async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
39
- const dir = path.dirname(filePath);
40
- await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
41
- const tmp = path.join(dir, `${path.basename(filePath)}.${crypto.randomUUID()}.tmp`);
42
- await fs.promises.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, {
43
- encoding: "utf-8",
44
- });
45
- await fs.promises.chmod(tmp, 0o600);
46
- await fs.promises.rename(tmp, filePath);
24
+ await writeJsonFileAtomically(filePath, value);
47
25
  }
48
26
 
49
27
  async function ensureJsonFile(filePath: string, fallback: unknown) {
@@ -0,0 +1,16 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import type { PluginRuntime } from "openclaw/plugin-sdk";
4
+
5
+ export const msteamsRuntimeStub = {
6
+ state: {
7
+ resolveStateDir: (env: NodeJS.ProcessEnv = process.env, homedir?: () => string) => {
8
+ const override = env.OPENCLAW_STATE_DIR?.trim() || env.OPENCLAW_STATE_DIR?.trim();
9
+ if (override) {
10
+ return override;
11
+ }
12
+ const resolvedHome = homedir ? homedir() : os.homedir();
13
+ return path.join(resolvedHome, ".openclaw");
14
+ },
15
+ },
16
+ } as unknown as PluginRuntime;