fix(gateway): harden agent provider routing
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
- Models: centralize model override validation + hooks Gmail warnings in doctor. (#602) — thanks @steipete
|
||||
- Agents: avoid base-to-string error stringification in model fallback. (#604) — thanks @steipete
|
||||
- Agents: `sessions_spawn` inherits the requester's provider for child runs (avoid WhatsApp fallback). (#528) — thanks @rlmestre
|
||||
- Gateway/CLI: harden agent provider routing + validation (Slack/MS Teams + aliases). (follow-up #528) — thanks @steipete
|
||||
- Agents: treat billing/insufficient-credits errors as failover-worthy so model fallbacks kick in. (#486) — thanks @steipete
|
||||
- Auth: default billing disable backoff to 5h (doubling, 24h cap) and surface disabled/cooldown profiles in `models list` + doctor. (#486) — thanks @steipete
|
||||
- Commands: harden slash command registry and list text-only commands in `/commands`.
|
||||
|
||||
2
src/agents/lanes.ts
Normal file
2
src/agents/lanes.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const AGENT_LANE_NESTED = "nested" as const;
|
||||
export const AGENT_LANE_SUBAGENT = "subagent" as const;
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
resolveStorePath,
|
||||
} from "../config/sessions.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { INTERNAL_MESSAGE_PROVIDER } from "../utils/message-provider.js";
|
||||
import { AGENT_LANE_NESTED } from "./lanes.js";
|
||||
import { readLatestAssistantReply, runAgentStep } from "./tools/agent-step.js";
|
||||
import { resolveAnnounceTarget } from "./tools/sessions-announce-target.js";
|
||||
import { isAnnounceSkip } from "./tools/sessions-send-helpers.js";
|
||||
@@ -241,7 +243,8 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
message: "Sub-agent announce step.",
|
||||
extraSystemPrompt: announcePrompt,
|
||||
timeoutMs: params.timeoutMs,
|
||||
lane: "nested",
|
||||
provider: INTERNAL_MESSAGE_PROVIDER,
|
||||
lane: AGENT_LANE_NESTED,
|
||||
});
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { INTERNAL_MESSAGE_PROVIDER } from "../../utils/message-provider.js";
|
||||
import { AGENT_LANE_NESTED } from "../lanes.js";
|
||||
import { extractAssistantText, stripToolMessages } from "./sessions-helpers.js";
|
||||
|
||||
export async function readLatestAssistantReply(params: {
|
||||
@@ -34,8 +36,8 @@ export async function runAgentStep(params: {
|
||||
sessionKey: params.sessionKey,
|
||||
idempotencyKey: stepIdem,
|
||||
deliver: false,
|
||||
provider: params.provider ?? "webchat",
|
||||
lane: params.lane ?? "nested",
|
||||
provider: params.provider ?? INTERNAL_MESSAGE_PROVIDER,
|
||||
lane: params.lane ?? AGENT_LANE_NESTED,
|
||||
extraSystemPrompt: params.extraSystemPrompt,
|
||||
},
|
||||
timeoutMs: 10_000,
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
parseAgentSessionKey,
|
||||
} from "../../routing/session-key.js";
|
||||
import { SESSION_LABEL_MAX_LENGTH } from "../../sessions/session-label.js";
|
||||
import type { GatewayMessageProvider } from "../../utils/message-provider.js";
|
||||
import {
|
||||
type GatewayMessageProvider,
|
||||
INTERNAL_MESSAGE_PROVIDER,
|
||||
} from "../../utils/message-provider.js";
|
||||
import { AGENT_LANE_NESTED } from "../lanes.js";
|
||||
import { readLatestAssistantReply, runAgentStep } from "./agent-step.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
import { jsonResult, readStringParam } from "./common.js";
|
||||
@@ -297,8 +301,8 @@ export function createSessionsSendTool(opts?: {
|
||||
sessionKey: resolvedKey,
|
||||
idempotencyKey,
|
||||
deliver: false,
|
||||
provider: "webchat",
|
||||
lane: "nested",
|
||||
provider: INTERNAL_MESSAGE_PROVIDER,
|
||||
lane: AGENT_LANE_NESTED,
|
||||
extraSystemPrompt: agentMessageContext,
|
||||
};
|
||||
const requesterSessionKey = opts?.agentSessionKey;
|
||||
@@ -362,7 +366,7 @@ export function createSessionsSendTool(opts?: {
|
||||
message: incomingMessage,
|
||||
extraSystemPrompt: replyPrompt,
|
||||
timeoutMs: announceTimeoutMs,
|
||||
lane: "nested",
|
||||
lane: AGENT_LANE_NESTED,
|
||||
});
|
||||
if (!replyText || isReplySkip(replyText)) {
|
||||
break;
|
||||
@@ -388,7 +392,7 @@ export function createSessionsSendTool(opts?: {
|
||||
message: "Agent-to-agent announce step.",
|
||||
extraSystemPrompt: announcePrompt,
|
||||
timeoutMs: announceTimeoutMs,
|
||||
lane: "nested",
|
||||
lane: AGENT_LANE_NESTED,
|
||||
});
|
||||
if (
|
||||
announceTarget &&
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "../../routing/session-key.js";
|
||||
import type { GatewayMessageProvider } from "../../utils/message-provider.js";
|
||||
import { resolveAgentConfig } from "../agent-scope.js";
|
||||
import { AGENT_LANE_SUBAGENT } from "../lanes.js";
|
||||
import { buildSubagentSystemPrompt } from "../subagent-announce.js";
|
||||
import { registerSubagentRun } from "../subagent-registry.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
@@ -174,7 +175,7 @@ export function createSessionsSpawnTool(opts?: {
|
||||
provider: opts?.agentProvider,
|
||||
idempotencyKey: childIdem,
|
||||
deliver: false,
|
||||
lane: "subagent",
|
||||
lane: AGENT_LANE_SUBAGENT,
|
||||
extraSystemPrompt: childSystemPrompt,
|
||||
timeout: runTimeoutSeconds > 0 ? runTimeoutSeconds : undefined,
|
||||
label: label || undefined,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
resolveDefaultIMessageAccountId,
|
||||
resolveIMessageAccount,
|
||||
} from "../imessage/accounts.js";
|
||||
import { resolveMSTeamsCredentials } from "../msteams/token.js";
|
||||
import {
|
||||
type ChatProviderId,
|
||||
getChatProviderMeta,
|
||||
@@ -564,6 +565,20 @@ async function buildProviderStatusIndex(
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const accountId = DEFAULT_ACCOUNT_ID;
|
||||
const hasCreds = Boolean(resolveMSTeamsCredentials(cfg.msteams));
|
||||
const hasConfig = Boolean(cfg.msteams);
|
||||
const enabled = cfg.msteams?.enabled !== false;
|
||||
map.set(providerAccountKey("msteams", accountId), {
|
||||
provider: "msteams",
|
||||
accountId,
|
||||
state: hasCreds ? "configured" : "not configured",
|
||||
enabled,
|
||||
configured: hasCreds || hasConfig,
|
||||
});
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -584,6 +599,8 @@ function resolveDefaultAccountId(
|
||||
return resolveDefaultSignalAccountId(cfg) || DEFAULT_ACCOUNT_ID;
|
||||
case "imessage":
|
||||
return resolveDefaultIMessageAccountId(cfg) || DEFAULT_ACCOUNT_ID;
|
||||
case "msteams":
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@ import {
|
||||
formatUsageReportLines,
|
||||
loadProviderUsageSummary,
|
||||
} from "../../infra/provider-usage.js";
|
||||
import { resolveMSTeamsCredentials } from "../../msteams/token.js";
|
||||
import {
|
||||
type ChatProviderId,
|
||||
listChatProviders,
|
||||
} from "../../providers/registry.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import {
|
||||
listSignalAccountIds,
|
||||
@@ -104,6 +106,7 @@ export async function providersListCommand(
|
||||
slack: listSlackAccountIds(cfg),
|
||||
signal: listSignalAccountIds(cfg),
|
||||
imessage: listIMessageAccountIds(cfg),
|
||||
msteams: [DEFAULT_ACCOUNT_ID],
|
||||
};
|
||||
|
||||
const lineBuilders: Record<
|
||||
@@ -194,6 +197,19 @@ export async function providersListCommand(
|
||||
});
|
||||
return `- ${label}: ${formatEnabled(account.enabled)}`;
|
||||
},
|
||||
msteams: async (accountId) => {
|
||||
const label = formatProviderAccountLabel({
|
||||
provider: "msteams",
|
||||
accountId,
|
||||
providerStyle: theme.accent,
|
||||
accountStyle: theme.heading,
|
||||
});
|
||||
const configured = Boolean(resolveMSTeamsCredentials(cfg.msteams));
|
||||
const enabled = cfg.msteams?.enabled !== false;
|
||||
return `- ${label}: ${formatConfigured(configured)}, ${formatEnabled(
|
||||
enabled,
|
||||
)}`;
|
||||
},
|
||||
};
|
||||
|
||||
const authStore = loadAuthProfileStore();
|
||||
@@ -217,6 +233,7 @@ export async function providersListCommand(
|
||||
slack: accountIdsByProvider.slack,
|
||||
signal: accountIdsByProvider.signal,
|
||||
imessage: accountIdsByProvider.imessage,
|
||||
msteams: accountIdsByProvider.msteams,
|
||||
},
|
||||
auth: authProfiles,
|
||||
...(usage ? { usage } : {}),
|
||||
|
||||
@@ -42,6 +42,8 @@ function listAccountIds(cfg: ClawdbotConfig, provider: ChatProvider): string[] {
|
||||
return listSignalAccountIds(cfg);
|
||||
case "imessage":
|
||||
return listIMessageAccountIds(cfg);
|
||||
case "msteams":
|
||||
return [DEFAULT_ACCOUNT_ID];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
} from "../../imessage/accounts.js";
|
||||
import { formatAge } from "../../infra/provider-summary.js";
|
||||
import { collectProvidersStatusIssues } from "../../infra/providers-status-issues.js";
|
||||
import { resolveMSTeamsCredentials } from "../../msteams/token.js";
|
||||
import { listChatProviders } from "../../providers/registry.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import {
|
||||
listSignalAccountIds,
|
||||
@@ -340,6 +342,18 @@ async function formatConfigProvidersStatusLines(
|
||||
configured: imsgConfigured,
|
||||
};
|
||||
}),
|
||||
msteams: [
|
||||
{
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
enabled: cfg.msteams?.enabled !== false,
|
||||
configured: Boolean(resolveMSTeamsCredentials(cfg.msteams)),
|
||||
dmPolicy: cfg.msteams?.dmPolicy ?? "pairing",
|
||||
allowFrom: (cfg.msteams?.allowFrom ?? [])
|
||||
.map((value) => String(value ?? "").trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 2),
|
||||
},
|
||||
],
|
||||
} satisfies Partial<Record<ChatProvider, Array<Record<string, unknown>>>>;
|
||||
|
||||
// WhatsApp linked info (config-only best-effort).
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type Static, type TSchema, Type } from "@sinclair/typebox";
|
||||
import { SESSION_LABEL_MAX_LENGTH } from "../../sessions/session-label.js";
|
||||
import { GATEWAY_AGENT_PROVIDER_VALUES } from "../../utils/message-provider.js";
|
||||
|
||||
const NonEmptyString = Type.String({ minLength: 1 });
|
||||
const SessionLabelString = Type.String({
|
||||
@@ -7,6 +8,10 @@ const SessionLabelString = Type.String({
|
||||
maxLength: SESSION_LABEL_MAX_LENGTH,
|
||||
});
|
||||
|
||||
const AgentProviderSchema = Type.Union(
|
||||
GATEWAY_AGENT_PROVIDER_VALUES.map((provider) => Type.Literal(provider)),
|
||||
);
|
||||
|
||||
export const PresenceEntrySchema = Type.Object(
|
||||
{
|
||||
host: Type.Optional(NonEmptyString),
|
||||
@@ -225,7 +230,7 @@ export const AgentParamsSchema = Type.Object(
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
thinking: Type.Optional(Type.String()),
|
||||
deliver: Type.Optional(Type.Boolean()),
|
||||
provider: Type.Optional(Type.String()),
|
||||
provider: Type.Optional(AgentProviderSchema),
|
||||
timeout: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lane: Type.Optional(Type.String()),
|
||||
extraSystemPrompt: Type.Optional(Type.String()),
|
||||
|
||||
@@ -12,7 +12,12 @@ import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||
import { normalizeMainKey } from "../../routing/session-key.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
||||
import { normalizeMessageProvider } from "../../utils/message-provider.js";
|
||||
import {
|
||||
INTERNAL_MESSAGE_PROVIDER,
|
||||
isDeliverableMessageProvider,
|
||||
isGatewayMessageProvider,
|
||||
normalizeMessageProvider,
|
||||
} from "../../utils/message-provider.js";
|
||||
import { normalizeE164 } from "../../utils.js";
|
||||
import {
|
||||
type AgentWaitParams,
|
||||
@@ -161,21 +166,18 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
if (requestedProvider === "last") {
|
||||
// WebChat is not a deliverable surface. Treat it as "unset" for routing,
|
||||
// so VoiceWake and CLI callers don't get stuck with deliver=false.
|
||||
if (lastProvider && lastProvider !== "webchat") return lastProvider;
|
||||
return wantsDelivery ? "whatsapp" : "webchat";
|
||||
if (lastProvider && lastProvider !== INTERNAL_MESSAGE_PROVIDER) {
|
||||
return lastProvider;
|
||||
}
|
||||
return wantsDelivery ? "whatsapp" : INTERNAL_MESSAGE_PROVIDER;
|
||||
}
|
||||
if (
|
||||
requestedProvider === "whatsapp" ||
|
||||
requestedProvider === "telegram" ||
|
||||
requestedProvider === "discord" ||
|
||||
requestedProvider === "signal" ||
|
||||
requestedProvider === "imessage" ||
|
||||
requestedProvider === "webchat"
|
||||
) {
|
||||
return requestedProvider;
|
||||
|
||||
if (isGatewayMessageProvider(requestedProvider)) return requestedProvider;
|
||||
|
||||
if (lastProvider && lastProvider !== INTERNAL_MESSAGE_PROVIDER) {
|
||||
return lastProvider;
|
||||
}
|
||||
if (lastProvider && lastProvider !== "webchat") return lastProvider;
|
||||
return wantsDelivery ? "whatsapp" : "webchat";
|
||||
return wantsDelivery ? "whatsapp" : INTERNAL_MESSAGE_PROVIDER;
|
||||
})();
|
||||
|
||||
const resolvedTo = (() => {
|
||||
@@ -184,13 +186,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
? request.to.trim()
|
||||
: undefined;
|
||||
if (explicit) return explicit;
|
||||
if (
|
||||
resolvedProvider === "whatsapp" ||
|
||||
resolvedProvider === "telegram" ||
|
||||
resolvedProvider === "discord" ||
|
||||
resolvedProvider === "signal" ||
|
||||
resolvedProvider === "imessage"
|
||||
) {
|
||||
if (isDeliverableMessageProvider(resolvedProvider)) {
|
||||
return lastTo || undefined;
|
||||
}
|
||||
return undefined;
|
||||
@@ -225,7 +221,9 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
return allowFrom[0];
|
||||
})();
|
||||
|
||||
const deliver = request.deliver === true && resolvedProvider !== "webchat";
|
||||
const deliver =
|
||||
request.deliver === true &&
|
||||
resolvedProvider !== INTERNAL_MESSAGE_PROVIDER;
|
||||
|
||||
const accepted = {
|
||||
runId,
|
||||
|
||||
@@ -286,6 +286,50 @@ describe("gateway server agent", () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
test("agent routes main last-channel slack", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
main: {
|
||||
sessionId: "sess-slack",
|
||||
updatedAt: Date.now(),
|
||||
lastProvider: "slack",
|
||||
lastTo: "channel:slack-123",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
const res = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "last",
|
||||
deliver: true,
|
||||
idempotencyKey: "idem-agent-last-slack",
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expectProviders(call, "slack");
|
||||
expect(call.to).toBe("channel:slack-123");
|
||||
expect(call.deliver).toBe(true);
|
||||
expect(call.bestEffortDeliver).toBe(true);
|
||||
expect(call.sessionId).toBe("sess-slack");
|
||||
|
||||
ws.close();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
test("agent routes main last-channel signal", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
@@ -330,6 +374,125 @@ describe("gateway server agent", () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
test("agent routes main last-channel msteams", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
main: {
|
||||
sessionId: "sess-teams",
|
||||
updatedAt: Date.now(),
|
||||
lastProvider: "msteams",
|
||||
lastTo: "conversation:teams-123",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
const res = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "last",
|
||||
deliver: true,
|
||||
idempotencyKey: "idem-agent-last-msteams",
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expectProviders(call, "msteams");
|
||||
expect(call.to).toBe("conversation:teams-123");
|
||||
expect(call.deliver).toBe(true);
|
||||
expect(call.bestEffortDeliver).toBe(true);
|
||||
expect(call.sessionId).toBe("sess-teams");
|
||||
|
||||
ws.close();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
test("agent accepts provider aliases (imsg/teams)", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
await fs.writeFile(
|
||||
testState.sessionStorePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
main: {
|
||||
sessionId: "sess-alias",
|
||||
updatedAt: Date.now(),
|
||||
lastProvider: "imessage",
|
||||
lastTo: "chat_id:123",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
const resIMessage = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "imsg",
|
||||
deliver: true,
|
||||
idempotencyKey: "idem-agent-imsg",
|
||||
});
|
||||
expect(resIMessage.ok).toBe(true);
|
||||
|
||||
const resTeams = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "teams",
|
||||
to: "conversation:teams-abc",
|
||||
deliver: false,
|
||||
idempotencyKey: "idem-agent-teams",
|
||||
});
|
||||
expect(resTeams.ok).toBe(true);
|
||||
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const lastIMessageCall = spy.mock.calls.at(-2)?.[0] as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expectProviders(lastIMessageCall, "imessage");
|
||||
expect(lastIMessageCall.to).toBe("chat_id:123");
|
||||
|
||||
const lastTeamsCall = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expectProviders(lastTeamsCall, "msteams");
|
||||
expect(lastTeamsCall.to).toBe("conversation:teams-abc");
|
||||
|
||||
ws.close();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
test("agent rejects unknown provider", async () => {
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
const res = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "sms",
|
||||
idempotencyKey: "idem-agent-bad-provider",
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
expect(res.error?.code).toBe("INVALID_REQUEST");
|
||||
|
||||
ws.close();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
test("agent ignores webchat last-channel for routing", async () => {
|
||||
testState.allowFrom = ["+1555"];
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
|
||||
@@ -5,34 +5,23 @@ import { resolveMSTeamsCredentials } from "../../msteams/token.js";
|
||||
import { listEnabledSignalAccounts } from "../../signal/accounts.js";
|
||||
import { listEnabledSlackAccounts } from "../../slack/accounts.js";
|
||||
import { listEnabledTelegramAccounts } from "../../telegram/accounts.js";
|
||||
import { normalizeMessageProvider } from "../../utils/message-provider.js";
|
||||
import {
|
||||
DELIVERABLE_MESSAGE_PROVIDERS,
|
||||
type DeliverableMessageProvider,
|
||||
normalizeMessageProvider,
|
||||
} from "../../utils/message-provider.js";
|
||||
import {
|
||||
listEnabledWhatsAppAccounts,
|
||||
resolveWhatsAppAccount,
|
||||
} from "../../web/accounts.js";
|
||||
import { webAuthExists } from "../../web/session.js";
|
||||
|
||||
export type MessageProviderId =
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| "msteams";
|
||||
export type MessageProviderId = DeliverableMessageProvider;
|
||||
|
||||
const MESSAGE_PROVIDERS: MessageProviderId[] = [
|
||||
"whatsapp",
|
||||
"telegram",
|
||||
"discord",
|
||||
"slack",
|
||||
"signal",
|
||||
"imessage",
|
||||
"msteams",
|
||||
];
|
||||
const MESSAGE_PROVIDERS = [...DELIVERABLE_MESSAGE_PROVIDERS];
|
||||
|
||||
function isKnownProvider(value: string): value is MessageProviderId {
|
||||
return (MESSAGE_PROVIDERS as string[]).includes(value);
|
||||
return (MESSAGE_PROVIDERS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
async function isWhatsAppConfigured(cfg: ClawdbotConfig): Promise<boolean> {
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import type {
|
||||
DeliverableMessageProvider,
|
||||
GatewayMessageProvider,
|
||||
} from "../../utils/message-provider.js";
|
||||
import { normalizeE164 } from "../../utils.js";
|
||||
|
||||
export type OutboundProvider =
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| "msteams"
|
||||
| "none";
|
||||
export type OutboundProvider = DeliverableMessageProvider | "none";
|
||||
|
||||
export type HeartbeatTarget = OutboundProvider | "last";
|
||||
|
||||
@@ -25,15 +21,7 @@ export type OutboundTargetResolution =
|
||||
| { ok: false; error: Error };
|
||||
|
||||
export function resolveOutboundTarget(params: {
|
||||
provider:
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| "msteams"
|
||||
| "webchat";
|
||||
provider: GatewayMessageProvider;
|
||||
to?: string;
|
||||
allowFrom?: string[];
|
||||
}): OutboundTargetResolution {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
describe("provider registry", () => {
|
||||
it("normalizes aliases", () => {
|
||||
expect(normalizeChatProviderId("imsg")).toBe("imessage");
|
||||
expect(normalizeChatProviderId("teams")).toBe("msteams");
|
||||
});
|
||||
|
||||
it("keeps Telegram first in the default order", () => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { normalizeMessageProvider } from "../utils/message-provider.js";
|
||||
|
||||
export const CHAT_PROVIDER_ORDER = [
|
||||
"telegram",
|
||||
"whatsapp",
|
||||
@@ -5,6 +7,7 @@ export const CHAT_PROVIDER_ORDER = [
|
||||
"slack",
|
||||
"signal",
|
||||
"imessage",
|
||||
"msteams",
|
||||
] as const;
|
||||
|
||||
export type ChatProviderId = (typeof CHAT_PROVIDER_ORDER)[number];
|
||||
@@ -69,10 +72,14 @@ const CHAT_PROVIDER_META: Record<ChatProviderId, ChatProviderMeta> = {
|
||||
docsLabel: "imessage",
|
||||
blurb: "this is still a work in progress.",
|
||||
},
|
||||
};
|
||||
|
||||
const CHAT_PROVIDER_ALIASES: Record<string, ChatProviderId> = {
|
||||
imsg: "imessage",
|
||||
msteams: {
|
||||
id: "msteams",
|
||||
label: "MS Teams",
|
||||
selectionLabel: "Microsoft Teams (Bot Framework)",
|
||||
docsPath: "/msteams",
|
||||
docsLabel: "msteams",
|
||||
blurb: "supported (Bot Framework).",
|
||||
},
|
||||
};
|
||||
|
||||
const WEBSITE_URL = "https://clawd.bot";
|
||||
@@ -88,9 +95,8 @@ export function getChatProviderMeta(id: ChatProviderId): ChatProviderMeta {
|
||||
export function normalizeChatProviderId(
|
||||
raw?: string | null,
|
||||
): ChatProviderId | null {
|
||||
const trimmed = (raw ?? "").trim().toLowerCase();
|
||||
if (!trimmed) return null;
|
||||
const normalized = CHAT_PROVIDER_ALIASES[trimmed] ?? trimmed;
|
||||
const normalized = normalizeMessageProvider(raw);
|
||||
if (!normalized) return null;
|
||||
return CHAT_PROVIDER_ORDER.includes(normalized as ChatProviderId)
|
||||
? (normalized as ChatProviderId)
|
||||
: null;
|
||||
|
||||
@@ -8,17 +8,7 @@ export function normalizeMessageProvider(
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export type GatewayMessageProvider =
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| "msteams"
|
||||
| "webchat";
|
||||
|
||||
const GATEWAY_MESSAGE_PROVIDERS: GatewayMessageProvider[] = [
|
||||
export const DELIVERABLE_MESSAGE_PROVIDERS = [
|
||||
"whatsapp",
|
||||
"telegram",
|
||||
"discord",
|
||||
@@ -26,13 +16,48 @@ const GATEWAY_MESSAGE_PROVIDERS: GatewayMessageProvider[] = [
|
||||
"signal",
|
||||
"imessage",
|
||||
"msteams",
|
||||
] as const;
|
||||
|
||||
export type DeliverableMessageProvider =
|
||||
(typeof DELIVERABLE_MESSAGE_PROVIDERS)[number];
|
||||
|
||||
export const INTERNAL_MESSAGE_PROVIDER = "webchat" as const;
|
||||
export type InternalMessageProvider = typeof INTERNAL_MESSAGE_PROVIDER;
|
||||
|
||||
export type GatewayMessageProvider =
|
||||
| DeliverableMessageProvider
|
||||
| InternalMessageProvider;
|
||||
|
||||
export const GATEWAY_MESSAGE_PROVIDERS = [
|
||||
...DELIVERABLE_MESSAGE_PROVIDERS,
|
||||
"webchat",
|
||||
];
|
||||
] as const;
|
||||
|
||||
export const GATEWAY_AGENT_PROVIDER_ALIASES = ["imsg", "teams"] as const;
|
||||
export type GatewayAgentProviderAlias =
|
||||
(typeof GATEWAY_AGENT_PROVIDER_ALIASES)[number];
|
||||
|
||||
export type GatewayAgentProviderHint =
|
||||
| GatewayMessageProvider
|
||||
| "last"
|
||||
| GatewayAgentProviderAlias;
|
||||
|
||||
export const GATEWAY_AGENT_PROVIDER_VALUES = [
|
||||
...GATEWAY_MESSAGE_PROVIDERS,
|
||||
"last",
|
||||
...GATEWAY_AGENT_PROVIDER_ALIASES,
|
||||
] as const;
|
||||
|
||||
export function isGatewayMessageProvider(
|
||||
value: string,
|
||||
): value is GatewayMessageProvider {
|
||||
return (GATEWAY_MESSAGE_PROVIDERS as string[]).includes(value);
|
||||
return (GATEWAY_MESSAGE_PROVIDERS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
export function isDeliverableMessageProvider(
|
||||
value: string,
|
||||
): value is DeliverableMessageProvider {
|
||||
return (DELIVERABLE_MESSAGE_PROVIDERS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
export function resolveGatewayMessageProvider(
|
||||
|
||||
Reference in New Issue
Block a user