feat!: move msteams to plugin
This commit is contained in:
1 parent
dae34f3a61
commit
d9f9e93dee
73 files changed
+711
-243
No files matched your search
@@ -2,6 +2,7 @@
|
||||
|
||||
## 2026.1.15 (unreleased)
|
||||
|
||||
- **BREAKING:** Microsoft Teams is now a plugin; install `@clawdbot/msteams` via `clawdbot plugins install @clawdbot/msteams`.
|
||||
- CLI: set process titles to `clawdbot-<command>` for clearer process listings.
|
||||
- Heartbeat: tighten prompt guidance + suppress duplicate alerts for 24h. (#980) — thanks @voidserf.
|
||||
- Plugins: add provider auth registry + `clawdbot models auth login` for plugin-driven OAuth/API key flows.
|
||||
|
||||
@@ -17,7 +17,7 @@ Text is supported everywhere; media and reactions vary by channel.
|
||||
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
|
||||
- [Signal](/channels/signal) — signal-cli; privacy-focused.
|
||||
- [iMessage](/channels/imessage) — macOS only; native integration.
|
||||
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support.
|
||||
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately).
|
||||
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
|
||||
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
|
||||
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
|
||||
|
||||
+45
-17
@@ -3,20 +3,43 @@ summary: "Microsoft Teams bot support status, capabilities, and configuration"
|
||||
read_when:
|
||||
- Working on MS Teams channel features
|
||||
---
|
||||
# Microsoft Teams (Bot Framework)
|
||||
# Microsoft Teams (plugin)
|
||||
|
||||
> "Abandon all hope, ye who enter here."
|
||||
|
||||
|
||||
Updated: 2026-01-08
|
||||
Updated: 2026-01-16
|
||||
|
||||
Status: text + DM attachments are supported; channel/group attachments require Microsoft Graph permissions. Polls are sent via Adaptive Cards.
|
||||
|
||||
## Plugin required
|
||||
Microsoft Teams ships as a plugin and is not bundled with the core install.
|
||||
|
||||
**Breaking change (2026.1.15):** MS Teams moved out of core. If you use it, you must install the plugin.
|
||||
|
||||
Explainable: keeps core installs lighter and lets MS Teams dependencies update independently.
|
||||
|
||||
Install via CLI (npm registry):
|
||||
```bash
|
||||
clawdbot plugins install @clawdbot/msteams
|
||||
```
|
||||
|
||||
Local checkout (when running from a git repo):
|
||||
```bash
|
||||
clawdbot plugins install ./extensions/msteams
|
||||
```
|
||||
|
||||
If you choose Teams during configure/onboarding and a git checkout is detected,
|
||||
Clawdbot will offer the local install path automatically.
|
||||
|
||||
Details: [Plugins](/plugin)
|
||||
|
||||
## Quick setup (beginner)
|
||||
1) Create an **Azure Bot** (App ID + client secret + tenant ID).
|
||||
2) Configure Clawdbot with those credentials.
|
||||
3) Expose `/api/messages` (port 3978 by default) via a public URL or tunnel.
|
||||
4) Install the Teams app package and start the gateway.
|
||||
1) Install the Microsoft Teams plugin.
|
||||
2) Create an **Azure Bot** (App ID + client secret + tenant ID).
|
||||
3) Configure Clawdbot with those credentials.
|
||||
4) Expose `/api/messages` (port 3978 by default) via a public URL or tunnel.
|
||||
5) Install the Teams app package and start the gateway.
|
||||
|
||||
Minimal config:
|
||||
```json5
|
||||
@@ -73,11 +96,12 @@ Example:
|
||||
```
|
||||
|
||||
## How it works
|
||||
1. Create an **Azure Bot** (App ID + secret + tenant ID).
|
||||
2. Build a **Teams app package** that references the bot and includes the RSC permissions below.
|
||||
3. Upload/install the Teams app into a team (or personal scope for DMs).
|
||||
4. Configure `msteams` in `~/.clawdbot/clawdbot.json` (or env vars) and start the gateway.
|
||||
5. The gateway listens for Bot Framework webhook traffic on `/api/messages` by default.
|
||||
1. Install the Microsoft Teams plugin.
|
||||
2. Create an **Azure Bot** (App ID + secret + tenant ID).
|
||||
3. Build a **Teams app package** that references the bot and includes the RSC permissions below.
|
||||
4. Upload/install the Teams app into a team (or personal scope for DMs).
|
||||
5. Configure `msteams` in `~/.clawdbot/clawdbot.json` (or env vars) and start the gateway.
|
||||
6. The gateway listens for Bot Framework webhook traffic on `/api/messages` by default.
|
||||
|
||||
## Azure Bot Setup (Prerequisites)
|
||||
|
||||
@@ -166,13 +190,17 @@ This is often easier than hand-editing JSON manifests.
|
||||
3. Check gateway logs for incoming activity
|
||||
|
||||
## Setup (minimal text-only)
|
||||
1. **Bot registration**
|
||||
1. **Install the Microsoft Teams plugin**
|
||||
- From npm: `clawdbot plugins install @clawdbot/msteams`
|
||||
- From a local checkout: `clawdbot plugins install ./extensions/msteams`
|
||||
|
||||
2. **Bot registration**
|
||||
- Create an Azure Bot (see above) and note:
|
||||
- App ID
|
||||
- Client secret (App password)
|
||||
- Tenant ID (single-tenant)
|
||||
|
||||
2. **Teams app manifest**
|
||||
3. **Teams app manifest**
|
||||
- Include a `bot` entry with `botId = <App ID>`.
|
||||
- Scopes: `personal`, `team`, `groupChat`.
|
||||
- `supportsFiles: true` (required for personal scope file handling).
|
||||
@@ -180,7 +208,7 @@ This is often easier than hand-editing JSON manifests.
|
||||
- Create icons: `outline.png` (32x32) and `color.png` (192x192).
|
||||
- Zip all three files together: `manifest.json`, `outline.png`, `color.png`.
|
||||
|
||||
3. **Configure Clawdbot**
|
||||
4. **Configure Clawdbot**
|
||||
```json
|
||||
{
|
||||
"msteams": {
|
||||
@@ -198,12 +226,12 @@ This is often easier than hand-editing JSON manifests.
|
||||
- `MSTEAMS_APP_PASSWORD`
|
||||
- `MSTEAMS_TENANT_ID`
|
||||
|
||||
4. **Bot endpoint**
|
||||
5. **Bot endpoint**
|
||||
- Set the Azure Bot Messaging Endpoint to:
|
||||
- `https://<host>:3978/api/messages` (or your chosen path/port).
|
||||
|
||||
5. **Run the gateway**
|
||||
- The Teams channel starts automatically when `msteams` config exists and credentials are set.
|
||||
6. **Run the gateway**
|
||||
- The Teams channel starts automatically when the plugin is installed and `msteams` config exists with credentials.
|
||||
|
||||
## History context
|
||||
- `channels.msteams.historyLimit` controls how many recent channel/group messages are wrapped into the prompt.
|
||||
|
||||
@@ -35,9 +35,11 @@ See [Voice Call](/plugins/voice-call) for a concrete example plugin.
|
||||
|
||||
## Available plugins (official)
|
||||
|
||||
- Microsoft Teams is plugin-only as of 2026.1.15; install `@clawdbot/msteams` if you use Teams.
|
||||
- [Voice Call](/plugins/voice-call) — `@clawdbot/voice-call`
|
||||
- [Matrix](/channels/matrix) — `@clawdbot/matrix`
|
||||
- [Zalo](/channels/zalo) — `@clawdbot/zalo`
|
||||
- [Microsoft Teams](/channels/msteams) — `@clawdbot/msteams`
|
||||
|
||||
Clawdbot plugins are **TypeScript modules** loaded at runtime via jiti. They can
|
||||
register:
|
||||
|
||||
@@ -12,6 +12,7 @@ Use `pnpm` (Node 22+) from the repo root. Keep the working tree clean before tag
|
||||
|
||||
1) **Version & metadata**
|
||||
- [ ] Bump `package.json` version (e.g., `1.1.0`).
|
||||
- [ ] Run `pnpm plugins:sync` to align extension package versions + changelogs.
|
||||
- [ ] Update CLI/version strings: [`src/cli/program.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/cli/program.ts) and the Baileys user agent in [`src/provider-web.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/provider-web.ts).
|
||||
- [ ] Confirm package metadata (name, description, repository, keywords, license) and `bin` map points to [`dist/entry.js`](https://github.com/clawdbot/clawdbot/blob/main/dist/entry.js) for `clawdbot`.
|
||||
- [ ] If dependencies changed, run `pnpm install` so `pnpm-lock.yaml` is current.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## 2026.1.15
|
||||
|
||||
### Features
|
||||
- Microsoft Teams channel plugin (Bot Framework) with polls, media, threads, and gateway monitor.
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ClawdbotPluginApi } from "../../src/plugins/types.js";
|
||||
|
||||
import { msteamsPlugin } from "./src/channel.js";
|
||||
|
||||
const plugin = {
|
||||
id: "msteams",
|
||||
name: "Microsoft Teams",
|
||||
description: "Microsoft Teams channel plugin (Bot Framework)",
|
||||
register(api: ClawdbotPluginApi) {
|
||||
api.registerChannel({ plugin: msteamsPlugin });
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@clawdbot/msteams",
|
||||
"version": "2026.1.15",
|
||||
"type": "module",
|
||||
"description": "Clawdbot Microsoft Teams channel plugin",
|
||||
"clawdbot": {
|
||||
"extensions": ["./index.ts"]
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/agents-hosting": "^1.1.1",
|
||||
"@microsoft/agents-hosting-express": "^1.1.1",
|
||||
"@microsoft/agents-hosting-extensions-teams": "^1.1.1",
|
||||
"express": "^5.2.1",
|
||||
"proper-lockfile": "^4.1.2"
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,19 @@ const saveMediaBufferMock = vi.fn(async () => ({
|
||||
contentType: "image/png",
|
||||
}));
|
||||
|
||||
vi.mock("../media/mime.js", () => ({
|
||||
const modulePaths = vi.hoisted(() => {
|
||||
const downloadModuleUrl = new URL("./attachments/download.js", import.meta.url);
|
||||
return {
|
||||
mimeModulePath: new URL("../../../../src/media/mime.js", downloadModuleUrl).pathname,
|
||||
storeModulePath: new URL("../../../../src/media/store.js", downloadModuleUrl).pathname,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock(modulePaths.mimeModulePath, () => ({
|
||||
detectMime: (...args: unknown[]) => detectMimeMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../media/store.js", () => ({
|
||||
vi.mock(modulePaths.storeModulePath, () => ({
|
||||
saveMediaBuffer: (...args: unknown[]) => saveMediaBufferMock(...args),
|
||||
}));
|
||||
|
||||
File renamed without changes.
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { detectMime } from "../../media/mime.js";
|
||||
import { saveMediaBuffer } from "../../media/store.js";
|
||||
import { detectMime } from "../../../../src/media/mime.js";
|
||||
import { saveMediaBuffer } from "../../../../src/media/store.js";
|
||||
import {
|
||||
extractInlineImageCandidates,
|
||||
inferPlaceholder,
|
||||
@@ -1,5 +1,5 @@
|
||||
import { detectMime } from "../../media/mime.js";
|
||||
import { saveMediaBuffer } from "../../media/store.js";
|
||||
import { detectMime } from "../../../../src/media/mime.js";
|
||||
import { saveMediaBuffer } from "../../../../src/media/store.js";
|
||||
import { downloadMSTeamsImageAttachments } from "./download.js";
|
||||
import { GRAPH_ROOT, isRecord, normalizeContentType, resolveAllowedHosts } from "./shared.js";
|
||||
import type {
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,12 +1,12 @@
|
||||
import { chunkMarkdownText } from "../../auto-reply/chunk.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { createMSTeamsPollStoreFs } from "../../msteams/polls.js";
|
||||
import { sendMessageMSTeams, sendPollMSTeams } from "../../msteams/send.js";
|
||||
import { resolveMSTeamsCredentials } from "../../msteams/token.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||
import { msteamsOnboardingAdapter } from "./onboarding/msteams.js";
|
||||
import { PAIRING_APPROVED_MESSAGE } from "./pairing-message.js";
|
||||
import type { ChannelMessageActionName, ChannelPlugin } from "./types.js";
|
||||
import type { ClawdbotConfig } from "../../../src/config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import { PAIRING_APPROVED_MESSAGE } from "../../../src/channels/plugins/pairing-message.js";
|
||||
import type { ChannelMessageActionName, ChannelPlugin } from "../../../src/channels/plugins/types.js";
|
||||
|
||||
import { msteamsOnboardingAdapter } from "./onboarding.js";
|
||||
import { msteamsOutbound } from "./outbound.js";
|
||||
import { sendMessageMSTeams } from "./send.js";
|
||||
import { resolveMSTeamsCredentials } from "./token.js";
|
||||
|
||||
type ResolvedMSTeamsAccount = {
|
||||
accountId: string;
|
||||
@@ -17,10 +17,12 @@ type ResolvedMSTeamsAccount = {
|
||||
const meta = {
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
selectionLabel: "Microsoft Teams (Bot)",
|
||||
docsPath: "/msteams",
|
||||
selectionLabel: "Microsoft Teams (Bot Framework)",
|
||||
docsPath: "/channels/msteams",
|
||||
docsLabel: "msteams",
|
||||
blurb: "bot via Microsoft Teams.",
|
||||
blurb: "Bot Framework; enterprise support.",
|
||||
aliases: ["teams"],
|
||||
order: 60,
|
||||
} as const;
|
||||
|
||||
export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
@@ -120,58 +122,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
return ["poll"] satisfies ChannelMessageActionName[];
|
||||
},
|
||||
},
|
||||
outbound: {
|
||||
deliveryMode: "direct",
|
||||
chunker: chunkMarkdownText,
|
||||
textChunkLimit: 4000,
|
||||
pollMaxOptions: 12,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
ok: false,
|
||||
error: new Error(
|
||||
"Delivering to MS Teams requires --to <conversationId|user:ID|conversation:ID>",
|
||||
),
|
||||
};
|
||||
}
|
||||
return { ok: true, to: trimmed };
|
||||
},
|
||||
sendText: async ({ cfg, to, text, deps }) => {
|
||||
const send = deps?.sendMSTeams ?? ((to, text) => sendMessageMSTeams({ cfg, to, text }));
|
||||
const result = await send(to, text);
|
||||
return { channel: "msteams", ...result };
|
||||
},
|
||||
sendMedia: async ({ cfg, to, text, mediaUrl, deps }) => {
|
||||
const send =
|
||||
deps?.sendMSTeams ??
|
||||
((to, text, opts) => sendMessageMSTeams({ cfg, to, text, mediaUrl: opts?.mediaUrl }));
|
||||
const result = await send(to, text, { mediaUrl });
|
||||
return { channel: "msteams", ...result };
|
||||
},
|
||||
sendPoll: async ({ cfg, to, poll }) => {
|
||||
const maxSelections = poll.maxSelections ?? 1;
|
||||
const result = await sendPollMSTeams({
|
||||
cfg,
|
||||
to,
|
||||
question: poll.question,
|
||||
options: poll.options,
|
||||
maxSelections,
|
||||
});
|
||||
const pollStore = createMSTeamsPollStoreFs();
|
||||
await pollStore.createPoll({
|
||||
id: result.pollId,
|
||||
question: poll.question,
|
||||
options: poll.options,
|
||||
maxSelections,
|
||||
createdAt: new Date().toISOString(),
|
||||
conversationId: result.conversationId,
|
||||
messageId: result.messageId,
|
||||
votes: {},
|
||||
});
|
||||
return result;
|
||||
},
|
||||
},
|
||||
outbound: msteamsOutbound,
|
||||
status: {
|
||||
defaultRuntime: {
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
@@ -204,7 +155,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
||||
},
|
||||
gateway: {
|
||||
startAccount: async (ctx) => {
|
||||
const { monitorMSTeamsProvider } = await import("../../msteams/index.js");
|
||||
const { monitorMSTeamsProvider } = await import("./index.js");
|
||||
const port = ctx.cfg.channels?.msteams?.webhook?.port ?? 3978;
|
||||
ctx.setStatus({ accountId: ctx.accountId, port });
|
||||
ctx.log?.info(`starting provider (port ${port})`);
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
||||
import { SILENT_REPLY_TOKEN } from "../../../src/auto-reply/tokens.js";
|
||||
import type { StoredConversationReference } from "./conversation-store.js";
|
||||
import {
|
||||
type MSTeamsAdapter,
|
||||
@@ -1,7 +1,7 @@
|
||||
import { chunkMarkdownText } from "../auto-reply/chunk.js";
|
||||
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
import type { MSTeamsReplyStyle } from "../config/types.js";
|
||||
import { chunkMarkdownText } from "../../../src/auto-reply/chunk.js";
|
||||
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../../../src/auto-reply/tokens.js";
|
||||
import type { ReplyPayload } from "../../../src/auto-reply/types.js";
|
||||
import type { MSTeamsReplyStyle } from "../../../src/config/types.js";
|
||||
import type { StoredConversationReference } from "./conversation-store.js";
|
||||
import { classifyMSTeamsSendError } from "./errors.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ClawdbotConfig } from "../config/types.js";
|
||||
import { danger } from "../globals.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { ClawdbotConfig } from "../../../src/config/types.js";
|
||||
import { danger } from "../../../src/globals.js";
|
||||
import type { RuntimeEnv } from "../../../src/runtime.js";
|
||||
import type { MSTeamsConversationStore } from "./conversation-store.js";
|
||||
import type { MSTeamsAdapter } from "./messenger.js";
|
||||
import { createMSTeamsMessageHandler } from "./monitor-handler/message-handler.js";
|
||||
File renamed without changes.
+9
-9
@@ -1,23 +1,23 @@
|
||||
import { hasControlCommand } from "../../auto-reply/command-detection.js";
|
||||
import { formatAgentEnvelope } from "../../auto-reply/envelope.js";
|
||||
import { hasControlCommand } from "../../../../src/auto-reply/command-detection.js";
|
||||
import { formatAgentEnvelope } from "../../../../src/auto-reply/envelope.js";
|
||||
import {
|
||||
createInboundDebouncer,
|
||||
resolveInboundDebounceMs,
|
||||
} from "../../auto-reply/inbound-debounce.js";
|
||||
import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js";
|
||||
} from "../../../../src/auto-reply/inbound-debounce.js";
|
||||
import { dispatchReplyFromConfig } from "../../../../src/auto-reply/reply/dispatch-from-config.js";
|
||||
import {
|
||||
buildHistoryContextFromMap,
|
||||
clearHistoryEntries,
|
||||
DEFAULT_GROUP_HISTORY_LIMIT,
|
||||
type HistoryEntry,
|
||||
} from "../../auto-reply/reply/history.js";
|
||||
import { danger, logVerbose, shouldLogVerbose } from "../../globals.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
} from "../../../../src/auto-reply/reply/history.js";
|
||||
import { danger, logVerbose, shouldLogVerbose } from "../../../../src/globals.js";
|
||||
import { enqueueSystemEvent } from "../../../../src/infra/system-events.js";
|
||||
import {
|
||||
readChannelAllowFromStore,
|
||||
upsertChannelPairingRequest,
|
||||
} from "../../pairing/pairing-store.js";
|
||||
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
||||
} from "../../../../src/pairing/pairing-store.js";
|
||||
import { resolveAgentRoute } from "../../../../src/routing/resolve-route.js";
|
||||
|
||||
import {
|
||||
buildMSTeamsAttachmentPlaceholder,
|
||||
File renamed without changes.
@@ -1,8 +1,8 @@
|
||||
import type { Request, Response } from "express";
|
||||
import { resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||
import type { ClawdbotConfig } from "../config/types.js";
|
||||
import { getChildLogger } from "../logging.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { resolveTextChunkLimit } from "../../../src/auto-reply/chunk.js";
|
||||
import type { ClawdbotConfig } from "../../../src/config/types.js";
|
||||
import { getChildLogger } from "../../../src/logging.js";
|
||||
import type { RuntimeEnv } from "../../../src/runtime.js";
|
||||
import type { MSTeamsConversationStore } from "./conversation-store.js";
|
||||
import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
|
||||
import { formatUnknownError } from "./errors.js";
|
||||
@@ -1,11 +1,15 @@
|
||||
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||
import type { DmPolicy } from "../../../config/types.js";
|
||||
import { resolveMSTeamsCredentials } from "../../../msteams/token.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../wizard/prompts.js";
|
||||
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
|
||||
import { addWildcardAllowFrom } from "./helpers.js";
|
||||
import type { ClawdbotConfig } from "../../../src/config/config.js";
|
||||
import type { DmPolicy } from "../../../src/config/types.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
import type {
|
||||
ChannelOnboardingAdapter,
|
||||
ChannelOnboardingDmPolicy,
|
||||
} from "../../../src/channels/plugins/onboarding-types.js";
|
||||
import { addWildcardAllowFrom } from "../../../src/channels/plugins/onboarding/helpers.js";
|
||||
|
||||
import { resolveMSTeamsCredentials } from "./token.js";
|
||||
|
||||
const channel = "msteams" as const;
|
||||
|
||||
@@ -34,7 +38,7 @@ async function noteMSTeamsCredentialHelp(prompter: WizardPrompter): Promise<void
|
||||
"2) Add a client secret (App Password)",
|
||||
"3) Set webhook URL + messaging endpoint",
|
||||
"Tip: you can also set MSTEAMS_APP_ID / MSTEAMS_APP_PASSWORD / MSTEAMS_TENANT_ID.",
|
||||
`Docs: ${formatDocsLink("/msteams", "msteams")}`,
|
||||
`Docs: ${formatDocsLink("/channels/msteams", "msteams")}`,
|
||||
].join("\n"),
|
||||
"MS Teams credentials",
|
||||
);
|
||||
@@ -1,7 +1,8 @@
|
||||
import { chunkMarkdownText } from "../../../auto-reply/chunk.js";
|
||||
import { createMSTeamsPollStoreFs } from "../../../msteams/polls.js";
|
||||
import { sendMessageMSTeams, sendPollMSTeams } from "../../../msteams/send.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
import { chunkMarkdownText } from "../../../src/auto-reply/chunk.js";
|
||||
import type { ChannelOutboundAdapter } from "../../../src/channels/plugins/types.js";
|
||||
|
||||
import { createMSTeamsPollStoreFs } from "./polls.js";
|
||||
import { sendMessageMSTeams, sendPollMSTeams } from "./send.js";
|
||||
|
||||
export const msteamsOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { MSTeamsConfig } from "../config/types.js";
|
||||
import type { MSTeamsConfig } from "../../../src/config/types.js";
|
||||
import {
|
||||
isMSTeamsGroupAllowed,
|
||||
resolveMSTeamsReplyPolicy,
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
MSTeamsConfig,
|
||||
MSTeamsReplyStyle,
|
||||
MSTeamsTeamConfig,
|
||||
} from "../config/types.js";
|
||||
} from "../../../src/config/types.js";
|
||||
|
||||
export type MSTeamsResolvedRouteConfig = {
|
||||
teamConfig?: MSTeamsTeamConfig;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { MSTeamsConfig } from "../config/types.js";
|
||||
import type { MSTeamsConfig } from "../../../src/config/types.js";
|
||||
|
||||
const hostMockState = vi.hoisted(() => ({
|
||||
tokenError: null as Error | null,
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MSTeamsConfig } from "../config/types.js";
|
||||
import type { MSTeamsConfig } from "../../../src/config/types.js";
|
||||
import { formatUnknownError } from "./errors.js";
|
||||
import { loadMSTeamsSdkWithAuth } from "./sdk.js";
|
||||
import { resolveMSTeamsCredentials } from "./token.js";
|
||||
@@ -1,8 +1,8 @@
|
||||
import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../agents/identity.js";
|
||||
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 { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../../src/agents/identity.js";
|
||||
import { createReplyDispatcherWithTyping } from "../../../src/auto-reply/reply/reply-dispatcher.js";
|
||||
import type { ClawdbotConfig, MSTeamsReplyStyle } from "../../../src/config/types.js";
|
||||
import { danger } from "../../../src/globals.js";
|
||||
import type { RuntimeEnv } from "../../../src/runtime.js";
|
||||
import type { StoredConversationReference } from "./conversation-store.js";
|
||||
import {
|
||||
classifyMSTeamsSendError,
|
||||
File renamed without changes.
File renamed without changes.
@@ -1,5 +1,5 @@
|
||||
import type { ClawdbotConfig } from "../config/types.js";
|
||||
import type { getChildLogger as getChildLoggerFn } from "../logging.js";
|
||||
import type { ClawdbotConfig } from "../../../src/config/types.js";
|
||||
import type { getChildLogger as getChildLoggerFn } from "../../../src/logging.js";
|
||||
import type {
|
||||
MSTeamsConversationStore,
|
||||
StoredConversationReference,
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClawdbotConfig } from "../config/types.js";
|
||||
import type { ClawdbotConfig } from "../../../src/config/types.js";
|
||||
import type { StoredConversationReference } from "./conversation-store.js";
|
||||
import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
|
||||
import {
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { resolveStateDir } from "../../../src/config/paths.js";
|
||||
|
||||
export type MSTeamsStorePathOptions = {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
File renamed without changes.
@@ -1,4 +1,4 @@
|
||||
import type { MSTeamsConfig } from "../config/types.js";
|
||||
import type { MSTeamsConfig } from "../../../src/config/types.js";
|
||||
|
||||
export type MSTeamsCredentials = {
|
||||
appId: string;
|
||||
@@ -26,7 +26,6 @@
|
||||
"dist/infra/**",
|
||||
"dist/macos/**",
|
||||
"dist/media/**",
|
||||
"dist/msteams/**",
|
||||
"dist/process/**",
|
||||
"dist/plugins/**",
|
||||
"dist/security/**",
|
||||
@@ -144,9 +143,6 @@
|
||||
"@mariozechner/pi-ai": "0.46.0",
|
||||
"@mariozechner/pi-coding-agent": "^0.46.0",
|
||||
"@mariozechner/pi-tui": "^0.46.0",
|
||||
"@microsoft/agents-hosting": "^1.1.1",
|
||||
"@microsoft/agents-hosting-express": "^1.1.1",
|
||||
"@microsoft/agents-hosting-extensions-teams": "^1.1.1",
|
||||
"@sinclair/typebox": "0.34.47",
|
||||
"@slack/bolt": "^4.6.0",
|
||||
"@slack/web-api": "^7.13.0",
|
||||
|
||||
Generated
+18
-9
@@ -44,15 +44,6 @@ importers:
|
||||
'@mariozechner/pi-tui':
|
||||
specifier: ^0.46.0
|
||||
version: 0.46.0
|
||||
'@microsoft/agents-hosting':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
'@microsoft/agents-hosting-express':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
'@microsoft/agents-hosting-extensions-teams':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
'@sinclair/typebox':
|
||||
specifier: 0.34.47
|
||||
version: 0.34.47
|
||||
@@ -245,6 +236,24 @@ importers:
|
||||
specifier: 40.0.0
|
||||
version: 40.0.0
|
||||
|
||||
extensions/msteams:
|
||||
dependencies:
|
||||
'@microsoft/agents-hosting':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
'@microsoft/agents-hosting-express':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
'@microsoft/agents-hosting-extensions-teams':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
express:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1
|
||||
proper-lockfile:
|
||||
specifier: ^4.1.2
|
||||
version: 4.1.2
|
||||
|
||||
extensions/voice-call:
|
||||
dependencies:
|
||||
'@sinclair/typebox':
|
||||
|
||||
@@ -10,7 +10,6 @@ type PackResult = { files?: PackFile[] };
|
||||
const requiredPaths = [
|
||||
"dist/discord/send.js",
|
||||
"dist/hooks/gmail.js",
|
||||
"dist/msteams/send.js",
|
||||
"dist/whatsapp/normalize.js",
|
||||
];
|
||||
const forbiddenPrefixes = ["dist/Clawdbot.app/"];
|
||||
@@ -68,6 +67,7 @@ function checkPluginVersions() {
|
||||
for (const item of mismatches) {
|
||||
console.error(` - ${item}`);
|
||||
}
|
||||
console.error("release-check: run `pnpm plugins:sync` to align plugin versions.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ChannelOutboundAdapter, ChannelPlugin } from "../../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { PluginRegistry } from "../../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -22,9 +25,6 @@ vi.mock("../../discord/send.js", () => ({
|
||||
vi.mock("../../imessage/send.js", () => ({
|
||||
sendMessageIMessage: mocks.sendMessageIMessage,
|
||||
}));
|
||||
vi.mock("../../msteams/send.js", () => ({
|
||||
sendMessageMSTeams: mocks.sendMessageMSTeams,
|
||||
}));
|
||||
vi.mock("../../signal/send.js", () => ({
|
||||
sendMessageSignal: mocks.sendMessageSignal,
|
||||
}));
|
||||
@@ -41,6 +41,14 @@ vi.mock("../../web/outbound.js", () => ({
|
||||
const { routeReply } = await import("./route-reply.js");
|
||||
|
||||
describe("routeReply", () => {
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
it("skips sends when abort signal is already aborted", async () => {
|
||||
mocks.sendMessageSlack.mockClear();
|
||||
const controller = new AbortController();
|
||||
@@ -221,6 +229,17 @@ describe("routeReply", () => {
|
||||
|
||||
it("routes MS Teams via proactive sender", async () => {
|
||||
mocks.sendMessageMSTeams.mockClear();
|
||||
setActivePluginRegistry(
|
||||
createRegistry([
|
||||
{
|
||||
pluginId: "msteams",
|
||||
source: "test",
|
||||
plugin: createMSTeamsPlugin({
|
||||
outbound: createMSTeamsOutbound(),
|
||||
}),
|
||||
},
|
||||
]),
|
||||
);
|
||||
const cfg = {
|
||||
channels: {
|
||||
msteams: {
|
||||
@@ -243,3 +262,46 @@ describe("routeReply", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const createRegistry = (channels: PluginRegistry["channels"]): PluginRegistry => ({
|
||||
plugins: [],
|
||||
tools: [],
|
||||
channels,
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpHandlers: [],
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const emptyRegistry = createRegistry([]);
|
||||
|
||||
const createMSTeamsOutbound = (): ChannelOutboundAdapter => ({
|
||||
deliveryMode: "direct",
|
||||
sendText: async ({ cfg, to, text }) => {
|
||||
const result = await mocks.sendMessageMSTeams({ cfg, to, text });
|
||||
return { channel: "msteams", ...result };
|
||||
},
|
||||
sendMedia: async ({ cfg, to, text, mediaUrl }) => {
|
||||
const result = await mocks.sendMessageMSTeams({ cfg, to, text, mediaUrl });
|
||||
return { channel: "msteams", ...result };
|
||||
},
|
||||
});
|
||||
|
||||
const createMSTeamsPlugin = (params: { outbound: ChannelOutboundAdapter }): ChannelPlugin => ({
|
||||
id: "msteams",
|
||||
meta: {
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
selectionLabel: "Microsoft Teams (Bot Framework)",
|
||||
docsPath: "/channels/msteams",
|
||||
blurb: "Bot Framework; enterprise support.",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
outbound: params.outbound,
|
||||
});
|
||||
@@ -293,27 +293,6 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
||||
}),
|
||||
},
|
||||
},
|
||||
msteams: {
|
||||
id: "msteams",
|
||||
capabilities: {
|
||||
chatTypes: ["direct", "channel", "thread"],
|
||||
polls: true,
|
||||
threads: true,
|
||||
media: true,
|
||||
},
|
||||
outbound: { textChunkLimit: 4000 },
|
||||
config: {
|
||||
resolveAllowFrom: ({ cfg }) => cfg.channels?.msteams?.allowFrom ?? [],
|
||||
formatAllowFrom: ({ allowFrom }) => formatLower(allowFrom),
|
||||
},
|
||||
threading: {
|
||||
buildToolContext: ({ context, hasRepliedRef }) => ({
|
||||
currentChannelId: context.To?.trim() || undefined,
|
||||
currentThreadTs: context.ReplyToId,
|
||||
hasRepliedRef,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function buildDockFromPlugin(plugin: ChannelPlugin): ChannelDock {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getChannelPluginCatalogEntry, listChannelPluginCatalogEntries } from "./catalog.js";
|
||||
|
||||
describe("channel plugin catalog", () => {
|
||||
it("includes Microsoft Teams", () => {
|
||||
const entry = getChannelPluginCatalogEntry("msteams");
|
||||
expect(entry?.install.npmSpec).toBe("@clawdbot/msteams");
|
||||
expect(entry?.meta.aliases).toContain("teams");
|
||||
});
|
||||
|
||||
it("lists plugin catalog entries", () => {
|
||||
const ids = listChannelPluginCatalogEntries().map((entry) => entry.id);
|
||||
expect(ids).toContain("msteams");
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,24 @@ export type ChannelPluginCatalogEntry = {
|
||||
};
|
||||
|
||||
const CATALOG: ChannelPluginCatalogEntry[] = [
|
||||
{
|
||||
id: "msteams",
|
||||
meta: {
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
selectionLabel: "Microsoft Teams (Bot Framework)",
|
||||
docsPath: "/channels/msteams",
|
||||
docsLabel: "msteams",
|
||||
blurb: "Bot Framework; enterprise support.",
|
||||
aliases: ["teams"],
|
||||
order: 60,
|
||||
},
|
||||
install: {
|
||||
npmSpec: "@clawdbot/msteams",
|
||||
localPath: "extensions/msteams",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "matrix",
|
||||
meta: {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { CHAT_CHANNEL_ORDER, type ChatChannelId, normalizeChatChannelId } from "../registry.js";
|
||||
import { discordPlugin } from "./discord.js";
|
||||
import { imessagePlugin } from "./imessage.js";
|
||||
import { msteamsPlugin } from "./msteams.js";
|
||||
import { signalPlugin } from "./signal.js";
|
||||
import { slackPlugin } from "./slack.js";
|
||||
import { telegramPlugin } from "./telegram.js";
|
||||
@@ -27,7 +26,6 @@ function resolveCoreChannels(): ChannelPlugin[] {
|
||||
slackPlugin,
|
||||
signalPlugin,
|
||||
imessagePlugin,
|
||||
msteamsPlugin,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -85,7 +83,6 @@ export function normalizeChannelId(raw?: string | null): ChannelId | null {
|
||||
export {
|
||||
discordPlugin,
|
||||
imessagePlugin,
|
||||
msteamsPlugin,
|
||||
signalPlugin,
|
||||
slackPlugin,
|
||||
telegramPlugin,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import type { ChannelOutboundAdapter, ChannelPlugin } from "./types.js";
|
||||
import type { PluginRegistry } from "../../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { loadChannelPlugin } from "./load.js";
|
||||
import { loadChannelOutboundAdapter } from "./outbound/load.js";
|
||||
|
||||
const createRegistry = (channels: PluginRegistry["channels"]): PluginRegistry => ({
|
||||
plugins: [],
|
||||
tools: [],
|
||||
channels,
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpHandlers: [],
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const emptyRegistry = createRegistry([]);
|
||||
|
||||
const msteamsOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
sendText: async () => ({ channel: "msteams", messageId: "m1" }),
|
||||
sendMedia: async () => ({ channel: "msteams", messageId: "m2" }),
|
||||
};
|
||||
|
||||
const msteamsPlugin: ChannelPlugin = {
|
||||
id: "msteams",
|
||||
meta: {
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
selectionLabel: "Microsoft Teams (Bot Framework)",
|
||||
docsPath: "/channels/msteams",
|
||||
blurb: "Bot Framework; enterprise support.",
|
||||
aliases: ["teams"],
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
outbound: msteamsOutbound,
|
||||
};
|
||||
|
||||
const registryWithMSTeams = createRegistry([
|
||||
{ pluginId: "msteams", plugin: msteamsPlugin, source: "test" },
|
||||
]);
|
||||
|
||||
describe("channel plugin loader", () => {
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
it("loads channel plugins from the active registry", async () => {
|
||||
setActivePluginRegistry(registryWithMSTeams);
|
||||
const plugin = await loadChannelPlugin("msteams");
|
||||
expect(plugin).toBe(msteamsPlugin);
|
||||
});
|
||||
|
||||
it("loads outbound adapters from registered plugins", async () => {
|
||||
setActivePluginRegistry(registryWithMSTeams);
|
||||
const outbound = await loadChannelOutboundAdapter("msteams");
|
||||
expect(outbound).toBe(msteamsOutbound);
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,6 @@ const LOADERS: Record<ChatChannelId, PluginLoader> = {
|
||||
slack: async () => (await import("./slack.js")).slackPlugin,
|
||||
signal: async () => (await import("./signal.js")).signalPlugin,
|
||||
imessage: async () => (await import("./imessage.js")).imessagePlugin,
|
||||
msteams: async () => (await import("./msteams.js")).msteamsPlugin,
|
||||
};
|
||||
|
||||
const cache = new Map<ChannelId, ChannelPlugin>();
|
||||
|
||||
@@ -16,7 +16,6 @@ const LOADERS: Record<ChatChannelId, OutboundLoader> = {
|
||||
slack: async () => (await import("./slack.js")).slackOutbound,
|
||||
signal: async () => (await import("./signal.js")).signalOutbound,
|
||||
imessage: async () => (await import("./imessage.js")).imessageOutbound,
|
||||
msteams: async () => (await import("./msteams.js")).msteamsOutbound,
|
||||
};
|
||||
|
||||
const cache = new Map<ChannelId, ChannelOutboundAdapter>();
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
describe("channel registry", () => {
|
||||
it("normalizes aliases", () => {
|
||||
expect(normalizeChatChannelId("imsg")).toBe("imessage");
|
||||
expect(normalizeChatChannelId("teams")).toBe("msteams");
|
||||
expect(normalizeChatChannelId("web")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -18,6 +17,11 @@ describe("channel registry", () => {
|
||||
expect(channels[0]?.id).toBe("telegram");
|
||||
});
|
||||
|
||||
it("does not include MS Teams by default", () => {
|
||||
const channels = listChatChannels();
|
||||
expect(channels.some((channel) => channel.id === "msteams")).toBe(false);
|
||||
});
|
||||
|
||||
it("formats selection lines with docs labels", () => {
|
||||
const channels = listChatChannels();
|
||||
const first = channels[0];
|
||||
|
||||
@@ -9,7 +9,6 @@ export const CHAT_CHANNEL_ORDER = [
|
||||
"slack",
|
||||
"signal",
|
||||
"imessage",
|
||||
"msteams",
|
||||
] as const;
|
||||
|
||||
export type ChatChannelId = (typeof CHAT_CHANNEL_ORDER)[number];
|
||||
@@ -74,19 +73,10 @@ const CHAT_CHANNEL_META: Record<ChatChannelId, ChannelMeta> = {
|
||||
docsLabel: "imessage",
|
||||
blurb: "this is still a work in progress.",
|
||||
},
|
||||
msteams: {
|
||||
id: "msteams",
|
||||
label: "MS Teams",
|
||||
selectionLabel: "Microsoft Teams (Bot Framework)",
|
||||
docsPath: "/channels/msteams",
|
||||
docsLabel: "msteams",
|
||||
blurb: "supported (Bot Framework).",
|
||||
},
|
||||
};
|
||||
|
||||
export const CHAT_CHANNEL_ALIASES: Record<string, ChatChannelId> = {
|
||||
imsg: "imessage",
|
||||
teams: "msteams",
|
||||
};
|
||||
|
||||
const normalizeChannelKey = (raw?: string | null): string | undefined => {
|
||||
|
||||
+1
-15
@@ -1,9 +1,7 @@
|
||||
import { logWebSelfId, sendMessageWhatsApp } from "../channels/web/index.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { sendMessageDiscord } from "../discord/send.js";
|
||||
import { sendMessageIMessage } from "../imessage/send.js";
|
||||
import type { OutboundSendDeps } from "../infra/outbound/deliver.js";
|
||||
import { sendMessageMSTeams } from "../msteams/send.js";
|
||||
import { sendMessageSignal } from "../signal/send.js";
|
||||
import { sendMessageSlack } from "../slack/send.js";
|
||||
import { sendMessageTelegram } from "../telegram/send.js";
|
||||
@@ -15,7 +13,6 @@ export type CliDeps = {
|
||||
sendMessageSlack: typeof sendMessageSlack;
|
||||
sendMessageSignal: typeof sendMessageSignal;
|
||||
sendMessageIMessage: typeof sendMessageIMessage;
|
||||
sendMessageMSTeams: typeof sendMessageMSTeams;
|
||||
};
|
||||
|
||||
export function createDefaultDeps(): CliDeps {
|
||||
@@ -26,12 +23,11 @@ export function createDefaultDeps(): CliDeps {
|
||||
sendMessageSlack,
|
||||
sendMessageSignal,
|
||||
sendMessageIMessage,
|
||||
sendMessageMSTeams,
|
||||
};
|
||||
}
|
||||
|
||||
// Provider docking: extend this mapping when adding new outbound send deps.
|
||||
export function createOutboundSendDeps(deps: CliDeps, cfg: ClawdbotConfig): OutboundSendDeps {
|
||||
export function createOutboundSendDeps(deps: CliDeps): OutboundSendDeps {
|
||||
return {
|
||||
sendWhatsApp: deps.sendMessageWhatsApp,
|
||||
sendTelegram: deps.sendMessageTelegram,
|
||||
@@ -39,16 +35,6 @@ export function createOutboundSendDeps(deps: CliDeps, cfg: ClawdbotConfig): Outb
|
||||
sendSlack: deps.sendMessageSlack,
|
||||
sendSignal: deps.sendMessageSignal,
|
||||
sendIMessage: deps.sendMessageIMessage,
|
||||
// Provider docking: MS Teams send requires full cfg (credentials), wrap to match OutboundSendDeps.
|
||||
sendMSTeams: deps.sendMessageMSTeams
|
||||
? async (to, text, opts) =>
|
||||
await deps.sendMessageMSTeams({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
mediaUrl: opts?.mediaUrl,
|
||||
})
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ export async function deliverAgentCommandResult(params: {
|
||||
bestEffort: bestEffortDeliver,
|
||||
onError: (err) => logDeliveryError(err),
|
||||
onPayload: logPayload,
|
||||
deps: createOutboundSendDeps(deps, cfg),
|
||||
deps: createOutboundSendDeps(deps),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+2
-11
@@ -2,7 +2,7 @@ import {
|
||||
CHANNEL_MESSAGE_ACTION_NAMES,
|
||||
type ChannelMessageActionName,
|
||||
} from "../channels/plugins/types.js";
|
||||
import type { CliDeps } from "../cli/deps.js";
|
||||
import { createOutboundSendDeps, type CliDeps } from "../cli/deps.js";
|
||||
import { withProgress } from "../cli/progress.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import type { OutboundSendDeps } from "../infra/outbound/deliver.js";
|
||||
@@ -23,16 +23,7 @@ export async function messageCommand(
|
||||
throw new Error(`Unknown message action: ${action}`);
|
||||
}
|
||||
|
||||
const outboundDeps: OutboundSendDeps = {
|
||||
sendWhatsApp: deps.sendMessageWhatsApp,
|
||||
sendTelegram: deps.sendMessageTelegram,
|
||||
sendDiscord: deps.sendMessageDiscord,
|
||||
sendSlack: deps.sendMessageSlack,
|
||||
sendSignal: deps.sendMessageSignal,
|
||||
sendIMessage: deps.sendMessageIMessage,
|
||||
sendMSTeams: (to, text, opts) =>
|
||||
deps.sendMessageMSTeams({ cfg, to, text, mediaUrl: opts?.mediaUrl }),
|
||||
};
|
||||
const outboundDeps: OutboundSendDeps = createOutboundSendDeps(deps);
|
||||
|
||||
const run = async () =>
|
||||
await runMessageAction({
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { resolveChannelCapabilities } from "./channel-capabilities.js";
|
||||
import type { ClawdbotConfig } from "./config.js";
|
||||
|
||||
describe("resolveChannelCapabilities", () => {
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
it("returns undefined for missing inputs", () => {
|
||||
expect(resolveChannelCapabilities({})).toBeUndefined();
|
||||
expect(resolveChannelCapabilities({ cfg: {} as ClawdbotConfig })).toBeUndefined();
|
||||
@@ -74,6 +85,15 @@ describe("resolveChannelCapabilities", () => {
|
||||
});
|
||||
|
||||
it("supports msteams capabilities", () => {
|
||||
setActivePluginRegistry(
|
||||
createRegistry([
|
||||
{
|
||||
pluginId: "msteams",
|
||||
source: "test",
|
||||
plugin: createMSTeamsPlugin(),
|
||||
},
|
||||
]),
|
||||
);
|
||||
const cfg = {
|
||||
channels: { msteams: { capabilities: [" polls ", ""] } },
|
||||
} satisfies Partial<ClawdbotConfig>;
|
||||
@@ -86,3 +106,33 @@ describe("resolveChannelCapabilities", () => {
|
||||
).toEqual(["polls"]);
|
||||
});
|
||||
});
|
||||
|
||||
const createRegistry = (channels: PluginRegistry["channels"]): PluginRegistry => ({
|
||||
plugins: [],
|
||||
tools: [],
|
||||
channels,
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpHandlers: [],
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const emptyRegistry = createRegistry([]);
|
||||
|
||||
const createMSTeamsPlugin = (): ChannelPlugin => ({
|
||||
id: "msteams",
|
||||
meta: {
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
selectionLabel: "Microsoft Teams (Bot Framework)",
|
||||
docsPath: "/channels/msteams",
|
||||
blurb: "Bot Framework; enterprise support.",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
});
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
normalizeThinkLevel,
|
||||
supportsXHighThinking,
|
||||
} from "../../auto-reply/thinking.js";
|
||||
import type { CliDeps } from "../../cli/deps.js";
|
||||
import { createOutboundSendDeps, type CliDeps } from "../../cli/deps.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { resolveSessionTranscriptPath, updateSessionStore } from "../../config/sessions.js";
|
||||
import type { AgentDefaultsConfig } from "../../config/types.js";
|
||||
@@ -355,23 +355,7 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
accountId: resolvedDelivery.accountId,
|
||||
payloads,
|
||||
bestEffort: bestEffortDeliver,
|
||||
deps: {
|
||||
sendWhatsApp: params.deps.sendMessageWhatsApp,
|
||||
sendTelegram: params.deps.sendMessageTelegram,
|
||||
sendDiscord: params.deps.sendMessageDiscord,
|
||||
sendSlack: params.deps.sendMessageSlack,
|
||||
sendSignal: params.deps.sendMessageSignal,
|
||||
sendIMessage: params.deps.sendMessageIMessage,
|
||||
sendMSTeams: params.deps.sendMessageMSTeams
|
||||
? async (to, text, opts) =>
|
||||
await params.deps.sendMessageMSTeams({
|
||||
cfg: params.cfg,
|
||||
to,
|
||||
text,
|
||||
mediaUrl: opts?.mediaUrl,
|
||||
})
|
||||
: undefined,
|
||||
},
|
||||
deps: createOutboundSendDeps(params.deps),
|
||||
});
|
||||
} catch (err) {
|
||||
if (!bestEffortDeliver) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import {
|
||||
extractHookToken,
|
||||
normalizeAgentPayload,
|
||||
@@ -9,6 +12,13 @@ import {
|
||||
} from "./hooks.js";
|
||||
|
||||
describe("gateway hooks helpers", () => {
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
test("resolveHooksConfig normalizes paths + requires token", () => {
|
||||
const base = {
|
||||
hooks: {
|
||||
@@ -84,6 +94,15 @@ describe("gateway hooks helpers", () => {
|
||||
expect(imsg.value.channel).toBe("imessage");
|
||||
}
|
||||
|
||||
setActivePluginRegistry(
|
||||
createRegistry([
|
||||
{
|
||||
pluginId: "msteams",
|
||||
source: "test",
|
||||
plugin: createMSTeamsPlugin({ aliases: ["teams"] }),
|
||||
},
|
||||
]),
|
||||
);
|
||||
const teams = normalizeAgentPayload(
|
||||
{ message: "yo", channel: "teams" },
|
||||
{ idFactory: () => "x" },
|
||||
@@ -97,3 +116,34 @@ describe("gateway hooks helpers", () => {
|
||||
expect(bad.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
const createRegistry = (channels: PluginRegistry["channels"]): PluginRegistry => ({
|
||||
plugins: [],
|
||||
tools: [],
|
||||
channels,
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpHandlers: [],
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const emptyRegistry = createRegistry([]);
|
||||
|
||||
const createMSTeamsPlugin = (params: { aliases?: string[] }): ChannelPlugin => ({
|
||||
id: "msteams",
|
||||
meta: {
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
selectionLabel: "Microsoft Teams (Bot Framework)",
|
||||
docsPath: "/channels/msteams",
|
||||
blurb: "Bot Framework; enterprise support.",
|
||||
aliases: params.aliases,
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
});
|
||||
@@ -1,9 +1,12 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { WebSocket } from "ws";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import { emitAgentEvent, registerAgentRunContext } from "../infra/agent-events.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||
import {
|
||||
agentCommand,
|
||||
@@ -19,6 +22,33 @@ import {
|
||||
|
||||
installGatewayTestHooks();
|
||||
|
||||
const registryState = vi.hoisted(() => ({
|
||||
registry: {
|
||||
plugins: [],
|
||||
tools: [],
|
||||
channels: [],
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpHandlers: [],
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
diagnostics: [],
|
||||
} as PluginRegistry,
|
||||
}));
|
||||
|
||||
vi.mock("./server-plugins.js", async () => {
|
||||
const { setActivePluginRegistry } = await import("../plugins/runtime.js");
|
||||
return {
|
||||
loadGatewayPlugins: (params: { baseMethods: string[] }) => {
|
||||
setActivePluginRegistry(registryState.registry);
|
||||
return {
|
||||
pluginRegistry: registryState.registry,
|
||||
gatewayMethods: params.baseMethods ?? [],
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const _BASE_IMAGE_PNG =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X3mIAAAAASUVORK5CYII=";
|
||||
|
||||
@@ -28,7 +58,26 @@ function expectChannels(call: Record<string, unknown>, channel: string) {
|
||||
}
|
||||
|
||||
describe("gateway server agent", () => {
|
||||
beforeEach(() => {
|
||||
registryState.registry = emptyRegistry;
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
registryState.registry = emptyRegistry;
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
test("agent routes main last-channel msteams", async () => {
|
||||
const registry = createRegistry([
|
||||
{
|
||||
pluginId: "msteams",
|
||||
source: "test",
|
||||
plugin: createMSTeamsPlugin(),
|
||||
},
|
||||
]);
|
||||
registryState.registry = registry;
|
||||
setActivePluginRegistry(registry);
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await fs.writeFile(
|
||||
@@ -73,6 +122,15 @@ describe("gateway server agent", () => {
|
||||
});
|
||||
|
||||
test("agent accepts channel aliases (imsg/teams)", async () => {
|
||||
const registry = createRegistry([
|
||||
{
|
||||
pluginId: "msteams",
|
||||
source: "test",
|
||||
plugin: createMSTeamsPlugin({ aliases: ["teams"] }),
|
||||
},
|
||||
]);
|
||||
registryState.registry = registry;
|
||||
setActivePluginRegistry(registry);
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await fs.writeFile(
|
||||
@@ -410,3 +468,34 @@ describe("gateway server agent", () => {
|
||||
await server.close();
|
||||
});
|
||||
});
|
||||
|
||||
const createRegistry = (channels: PluginRegistry["channels"]): PluginRegistry => ({
|
||||
plugins: [],
|
||||
tools: [],
|
||||
channels,
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpHandlers: [],
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const emptyRegistry = createRegistry([]);
|
||||
|
||||
const createMSTeamsPlugin = (params?: { aliases?: string[] }): ChannelPlugin => ({
|
||||
id: "msteams",
|
||||
meta: {
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
selectionLabel: "Microsoft Teams (Bot Framework)",
|
||||
docsPath: "/channels/msteams",
|
||||
blurb: "Bot Framework; enterprise support.",
|
||||
aliases: params?.aliases,
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,8 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ChannelOutboundAdapter, ChannelPlugin } from "../../channels/plugins/types.js";
|
||||
import type { PluginRegistry } from "../../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { sendMessage, sendPoll } from "./message.js";
|
||||
|
||||
const callGatewayMock = vi.fn();
|
||||
@@ -11,6 +14,11 @@ vi.mock("../../gateway/call.js", () => ({
|
||||
describe("sendMessage channel normalization", () => {
|
||||
beforeEach(() => {
|
||||
callGatewayMock.mockReset();
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
it("normalizes Teams alias", async () => {
|
||||
@@ -18,6 +26,18 @@ describe("sendMessage channel normalization", () => {
|
||||
messageId: "m1",
|
||||
conversationId: "c1",
|
||||
}));
|
||||
setActivePluginRegistry(
|
||||
createRegistry([
|
||||
{
|
||||
pluginId: "msteams",
|
||||
source: "test",
|
||||
plugin: createMSTeamsPlugin({
|
||||
outbound: createMSTeamsOutbound(),
|
||||
aliases: ["teams"],
|
||||
}),
|
||||
},
|
||||
]),
|
||||
);
|
||||
const result = await sendMessage({
|
||||
cfg: {},
|
||||
to: "conversation:19:abc@thread.tacv2",
|
||||
@@ -48,10 +68,27 @@ describe("sendMessage channel normalization", () => {
|
||||
describe("sendPoll channel normalization", () => {
|
||||
beforeEach(() => {
|
||||
callGatewayMock.mockReset();
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
it("normalizes Teams alias for polls", async () => {
|
||||
callGatewayMock.mockResolvedValueOnce({ messageId: "p1" });
|
||||
setActivePluginRegistry(
|
||||
createRegistry([
|
||||
{
|
||||
pluginId: "msteams",
|
||||
source: "test",
|
||||
plugin: createMSTeamsPlugin({
|
||||
aliases: ["teams"],
|
||||
outbound: createMSTeamsOutbound({ includePoll: true }),
|
||||
}),
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await sendPoll({
|
||||
cfg: {},
|
||||
@@ -68,3 +105,64 @@ describe("sendPoll channel normalization", () => {
|
||||
expect(result.channel).toBe("msteams");
|
||||
});
|
||||
});
|
||||
|
||||
const createRegistry = (channels: PluginRegistry["channels"]): PluginRegistry => ({
|
||||
plugins: [],
|
||||
tools: [],
|
||||
channels,
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpHandlers: [],
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const emptyRegistry = createRegistry([]);
|
||||
|
||||
const createMSTeamsOutbound = (opts?: { includePoll?: boolean }): ChannelOutboundAdapter => ({
|
||||
deliveryMode: "direct",
|
||||
sendText: async ({ deps, to, text }) => {
|
||||
const send = deps?.sendMSTeams;
|
||||
if (!send) {
|
||||
throw new Error("sendMSTeams missing");
|
||||
}
|
||||
const result = await send(to, text);
|
||||
return { channel: "msteams", ...result };
|
||||
},
|
||||
sendMedia: async ({ deps, to, text, mediaUrl }) => {
|
||||
const send = deps?.sendMSTeams;
|
||||
if (!send) {
|
||||
throw new Error("sendMSTeams missing");
|
||||
}
|
||||
const result = await send(to, text, { mediaUrl });
|
||||
return { channel: "msteams", ...result };
|
||||
},
|
||||
...(opts?.includePoll
|
||||
? {
|
||||
pollMaxOptions: 12,
|
||||
sendPoll: async () => ({ channel: "msteams", messageId: "p1" }),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
const createMSTeamsPlugin = (params: {
|
||||
aliases?: string[];
|
||||
outbound: ChannelOutboundAdapter;
|
||||
}): ChannelPlugin => ({
|
||||
id: "msteams",
|
||||
meta: {
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
selectionLabel: "Microsoft Teams (Bot Framework)",
|
||||
docsPath: "/channels/msteams",
|
||||
blurb: "Bot Framework; enterprise support.",
|
||||
aliases: params.aliases,
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
outbound: params.outbound,
|
||||
});
|
||||
@@ -1,13 +1,61 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { resolveGatewayMessageChannel } from "./message-channel.js";
|
||||
|
||||
const createRegistry = (channels: PluginRegistry["channels"]): PluginRegistry => ({
|
||||
plugins: [],
|
||||
tools: [],
|
||||
channels,
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpHandlers: [],
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const emptyRegistry = createRegistry([]);
|
||||
|
||||
const msteamsPlugin = {
|
||||
id: "msteams",
|
||||
meta: {
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
selectionLabel: "Microsoft Teams (Bot Framework)",
|
||||
docsPath: "/channels/msteams",
|
||||
blurb: "Bot Framework; enterprise support.",
|
||||
aliases: ["teams"],
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
} satisfies ChannelPlugin;
|
||||
|
||||
describe("message-channel", () => {
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
it("normalizes gateway message channels and rejects unknown values", () => {
|
||||
expect(resolveGatewayMessageChannel("discord")).toBe("discord");
|
||||
expect(resolveGatewayMessageChannel(" imsg ")).toBe("imessage");
|
||||
expect(resolveGatewayMessageChannel("teams")).toBe("msteams");
|
||||
expect(resolveGatewayMessageChannel("web")).toBeUndefined();
|
||||
expect(resolveGatewayMessageChannel("nope")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("normalizes plugin aliases when registered", () => {
|
||||
setActivePluginRegistry(
|
||||
createRegistry([{ pluginId: "msteams", plugin: msteamsPlugin, source: "test" }]),
|
||||
);
|
||||
expect(resolveGatewayMessageChannel("teams")).toBe("msteams");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user