mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 17:38:27 +00:00
refactor!: rename chat providers to channels
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
type GatewayClientMode,
|
||||
type GatewayClientName,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
import { GatewayClient } from "./client.js";
|
||||
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
type GatewayClientMode,
|
||||
type GatewayClientName,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
import {
|
||||
type ConnectParams,
|
||||
type EventFrame,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { listProviderPlugins } from "../providers/plugins/index.js";
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import {
|
||||
buildGatewayReloadPlan,
|
||||
diffConfigPaths,
|
||||
@@ -37,11 +37,11 @@ describe("buildGatewayReloadPlan", () => {
|
||||
});
|
||||
|
||||
it("restarts providers when provider config prefixes change", () => {
|
||||
const changedPaths = ["web.enabled", "telegram.botToken"];
|
||||
const changedPaths = ["web.enabled", "channels.telegram.botToken"];
|
||||
const plan = buildGatewayReloadPlan(changedPaths);
|
||||
expect(plan.restartGateway).toBe(false);
|
||||
const expected = new Set(
|
||||
listProviderPlugins()
|
||||
listChannelPlugins()
|
||||
.filter((plugin) =>
|
||||
(plugin.reload?.configPrefixes ?? []).some((prefix) =>
|
||||
changedPaths.some(
|
||||
@@ -52,7 +52,7 @@ describe("buildGatewayReloadPlan", () => {
|
||||
.map((plugin) => plugin.id),
|
||||
);
|
||||
expect(expected.size).toBeGreaterThan(0);
|
||||
expect(plan.restartProviders).toEqual(expected);
|
||||
expect(plan.restartChannels).toEqual(expected);
|
||||
});
|
||||
|
||||
it("treats gateway.remote as no-op", () => {
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import chokidar from "chokidar";
|
||||
|
||||
import {
|
||||
type ChannelId,
|
||||
listChannelPlugins,
|
||||
} from "../channels/plugins/index.js";
|
||||
import type {
|
||||
ClawdbotConfig,
|
||||
ConfigFileSnapshot,
|
||||
GatewayReloadMode,
|
||||
} from "../config/config.js";
|
||||
import {
|
||||
listProviderPlugins,
|
||||
type ProviderId,
|
||||
} from "../providers/plugins/index.js";
|
||||
|
||||
export type GatewayReloadSettings = {
|
||||
mode: GatewayReloadMode;
|
||||
debounceMs: number;
|
||||
};
|
||||
|
||||
export type ProviderKind = ProviderId;
|
||||
export type ChannelKind = ChannelId;
|
||||
|
||||
export type GatewayReloadPlan = {
|
||||
changedPaths: string[];
|
||||
@@ -27,7 +26,7 @@ export type GatewayReloadPlan = {
|
||||
restartBrowserControl: boolean;
|
||||
restartCron: boolean;
|
||||
restartHeartbeat: boolean;
|
||||
restartProviders: Set<ProviderKind>;
|
||||
restartChannels: Set<ChannelKind>;
|
||||
noopPaths: string[];
|
||||
};
|
||||
|
||||
@@ -43,7 +42,7 @@ type ReloadAction =
|
||||
| "restart-browser-control"
|
||||
| "restart-cron"
|
||||
| "restart-heartbeat"
|
||||
| `restart-provider:${ProviderId}`;
|
||||
| `restart-channel:${ChannelId}`;
|
||||
|
||||
const DEFAULT_RELOAD_SETTINGS: GatewayReloadSettings = {
|
||||
mode: "hybrid",
|
||||
@@ -96,14 +95,14 @@ let cachedReloadRules: ReloadRule[] | null = null;
|
||||
|
||||
function listReloadRules(): ReloadRule[] {
|
||||
if (cachedReloadRules) return cachedReloadRules;
|
||||
// Provider docking: plugins contribute hot reload/no-op prefixes here.
|
||||
const providerReloadRules: ReloadRule[] = listProviderPlugins().flatMap(
|
||||
// Channel docking: plugins contribute hot reload/no-op prefixes here.
|
||||
const channelReloadRules: ReloadRule[] = listChannelPlugins().flatMap(
|
||||
(plugin) => [
|
||||
...(plugin.reload?.configPrefixes ?? []).map(
|
||||
(prefix): ReloadRule => ({
|
||||
prefix,
|
||||
kind: "hot",
|
||||
actions: [`restart-provider:${plugin.id}` as ReloadAction],
|
||||
actions: [`restart-channel:${plugin.id}` as ReloadAction],
|
||||
}),
|
||||
),
|
||||
...(plugin.reload?.noopPrefixes ?? []).map(
|
||||
@@ -116,7 +115,7 @@ function listReloadRules(): ReloadRule[] {
|
||||
);
|
||||
const rules = [
|
||||
...BASE_RELOAD_RULES,
|
||||
...providerReloadRules,
|
||||
...channelReloadRules,
|
||||
...BASE_RELOAD_RULES_TAIL,
|
||||
];
|
||||
cachedReloadRules = rules;
|
||||
@@ -203,14 +202,14 @@ export function buildGatewayReloadPlan(
|
||||
restartBrowserControl: false,
|
||||
restartCron: false,
|
||||
restartHeartbeat: false,
|
||||
restartProviders: new Set(),
|
||||
restartChannels: new Set(),
|
||||
noopPaths: [],
|
||||
};
|
||||
|
||||
const applyAction = (action: ReloadAction) => {
|
||||
if (action.startsWith("restart-provider:")) {
|
||||
const provider = action.slice("restart-provider:".length) as ProviderId;
|
||||
plan.restartProviders.add(provider);
|
||||
if (action.startsWith("restart-channel:")) {
|
||||
const channel = action.slice("restart-channel:".length) as ChannelId;
|
||||
plan.restartChannels.add(channel);
|
||||
return;
|
||||
}
|
||||
switch (action) {
|
||||
|
||||
@@ -29,7 +29,7 @@ import type { ClawdbotConfig, ModelProviderConfig } from "../config/types.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { GatewayClient } from "./client.js";
|
||||
import { renderCatNoncePngBase64 } from "./live-image-probe.js";
|
||||
|
||||
@@ -8,7 +8,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
|
||||
import { GatewayClient } from "./client.js";
|
||||
import { startGatewayServer } from "./server.js";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { rawDataToString } from "../infra/ws.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
||||
|
||||
async function getFreePort(): Promise<number> {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type HookMappingConfig,
|
||||
type HooksConfig,
|
||||
} from "../config/config.js";
|
||||
import type { HookMessageProvider } from "./hooks.js";
|
||||
import type { HookMessageChannel } from "./hooks.js";
|
||||
|
||||
export type HookMappingResolved = {
|
||||
id: string;
|
||||
@@ -19,7 +19,7 @@ export type HookMappingResolved = {
|
||||
messageTemplate?: string;
|
||||
textTemplate?: string;
|
||||
deliver?: boolean;
|
||||
provider?: HookMessageProvider;
|
||||
channel?: HookMessageChannel;
|
||||
to?: string;
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
@@ -52,7 +52,7 @@ export type HookAction =
|
||||
wakeMode: "now" | "next-heartbeat";
|
||||
sessionKey?: string;
|
||||
deliver?: boolean;
|
||||
provider?: HookMessageProvider;
|
||||
channel?: HookMessageChannel;
|
||||
to?: string;
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
@@ -90,7 +90,7 @@ type HookTransformResult = Partial<{
|
||||
name: string;
|
||||
sessionKey: string;
|
||||
deliver: boolean;
|
||||
provider: HookMessageProvider;
|
||||
channel: HookMessageChannel;
|
||||
to: string;
|
||||
model: string;
|
||||
thinking: string;
|
||||
@@ -179,7 +179,7 @@ function normalizeHookMapping(
|
||||
messageTemplate: mapping.messageTemplate,
|
||||
textTemplate: mapping.textTemplate,
|
||||
deliver: mapping.deliver,
|
||||
provider: mapping.provider,
|
||||
channel: mapping.channel,
|
||||
to: mapping.to,
|
||||
model: mapping.model,
|
||||
thinking: mapping.thinking,
|
||||
@@ -225,7 +225,7 @@ function buildActionFromMapping(
|
||||
wakeMode: mapping.wakeMode ?? "now",
|
||||
sessionKey: renderOptional(mapping.sessionKey, ctx),
|
||||
deliver: mapping.deliver,
|
||||
provider: mapping.provider,
|
||||
channel: mapping.channel,
|
||||
to: renderOptional(mapping.to, ctx),
|
||||
model: renderOptional(mapping.model, ctx),
|
||||
thinking: renderOptional(mapping.thinking, ctx),
|
||||
@@ -276,7 +276,7 @@ function mergeAction(
|
||||
typeof override.deliver === "boolean"
|
||||
? override.deliver
|
||||
: baseAgent?.deliver,
|
||||
provider: override.provider ?? baseAgent?.provider,
|
||||
channel: override.channel ?? baseAgent?.channel,
|
||||
to: override.to ?? baseAgent?.to,
|
||||
model: override.model ?? baseAgent?.model,
|
||||
thinking: override.thinking ?? baseAgent?.thinking,
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("gateway hooks helpers", () => {
|
||||
expect(normalizeWakePayload({ text: " ", mode: "now" }).ok).toBe(false);
|
||||
});
|
||||
|
||||
test("normalizeAgentPayload defaults + validates provider", () => {
|
||||
test("normalizeAgentPayload defaults + validates channel", () => {
|
||||
const ok = normalizeAgentPayload(
|
||||
{ message: "hello" },
|
||||
{ idFactory: () => "fixed" },
|
||||
@@ -64,7 +64,7 @@ describe("gateway hooks helpers", () => {
|
||||
expect(ok.ok).toBe(true);
|
||||
if (ok.ok) {
|
||||
expect(ok.value.sessionKey).toBe("hook:fixed");
|
||||
expect(ok.value.provider).toBe("last");
|
||||
expect(ok.value.channel).toBe("last");
|
||||
expect(ok.value.name).toBe("Hook");
|
||||
expect(ok.value.deliver).toBe(true);
|
||||
}
|
||||
@@ -79,24 +79,24 @@ describe("gateway hooks helpers", () => {
|
||||
}
|
||||
|
||||
const imsg = normalizeAgentPayload(
|
||||
{ message: "yo", provider: "imsg" },
|
||||
{ message: "yo", channel: "imsg" },
|
||||
{ idFactory: () => "x" },
|
||||
);
|
||||
expect(imsg.ok).toBe(true);
|
||||
if (imsg.ok) {
|
||||
expect(imsg.value.provider).toBe("imessage");
|
||||
expect(imsg.value.channel).toBe("imessage");
|
||||
}
|
||||
|
||||
const teams = normalizeAgentPayload(
|
||||
{ message: "yo", provider: "teams" },
|
||||
{ message: "yo", channel: "teams" },
|
||||
{ idFactory: () => "x" },
|
||||
);
|
||||
expect(teams.ok).toBe(true);
|
||||
if (teams.ok) {
|
||||
expect(teams.value.provider).toBe("msteams");
|
||||
expect(teams.value.channel).toBe("msteams");
|
||||
}
|
||||
|
||||
const bad = normalizeAgentPayload({ message: "yo", provider: "sms" });
|
||||
const bad = normalizeAgentPayload({ message: "yo", channel: "sms" });
|
||||
expect(bad.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import {
|
||||
listProviderPlugins,
|
||||
type ProviderId,
|
||||
} from "../providers/plugins/index.js";
|
||||
import { normalizeMessageProvider } from "../utils/message-provider.js";
|
||||
import { normalizeMessageChannel } from "../utils/message-channel.js";
|
||||
import {
|
||||
type HookMappingResolved,
|
||||
resolveHookMappings,
|
||||
@@ -142,29 +140,29 @@ export type HookAgentPayload = {
|
||||
wakeMode: "now" | "next-heartbeat";
|
||||
sessionKey: string;
|
||||
deliver: boolean;
|
||||
provider: HookMessageProvider;
|
||||
channel: HookMessageChannel;
|
||||
to?: string;
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
timeoutSeconds?: number;
|
||||
};
|
||||
|
||||
const HOOK_PROVIDER_VALUES = [
|
||||
const HOOK_CHANNEL_VALUES = [
|
||||
"last",
|
||||
...listProviderPlugins().map((plugin) => plugin.id),
|
||||
...listChannelPlugins().map((plugin) => plugin.id),
|
||||
];
|
||||
|
||||
export type HookMessageProvider = ProviderId | "last";
|
||||
export type HookMessageChannel = ChannelId | "last";
|
||||
|
||||
const hookProviderSet = new Set<string>(HOOK_PROVIDER_VALUES);
|
||||
export const HOOK_PROVIDER_ERROR = `provider must be ${HOOK_PROVIDER_VALUES.join("|")}`;
|
||||
const hookChannelSet = new Set<string>(HOOK_CHANNEL_VALUES);
|
||||
export const HOOK_CHANNEL_ERROR = `channel must be ${HOOK_CHANNEL_VALUES.join("|")}`;
|
||||
|
||||
export function resolveHookProvider(raw: unknown): HookMessageProvider | null {
|
||||
export function resolveHookChannel(raw: unknown): HookMessageChannel | null {
|
||||
if (raw === undefined) return "last";
|
||||
if (typeof raw !== "string") return null;
|
||||
const normalized = normalizeMessageProvider(raw);
|
||||
if (!normalized || !hookProviderSet.has(normalized)) return null;
|
||||
return normalized as HookMessageProvider;
|
||||
const normalized = normalizeMessageChannel(raw);
|
||||
if (!normalized || !hookChannelSet.has(normalized)) return null;
|
||||
return normalized as HookMessageChannel;
|
||||
}
|
||||
|
||||
export function resolveHookDeliver(raw: unknown): boolean {
|
||||
@@ -194,8 +192,8 @@ export function normalizeAgentPayload(
|
||||
typeof sessionKeyRaw === "string" && sessionKeyRaw.trim()
|
||||
? sessionKeyRaw.trim()
|
||||
: `hook:${idFactory()}`;
|
||||
const provider = resolveHookProvider(payload.provider);
|
||||
if (!provider) return { ok: false, error: HOOK_PROVIDER_ERROR };
|
||||
const channel = resolveHookChannel(payload.channel);
|
||||
if (!channel) return { ok: false, error: HOOK_CHANNEL_ERROR };
|
||||
const toRaw = payload.to;
|
||||
const to =
|
||||
typeof toRaw === "string" && toRaw.trim() ? toRaw.trim() : undefined;
|
||||
@@ -228,7 +226,7 @@ export function normalizeAgentPayload(
|
||||
wakeMode,
|
||||
sessionKey,
|
||||
deliver,
|
||||
provider,
|
||||
channel,
|
||||
to,
|
||||
model,
|
||||
thinking,
|
||||
|
||||
@@ -234,7 +234,7 @@ export async function handleOpenAiHttpRequest(
|
||||
sessionKey,
|
||||
runId,
|
||||
deliver: false,
|
||||
messageProvider: "webchat",
|
||||
messageChannel: "webchat",
|
||||
bestEffortDeliver: false,
|
||||
},
|
||||
defaultRuntime,
|
||||
@@ -352,7 +352,7 @@ export async function handleOpenAiHttpRequest(
|
||||
sessionKey,
|
||||
runId,
|
||||
deliver: false,
|
||||
messageProvider: "webchat",
|
||||
messageChannel: "webchat",
|
||||
bestEffortDeliver: false,
|
||||
},
|
||||
defaultRuntime,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { SystemPresence } from "../infra/system-presence.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
import { GatewayClient } from "./client.js";
|
||||
|
||||
export type GatewayProbeAuth = {
|
||||
|
||||
@@ -11,6 +11,12 @@ import {
|
||||
AgentsListResultSchema,
|
||||
type AgentWaitParams,
|
||||
AgentWaitParamsSchema,
|
||||
type ChannelsLogoutParams,
|
||||
ChannelsLogoutParamsSchema,
|
||||
type ChannelsStatusParams,
|
||||
ChannelsStatusParamsSchema,
|
||||
type ChannelsStatusResult,
|
||||
ChannelsStatusResultSchema,
|
||||
type ChatAbortParams,
|
||||
ChatAbortParamsSchema,
|
||||
type ChatEvent,
|
||||
@@ -86,12 +92,6 @@ import {
|
||||
type PresenceEntry,
|
||||
PresenceEntrySchema,
|
||||
ProtocolSchemas,
|
||||
type ProvidersLogoutParams,
|
||||
ProvidersLogoutParamsSchema,
|
||||
type ProvidersStatusParams,
|
||||
ProvidersStatusParamsSchema,
|
||||
type ProvidersStatusResult,
|
||||
ProvidersStatusResultSchema,
|
||||
type RequestFrame,
|
||||
RequestFrameSchema,
|
||||
type ResponseFrame,
|
||||
@@ -248,11 +248,11 @@ export const validateWizardStatusParams = ajv.compile<WizardStatusParams>(
|
||||
);
|
||||
export const validateTalkModeParams =
|
||||
ajv.compile<TalkModeParams>(TalkModeParamsSchema);
|
||||
export const validateProvidersStatusParams = ajv.compile<ProvidersStatusParams>(
|
||||
ProvidersStatusParamsSchema,
|
||||
export const validateChannelsStatusParams = ajv.compile<ChannelsStatusParams>(
|
||||
ChannelsStatusParamsSchema,
|
||||
);
|
||||
export const validateProvidersLogoutParams = ajv.compile<ProvidersLogoutParams>(
|
||||
ProvidersLogoutParamsSchema,
|
||||
export const validateChannelsLogoutParams = ajv.compile<ChannelsLogoutParams>(
|
||||
ChannelsLogoutParamsSchema,
|
||||
);
|
||||
export const validateModelsListParams = ajv.compile<ModelsListParams>(
|
||||
ModelsListParamsSchema,
|
||||
@@ -350,9 +350,9 @@ export {
|
||||
WizardNextResultSchema,
|
||||
WizardStartResultSchema,
|
||||
WizardStatusResultSchema,
|
||||
ProvidersStatusParamsSchema,
|
||||
ProvidersStatusResultSchema,
|
||||
ProvidersLogoutParamsSchema,
|
||||
ChannelsStatusParamsSchema,
|
||||
ChannelsStatusResultSchema,
|
||||
ChannelsLogoutParamsSchema,
|
||||
WebLoginStartParamsSchema,
|
||||
WebLoginWaitParamsSchema,
|
||||
AgentSummarySchema,
|
||||
@@ -417,9 +417,9 @@ export type {
|
||||
WizardStartResult,
|
||||
WizardStatusResult,
|
||||
TalkModeParams,
|
||||
ProvidersStatusParams,
|
||||
ProvidersStatusResult,
|
||||
ProvidersLogoutParams,
|
||||
ChannelsStatusParams,
|
||||
ChannelsStatusResult,
|
||||
ChannelsLogoutParams,
|
||||
WebLoginStartParams,
|
||||
WebLoginWaitParams,
|
||||
AgentSummary,
|
||||
|
||||
@@ -206,7 +206,7 @@ export const SendParamsSchema = Type.Object(
|
||||
message: NonEmptyString,
|
||||
mediaUrl: Type.Optional(Type.String()),
|
||||
gifPlayback: Type.Optional(Type.Boolean()),
|
||||
provider: Type.Optional(Type.String()),
|
||||
channel: Type.Optional(Type.String()),
|
||||
accountId: Type.Optional(Type.String()),
|
||||
idempotencyKey: NonEmptyString,
|
||||
},
|
||||
@@ -220,7 +220,7 @@ export const PollParamsSchema = Type.Object(
|
||||
options: Type.Array(NonEmptyString, { minItems: 2, maxItems: 12 }),
|
||||
maxSelections: Type.Optional(Type.Integer({ minimum: 1, maximum: 12 })),
|
||||
durationHours: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
provider: Type.Optional(Type.String()),
|
||||
channel: Type.Optional(Type.String()),
|
||||
accountId: Type.Optional(Type.String()),
|
||||
idempotencyKey: NonEmptyString,
|
||||
},
|
||||
@@ -235,7 +235,7 @@ export const AgentParamsSchema = Type.Object(
|
||||
thinking: Type.Optional(Type.String()),
|
||||
deliver: Type.Optional(Type.Boolean()),
|
||||
attachments: Type.Optional(Type.Array(Type.Unknown())),
|
||||
provider: Type.Optional(Type.String()),
|
||||
channel: Type.Optional(Type.String()),
|
||||
timeout: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lane: Type.Optional(Type.String()),
|
||||
extraSystemPrompt: Type.Optional(Type.String()),
|
||||
@@ -588,7 +588,7 @@ export const TalkModeParamsSchema = Type.Object(
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const ProvidersStatusParamsSchema = Type.Object(
|
||||
export const ChannelsStatusParamsSchema = Type.Object(
|
||||
{
|
||||
probe: Type.Optional(Type.Boolean()),
|
||||
timeoutMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
@@ -596,9 +596,9 @@ export const ProvidersStatusParamsSchema = Type.Object(
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
// Provider docking: providers.status is intentionally schema-light so new
|
||||
// providers can ship without protocol updates.
|
||||
export const ProviderAccountSnapshotSchema = Type.Object(
|
||||
// Channel docking: channels.status is intentionally schema-light so new
|
||||
// channels can ship without protocol updates.
|
||||
export const ChannelAccountSnapshotSchema = Type.Object(
|
||||
{
|
||||
accountId: NonEmptyString,
|
||||
name: Type.Optional(Type.String()),
|
||||
@@ -635,24 +635,24 @@ export const ProviderAccountSnapshotSchema = Type.Object(
|
||||
{ additionalProperties: true },
|
||||
);
|
||||
|
||||
export const ProvidersStatusResultSchema = Type.Object(
|
||||
export const ChannelsStatusResultSchema = Type.Object(
|
||||
{
|
||||
ts: Type.Integer({ minimum: 0 }),
|
||||
providerOrder: Type.Array(NonEmptyString),
|
||||
providerLabels: Type.Record(NonEmptyString, NonEmptyString),
|
||||
providers: Type.Record(NonEmptyString, Type.Unknown()),
|
||||
providerAccounts: Type.Record(
|
||||
channelOrder: Type.Array(NonEmptyString),
|
||||
channelLabels: Type.Record(NonEmptyString, NonEmptyString),
|
||||
channels: Type.Record(NonEmptyString, Type.Unknown()),
|
||||
channelAccounts: Type.Record(
|
||||
NonEmptyString,
|
||||
Type.Array(ProviderAccountSnapshotSchema),
|
||||
Type.Array(ChannelAccountSnapshotSchema),
|
||||
),
|
||||
providerDefaultAccountId: Type.Record(NonEmptyString, NonEmptyString),
|
||||
channelDefaultAccountId: Type.Record(NonEmptyString, NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const ProvidersLogoutParamsSchema = Type.Object(
|
||||
export const ChannelsLogoutParamsSchema = Type.Object(
|
||||
{
|
||||
provider: NonEmptyString,
|
||||
channel: NonEmptyString,
|
||||
accountId: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
@@ -788,7 +788,7 @@ export const CronPayloadSchema = Type.Union([
|
||||
thinking: Type.Optional(Type.String()),
|
||||
timeoutSeconds: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
deliver: Type.Optional(Type.Boolean()),
|
||||
provider: Type.Optional(
|
||||
channel: Type.Optional(
|
||||
Type.Union([Type.Literal("last"), NonEmptyString]),
|
||||
),
|
||||
to: Type.Optional(Type.String()),
|
||||
@@ -1078,9 +1078,9 @@ export const ProtocolSchemas: Record<string, TSchema> = {
|
||||
WizardStartResult: WizardStartResultSchema,
|
||||
WizardStatusResult: WizardStatusResultSchema,
|
||||
TalkModeParams: TalkModeParamsSchema,
|
||||
ProvidersStatusParams: ProvidersStatusParamsSchema,
|
||||
ProvidersStatusResult: ProvidersStatusResultSchema,
|
||||
ProvidersLogoutParams: ProvidersLogoutParamsSchema,
|
||||
ChannelsStatusParams: ChannelsStatusParamsSchema,
|
||||
ChannelsStatusResult: ChannelsStatusResultSchema,
|
||||
ChannelsLogoutParams: ChannelsLogoutParamsSchema,
|
||||
WebLoginStartParams: WebLoginStartParamsSchema,
|
||||
WebLoginWaitParams: WebLoginWaitParamsSchema,
|
||||
AgentSummary: AgentSummarySchema,
|
||||
@@ -1157,9 +1157,9 @@ export type WizardNextResult = Static<typeof WizardNextResultSchema>;
|
||||
export type WizardStartResult = Static<typeof WizardStartResultSchema>;
|
||||
export type WizardStatusResult = Static<typeof WizardStatusResultSchema>;
|
||||
export type TalkModeParams = Static<typeof TalkModeParamsSchema>;
|
||||
export type ProvidersStatusParams = Static<typeof ProvidersStatusParamsSchema>;
|
||||
export type ProvidersStatusResult = Static<typeof ProvidersStatusResultSchema>;
|
||||
export type ProvidersLogoutParams = Static<typeof ProvidersLogoutParamsSchema>;
|
||||
export type ChannelsStatusParams = Static<typeof ChannelsStatusParamsSchema>;
|
||||
export type ChannelsStatusResult = Static<typeof ChannelsStatusResultSchema>;
|
||||
export type ChannelsLogoutParams = Static<typeof ChannelsLogoutParamsSchema>;
|
||||
export type WebLoginStartParams = Static<typeof WebLoginStartParamsSchema>;
|
||||
export type WebLoginWaitParams = Static<typeof WebLoginWaitParamsSchema>;
|
||||
export type AgentSummary = Static<typeof AgentSummarySchema>;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
waitForEmbeddedPiRunEnd,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import { resolveAgentTimeoutMs } from "../agents/timeout.js";
|
||||
import { normalizeChannelId } from "../channels/plugins/index.js";
|
||||
import type { CliDeps } from "../cli/deps.js";
|
||||
import { agentCommand } from "../commands/agent.js";
|
||||
import type { HealthSummary } from "../commands/health.js";
|
||||
@@ -39,7 +40,6 @@ import {
|
||||
} from "../infra/voicewake.js";
|
||||
import { loadClawdbotPlugins } from "../plugins/loader.js";
|
||||
import { clearCommandLane } from "../process/command-queue.js";
|
||||
import { normalizeProviderId } from "../providers/plugins/index.js";
|
||||
import { normalizeMainKey } from "../routing/session-key.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import {
|
||||
@@ -471,11 +471,11 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
|
||||
label: entry?.label,
|
||||
displayName: entry?.displayName,
|
||||
chatType: entry?.chatType,
|
||||
provider: entry?.provider,
|
||||
channel: entry?.channel,
|
||||
subject: entry?.subject,
|
||||
room: entry?.room,
|
||||
space: entry?.space,
|
||||
lastProvider: entry?.lastProvider,
|
||||
lastChannel: entry?.lastChannel,
|
||||
lastTo: entry?.lastTo,
|
||||
skillsSnapshot: entry?.skillsSnapshot,
|
||||
};
|
||||
@@ -965,7 +965,7 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
|
||||
thinking: p.thinking,
|
||||
deliver: p.deliver,
|
||||
timeout: Math.ceil(timeoutMs / 1000).toString(),
|
||||
messageProvider: `node(${nodeId})`,
|
||||
messageChannel: `node(${nodeId})`,
|
||||
abortSignal: abortController.signal,
|
||||
},
|
||||
defaultRuntime,
|
||||
@@ -1074,7 +1074,7 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
|
||||
reasoningLevel: entry?.reasoningLevel,
|
||||
systemSent: entry?.systemSent,
|
||||
sendPolicy: entry?.sendPolicy,
|
||||
lastProvider: entry?.lastProvider,
|
||||
lastChannel: entry?.lastChannel,
|
||||
lastTo: entry?.lastTo,
|
||||
};
|
||||
if (storePath) {
|
||||
@@ -1095,7 +1095,7 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
|
||||
sessionKey,
|
||||
thinking: "low",
|
||||
deliver: false,
|
||||
messageProvider: "node",
|
||||
messageChannel: "node",
|
||||
},
|
||||
defaultRuntime,
|
||||
ctx.deps,
|
||||
@@ -1130,12 +1130,12 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
|
||||
|
||||
const channelRaw =
|
||||
typeof link?.channel === "string" ? link.channel.trim() : "";
|
||||
const provider = normalizeProviderId(channelRaw) ?? undefined;
|
||||
const channel = normalizeChannelId(channelRaw) ?? undefined;
|
||||
const to =
|
||||
typeof link?.to === "string" && link.to.trim()
|
||||
? link.to.trim()
|
||||
: undefined;
|
||||
const deliver = Boolean(link?.deliver) && Boolean(provider);
|
||||
const deliver = Boolean(link?.deliver) && Boolean(channel);
|
||||
|
||||
const sessionKeyRaw = (link?.sessionKey ?? "").trim();
|
||||
const sessionKey =
|
||||
@@ -1152,7 +1152,7 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
|
||||
reasoningLevel: entry?.reasoningLevel,
|
||||
systemSent: entry?.systemSent,
|
||||
sendPolicy: entry?.sendPolicy,
|
||||
lastProvider: entry?.lastProvider,
|
||||
lastChannel: entry?.lastChannel,
|
||||
lastTo: entry?.lastTo,
|
||||
};
|
||||
if (storePath) {
|
||||
@@ -1167,12 +1167,12 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
|
||||
thinking: link?.thinking ?? undefined,
|
||||
deliver,
|
||||
to,
|
||||
provider,
|
||||
channel,
|
||||
timeout:
|
||||
typeof link?.timeoutSeconds === "number"
|
||||
? link.timeoutSeconds.toString()
|
||||
: undefined,
|
||||
messageProvider: "node",
|
||||
messageChannel: "node",
|
||||
},
|
||||
defaultRuntime,
|
||||
ctx.deps,
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import {
|
||||
type ChannelId,
|
||||
getChannelPlugin,
|
||||
listChannelPlugins,
|
||||
} from "../channels/plugins/index.js";
|
||||
import type { ChannelAccountSnapshot } from "../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import type { createSubsystemLogger } from "../logging.js";
|
||||
import { resolveProviderDefaultAccountId } from "../providers/plugins/helpers.js";
|
||||
import {
|
||||
getProviderPlugin,
|
||||
listProviderPlugins,
|
||||
type ProviderId,
|
||||
} from "../providers/plugins/index.js";
|
||||
import type { ProviderAccountSnapshot } from "../providers/plugins/types.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
|
||||
export type ProviderRuntimeSnapshot = {
|
||||
providers: Partial<Record<ProviderId, ProviderAccountSnapshot>>;
|
||||
providerAccounts: Partial<
|
||||
Record<ProviderId, Record<string, ProviderAccountSnapshot>>
|
||||
export type ChannelRuntimeSnapshot = {
|
||||
channels: Partial<Record<ChannelId, ChannelAccountSnapshot>>;
|
||||
channelAccounts: Partial<
|
||||
Record<ChannelId, Record<string, ChannelAccountSnapshot>>
|
||||
>;
|
||||
};
|
||||
|
||||
type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
|
||||
|
||||
type ProviderRuntimeStore = {
|
||||
type ChannelRuntimeStore = {
|
||||
aborts: Map<string, AbortController>;
|
||||
tasks: Map<string, Promise<unknown>>;
|
||||
runtimes: Map<string, ProviderAccountSnapshot>;
|
||||
runtimes: Map<string, ChannelAccountSnapshot>;
|
||||
};
|
||||
|
||||
function createRuntimeStore(): ProviderRuntimeStore {
|
||||
function createRuntimeStore(): ChannelRuntimeStore {
|
||||
return {
|
||||
aborts: new Map(),
|
||||
tasks: new Map(),
|
||||
@@ -40,83 +40,80 @@ function isAccountEnabled(account: unknown): boolean {
|
||||
return enabled !== false;
|
||||
}
|
||||
|
||||
function resolveDefaultRuntime(
|
||||
providerId: ProviderId,
|
||||
): ProviderAccountSnapshot {
|
||||
const plugin = getProviderPlugin(providerId);
|
||||
function resolveDefaultRuntime(channelId: ChannelId): ChannelAccountSnapshot {
|
||||
const plugin = getChannelPlugin(channelId);
|
||||
return plugin?.status?.defaultRuntime ?? { accountId: DEFAULT_ACCOUNT_ID };
|
||||
}
|
||||
|
||||
function cloneDefaultRuntime(
|
||||
providerId: ProviderId,
|
||||
channelId: ChannelId,
|
||||
accountId: string,
|
||||
): ProviderAccountSnapshot {
|
||||
return { ...resolveDefaultRuntime(providerId), accountId };
|
||||
): ChannelAccountSnapshot {
|
||||
return { ...resolveDefaultRuntime(channelId), accountId };
|
||||
}
|
||||
|
||||
type ProviderManagerOptions = {
|
||||
type ChannelManagerOptions = {
|
||||
loadConfig: () => ClawdbotConfig;
|
||||
providerLogs: Record<ProviderId, SubsystemLogger>;
|
||||
providerRuntimeEnvs: Record<ProviderId, RuntimeEnv>;
|
||||
channelLogs: Record<ChannelId, SubsystemLogger>;
|
||||
channelRuntimeEnvs: Record<ChannelId, RuntimeEnv>;
|
||||
};
|
||||
|
||||
export type ProviderManager = {
|
||||
getRuntimeSnapshot: () => ProviderRuntimeSnapshot;
|
||||
startProviders: () => Promise<void>;
|
||||
startProvider: (provider: ProviderId, accountId?: string) => Promise<void>;
|
||||
stopProvider: (provider: ProviderId, accountId?: string) => Promise<void>;
|
||||
markProviderLoggedOut: (
|
||||
providerId: ProviderId,
|
||||
export type ChannelManager = {
|
||||
getRuntimeSnapshot: () => ChannelRuntimeSnapshot;
|
||||
startChannels: () => Promise<void>;
|
||||
startChannel: (channel: ChannelId, accountId?: string) => Promise<void>;
|
||||
stopChannel: (channel: ChannelId, accountId?: string) => Promise<void>;
|
||||
markChannelLoggedOut: (
|
||||
channelId: ChannelId,
|
||||
cleared: boolean,
|
||||
accountId?: string,
|
||||
) => void;
|
||||
};
|
||||
|
||||
// Provider docking: lifecycle hooks (`plugin.gateway`) flow through this manager.
|
||||
export function createProviderManager(
|
||||
opts: ProviderManagerOptions,
|
||||
): ProviderManager {
|
||||
const { loadConfig, providerLogs, providerRuntimeEnvs } = opts;
|
||||
// Channel docking: lifecycle hooks (`plugin.gateway`) flow through this manager.
|
||||
export function createChannelManager(
|
||||
opts: ChannelManagerOptions,
|
||||
): ChannelManager {
|
||||
const { loadConfig, channelLogs, channelRuntimeEnvs } = opts;
|
||||
|
||||
const providerStores = new Map<ProviderId, ProviderRuntimeStore>();
|
||||
const channelStores = new Map<ChannelId, ChannelRuntimeStore>();
|
||||
|
||||
const getStore = (providerId: ProviderId): ProviderRuntimeStore => {
|
||||
const existing = providerStores.get(providerId);
|
||||
const getStore = (channelId: ChannelId): ChannelRuntimeStore => {
|
||||
const existing = channelStores.get(channelId);
|
||||
if (existing) return existing;
|
||||
const next = createRuntimeStore();
|
||||
providerStores.set(providerId, next);
|
||||
channelStores.set(channelId, next);
|
||||
return next;
|
||||
};
|
||||
|
||||
const getRuntime = (
|
||||
providerId: ProviderId,
|
||||
channelId: ChannelId,
|
||||
accountId: string,
|
||||
): ProviderAccountSnapshot => {
|
||||
const store = getStore(providerId);
|
||||
): ChannelAccountSnapshot => {
|
||||
const store = getStore(channelId);
|
||||
return (
|
||||
store.runtimes.get(accountId) ??
|
||||
cloneDefaultRuntime(providerId, accountId)
|
||||
store.runtimes.get(accountId) ?? cloneDefaultRuntime(channelId, accountId)
|
||||
);
|
||||
};
|
||||
|
||||
const setRuntime = (
|
||||
providerId: ProviderId,
|
||||
channelId: ChannelId,
|
||||
accountId: string,
|
||||
patch: ProviderAccountSnapshot,
|
||||
): ProviderAccountSnapshot => {
|
||||
const store = getStore(providerId);
|
||||
const current = getRuntime(providerId, accountId);
|
||||
patch: ChannelAccountSnapshot,
|
||||
): ChannelAccountSnapshot => {
|
||||
const store = getStore(channelId);
|
||||
const current = getRuntime(channelId, accountId);
|
||||
const next = { ...current, ...patch, accountId };
|
||||
store.runtimes.set(accountId, next);
|
||||
return next;
|
||||
};
|
||||
|
||||
const startProvider = async (providerId: ProviderId, accountId?: string) => {
|
||||
const plugin = getProviderPlugin(providerId);
|
||||
const startChannel = async (channelId: ChannelId, accountId?: string) => {
|
||||
const plugin = getChannelPlugin(channelId);
|
||||
const startAccount = plugin?.gateway?.startAccount;
|
||||
if (!startAccount) return;
|
||||
const cfg = loadConfig();
|
||||
const store = getStore(providerId);
|
||||
const store = getStore(channelId);
|
||||
const accountIds = accountId
|
||||
? [accountId]
|
||||
: plugin.config.listAccountIds(cfg);
|
||||
@@ -130,7 +127,7 @@ export function createProviderManager(
|
||||
? plugin.config.isEnabled(account, cfg)
|
||||
: isAccountEnabled(account);
|
||||
if (!enabled) {
|
||||
setRuntime(providerId, id, {
|
||||
setRuntime(channelId, id, {
|
||||
accountId: id,
|
||||
running: false,
|
||||
lastError:
|
||||
@@ -144,7 +141,7 @@ export function createProviderManager(
|
||||
configured = await plugin.config.isConfigured(account, cfg);
|
||||
}
|
||||
if (!configured) {
|
||||
setRuntime(providerId, id, {
|
||||
setRuntime(channelId, id, {
|
||||
accountId: id,
|
||||
running: false,
|
||||
lastError:
|
||||
@@ -156,34 +153,34 @@ export function createProviderManager(
|
||||
|
||||
const abort = new AbortController();
|
||||
store.aborts.set(id, abort);
|
||||
setRuntime(providerId, id, {
|
||||
setRuntime(channelId, id, {
|
||||
accountId: id,
|
||||
running: true,
|
||||
lastStartAt: Date.now(),
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
const log = providerLogs[providerId];
|
||||
const log = channelLogs[channelId];
|
||||
const task = startAccount({
|
||||
cfg,
|
||||
accountId: id,
|
||||
account,
|
||||
runtime: providerRuntimeEnvs[providerId],
|
||||
runtime: channelRuntimeEnvs[channelId],
|
||||
abortSignal: abort.signal,
|
||||
log,
|
||||
getStatus: () => getRuntime(providerId, id),
|
||||
setStatus: (next) => setRuntime(providerId, id, next),
|
||||
getStatus: () => getRuntime(channelId, id),
|
||||
setStatus: (next) => setRuntime(channelId, id, next),
|
||||
});
|
||||
const tracked = Promise.resolve(task)
|
||||
.catch((err) => {
|
||||
const message = formatErrorMessage(err);
|
||||
setRuntime(providerId, id, { accountId: id, lastError: message });
|
||||
log.error?.(`[${id}] provider exited: ${message}`);
|
||||
setRuntime(channelId, id, { accountId: id, lastError: message });
|
||||
log.error?.(`[${id}] channel exited: ${message}`);
|
||||
})
|
||||
.finally(() => {
|
||||
store.aborts.delete(id);
|
||||
store.tasks.delete(id);
|
||||
setRuntime(providerId, id, {
|
||||
setRuntime(channelId, id, {
|
||||
accountId: id,
|
||||
running: false,
|
||||
lastStopAt: Date.now(),
|
||||
@@ -194,10 +191,10 @@ export function createProviderManager(
|
||||
);
|
||||
};
|
||||
|
||||
const stopProvider = async (providerId: ProviderId, accountId?: string) => {
|
||||
const plugin = getProviderPlugin(providerId);
|
||||
const stopChannel = async (channelId: ChannelId, accountId?: string) => {
|
||||
const plugin = getChannelPlugin(channelId);
|
||||
const cfg = loadConfig();
|
||||
const store = getStore(providerId);
|
||||
const store = getStore(channelId);
|
||||
const knownIds = new Set<string>([
|
||||
...store.aborts.keys(),
|
||||
...store.tasks.keys(),
|
||||
@@ -220,11 +217,11 @@ export function createProviderManager(
|
||||
cfg,
|
||||
accountId: id,
|
||||
account,
|
||||
runtime: providerRuntimeEnvs[providerId],
|
||||
runtime: channelRuntimeEnvs[channelId],
|
||||
abortSignal: abort?.signal ?? new AbortController().signal,
|
||||
log: providerLogs[providerId],
|
||||
getStatus: () => getRuntime(providerId, id),
|
||||
setStatus: (next) => setRuntime(providerId, id, next),
|
||||
log: channelLogs[channelId],
|
||||
getStatus: () => getRuntime(channelId, id),
|
||||
setStatus: (next) => setRuntime(channelId, id, next),
|
||||
});
|
||||
}
|
||||
try {
|
||||
@@ -234,7 +231,7 @@ export function createProviderManager(
|
||||
}
|
||||
store.aborts.delete(id);
|
||||
store.tasks.delete(id);
|
||||
setRuntime(providerId, id, {
|
||||
setRuntime(channelId, id, {
|
||||
accountId: id,
|
||||
running: false,
|
||||
lastStopAt: Date.now(),
|
||||
@@ -243,28 +240,28 @@ export function createProviderManager(
|
||||
);
|
||||
};
|
||||
|
||||
const startProviders = async () => {
|
||||
for (const plugin of listProviderPlugins()) {
|
||||
await startProvider(plugin.id);
|
||||
const startChannels = async () => {
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
await startChannel(plugin.id);
|
||||
}
|
||||
};
|
||||
|
||||
const markProviderLoggedOut = (
|
||||
providerId: ProviderId,
|
||||
const markChannelLoggedOut = (
|
||||
channelId: ChannelId,
|
||||
cleared: boolean,
|
||||
accountId?: string,
|
||||
) => {
|
||||
const plugin = getProviderPlugin(providerId);
|
||||
const plugin = getChannelPlugin(channelId);
|
||||
if (!plugin) return;
|
||||
const cfg = loadConfig();
|
||||
const resolvedId =
|
||||
accountId ??
|
||||
resolveProviderDefaultAccountId({
|
||||
resolveChannelDefaultAccountId({
|
||||
plugin,
|
||||
cfg,
|
||||
});
|
||||
const current = getRuntime(providerId, resolvedId);
|
||||
const next: ProviderAccountSnapshot = {
|
||||
const current = getRuntime(channelId, resolvedId);
|
||||
const next: ChannelAccountSnapshot = {
|
||||
accountId: resolvedId,
|
||||
running: false,
|
||||
lastError: cleared ? "logged out" : current.lastError,
|
||||
@@ -272,22 +269,22 @@ export function createProviderManager(
|
||||
if (typeof current.connected === "boolean") {
|
||||
next.connected = false;
|
||||
}
|
||||
setRuntime(providerId, resolvedId, next);
|
||||
setRuntime(channelId, resolvedId, next);
|
||||
};
|
||||
|
||||
const getRuntimeSnapshot = (): ProviderRuntimeSnapshot => {
|
||||
const getRuntimeSnapshot = (): ChannelRuntimeSnapshot => {
|
||||
const cfg = loadConfig();
|
||||
const providers: ProviderRuntimeSnapshot["providers"] = {};
|
||||
const providerAccounts: ProviderRuntimeSnapshot["providerAccounts"] = {};
|
||||
for (const plugin of listProviderPlugins()) {
|
||||
const channels: ChannelRuntimeSnapshot["channels"] = {};
|
||||
const channelAccounts: ChannelRuntimeSnapshot["channelAccounts"] = {};
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
const store = getStore(plugin.id);
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
const defaultAccountId = resolveProviderDefaultAccountId({
|
||||
const defaultAccountId = resolveChannelDefaultAccountId({
|
||||
plugin,
|
||||
cfg,
|
||||
accountIds,
|
||||
});
|
||||
const accounts: Record<string, ProviderAccountSnapshot> = {};
|
||||
const accounts: Record<string, ChannelAccountSnapshot> = {};
|
||||
for (const id of accountIds) {
|
||||
const account = plugin.config.resolveAccount(cfg, id);
|
||||
const enabled = plugin.config.isEnabled
|
||||
@@ -313,17 +310,17 @@ export function createProviderManager(
|
||||
const defaultAccount =
|
||||
accounts[defaultAccountId] ??
|
||||
cloneDefaultRuntime(plugin.id, defaultAccountId);
|
||||
providers[plugin.id] = defaultAccount;
|
||||
providerAccounts[plugin.id] = accounts;
|
||||
channels[plugin.id] = defaultAccount;
|
||||
channelAccounts[plugin.id] = accounts;
|
||||
}
|
||||
return { providers, providerAccounts };
|
||||
return { channels, channelAccounts };
|
||||
};
|
||||
|
||||
return {
|
||||
getRuntimeSnapshot,
|
||||
startProviders,
|
||||
startProvider,
|
||||
stopProvider,
|
||||
markProviderLoggedOut,
|
||||
startChannels,
|
||||
startChannel,
|
||||
stopChannel,
|
||||
markChannelLoggedOut,
|
||||
};
|
||||
}
|
||||
@@ -11,15 +11,15 @@ import type { createSubsystemLogger } from "../logging.js";
|
||||
import { handleControlUiHttpRequest } from "./control-ui.js";
|
||||
import {
|
||||
extractHookToken,
|
||||
HOOK_PROVIDER_ERROR,
|
||||
type HookMessageProvider,
|
||||
HOOK_CHANNEL_ERROR,
|
||||
type HookMessageChannel,
|
||||
type HooksConfigResolved,
|
||||
normalizeAgentPayload,
|
||||
normalizeHookHeaders,
|
||||
normalizeWakePayload,
|
||||
readJsonBody,
|
||||
resolveHookChannel,
|
||||
resolveHookDeliver,
|
||||
resolveHookProvider,
|
||||
} from "./hooks.js";
|
||||
import { applyHookMappings } from "./hooks-mapping.js";
|
||||
import { handleOpenAiHttpRequest } from "./openai-http.js";
|
||||
@@ -37,7 +37,7 @@ type HookDispatchers = {
|
||||
wakeMode: "now" | "next-heartbeat";
|
||||
sessionKey: string;
|
||||
deliver: boolean;
|
||||
provider: HookMessageProvider;
|
||||
channel: HookMessageChannel;
|
||||
to?: string;
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
@@ -168,9 +168,9 @@ export function createHooksRequestHandler(
|
||||
sendJson(res, 200, { ok: true, mode: mapped.action.mode });
|
||||
return true;
|
||||
}
|
||||
const provider = resolveHookProvider(mapped.action.provider);
|
||||
if (!provider) {
|
||||
sendJson(res, 400, { ok: false, error: HOOK_PROVIDER_ERROR });
|
||||
const channel = resolveHookChannel(mapped.action.channel);
|
||||
if (!channel) {
|
||||
sendJson(res, 400, { ok: false, error: HOOK_CHANNEL_ERROR });
|
||||
return true;
|
||||
}
|
||||
const runId = dispatchAgentHook({
|
||||
@@ -179,7 +179,7 @@ export function createHooksRequestHandler(
|
||||
wakeMode: mapped.action.wakeMode,
|
||||
sessionKey: mapped.action.sessionKey ?? "",
|
||||
deliver: resolveHookDeliver(mapped.action.deliver),
|
||||
provider,
|
||||
channel,
|
||||
to: mapped.action.to,
|
||||
model: mapped.action.model,
|
||||
thinking: mapped.action.thinking,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ErrorCodes, errorShape } from "./protocol/index.js";
|
||||
import { agentHandlers } from "./server-methods/agent.js";
|
||||
import { agentsHandlers } from "./server-methods/agents.js";
|
||||
import { channelsHandlers } from "./server-methods/channels.js";
|
||||
import { chatHandlers } from "./server-methods/chat.js";
|
||||
import { configHandlers } from "./server-methods/config.js";
|
||||
import { connectHandlers } from "./server-methods/connect.js";
|
||||
@@ -9,7 +10,6 @@ import { healthHandlers } from "./server-methods/health.js";
|
||||
import { logsHandlers } from "./server-methods/logs.js";
|
||||
import { modelsHandlers } from "./server-methods/models.js";
|
||||
import { nodeHandlers } from "./server-methods/nodes.js";
|
||||
import { providersHandlers } from "./server-methods/providers.js";
|
||||
import { sendHandlers } from "./server-methods/send.js";
|
||||
import { sessionsHandlers } from "./server-methods/sessions.js";
|
||||
import { skillsHandlers } from "./server-methods/skills.js";
|
||||
@@ -30,7 +30,7 @@ export const coreGatewayHandlers: GatewayRequestHandlers = {
|
||||
...logsHandlers,
|
||||
...voicewakeHandlers,
|
||||
...healthHandlers,
|
||||
...providersHandlers,
|
||||
...channelsHandlers,
|
||||
...chatHandlers,
|
||||
...cronHandlers,
|
||||
...webHandlers,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js";
|
||||
import { agentCommand } from "../../commands/agent.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import {
|
||||
@@ -10,15 +10,14 @@ import {
|
||||
} from "../../config/sessions.js";
|
||||
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||
import { resolveOutboundTarget } from "../../infra/outbound/targets.js";
|
||||
import { DEFAULT_CHAT_PROVIDER } from "../../providers/registry.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
||||
import {
|
||||
INTERNAL_MESSAGE_PROVIDER,
|
||||
isDeliverableMessageProvider,
|
||||
isGatewayMessageProvider,
|
||||
normalizeMessageProvider,
|
||||
} from "../../utils/message-provider.js";
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
isDeliverableMessageChannel,
|
||||
isGatewayMessageChannel,
|
||||
normalizeMessageChannel,
|
||||
} from "../../utils/message-channel.js";
|
||||
import { parseMessageWithAttachments } from "../chat-attachments.js";
|
||||
import {
|
||||
type AgentWaitParams,
|
||||
@@ -60,7 +59,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
fileName?: string;
|
||||
content?: unknown;
|
||||
}>;
|
||||
provider?: string;
|
||||
channel?: string;
|
||||
lane?: string;
|
||||
extraSystemPrompt?: string;
|
||||
idempotencyKey: string;
|
||||
@@ -115,21 +114,21 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const rawProvider =
|
||||
typeof request.provider === "string" ? request.provider.trim() : "";
|
||||
if (rawProvider) {
|
||||
const normalized = normalizeMessageProvider(rawProvider);
|
||||
const rawChannel =
|
||||
typeof request.channel === "string" ? request.channel.trim() : "";
|
||||
if (rawChannel) {
|
||||
const normalized = normalizeMessageChannel(rawChannel);
|
||||
if (
|
||||
normalized &&
|
||||
normalized !== "last" &&
|
||||
!isGatewayMessageProvider(normalized)
|
||||
!isGatewayMessageChannel(normalized)
|
||||
) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid agent params: unknown provider: ${normalized}`,
|
||||
`invalid agent params: unknown channel: ${normalized}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -162,7 +161,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
systemSent: entry?.systemSent,
|
||||
sendPolicy: entry?.sendPolicy,
|
||||
skillsSnapshot: entry?.skillsSnapshot,
|
||||
lastProvider: entry?.lastProvider,
|
||||
lastChannel: entry?.lastChannel,
|
||||
lastTo: entry?.lastTo,
|
||||
modelOverride: entry?.modelOverride,
|
||||
providerOverride: entry?.providerOverride,
|
||||
@@ -174,7 +173,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
cfg,
|
||||
entry,
|
||||
sessionKey: requestedSessionKey,
|
||||
provider: entry?.provider,
|
||||
channel: entry?.channel,
|
||||
chatType: entry?.chatType,
|
||||
});
|
||||
if (sendPolicy === "deny") {
|
||||
@@ -213,10 +212,9 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
|
||||
const runId = idem;
|
||||
|
||||
const requestedProvider =
|
||||
normalizeMessageProvider(request.provider) ?? "last";
|
||||
const requestedChannel = normalizeMessageChannel(request.channel) ?? "last";
|
||||
|
||||
const lastProvider = sessionEntry?.lastProvider;
|
||||
const lastChannel = sessionEntry?.lastChannel;
|
||||
const lastTo =
|
||||
typeof sessionEntry?.lastTo === "string"
|
||||
? sessionEntry.lastTo.trim()
|
||||
@@ -224,24 +222,22 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
|
||||
const wantsDelivery = request.deliver === true;
|
||||
|
||||
const resolvedProvider = (() => {
|
||||
if (requestedProvider === "last") {
|
||||
const resolvedChannel = (() => {
|
||||
if (requestedChannel === "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 !== INTERNAL_MESSAGE_PROVIDER) {
|
||||
return lastProvider;
|
||||
if (lastChannel && lastChannel !== INTERNAL_MESSAGE_CHANNEL) {
|
||||
return lastChannel;
|
||||
}
|
||||
return wantsDelivery
|
||||
? DEFAULT_CHAT_PROVIDER
|
||||
: INTERNAL_MESSAGE_PROVIDER;
|
||||
return wantsDelivery ? DEFAULT_CHAT_CHANNEL : INTERNAL_MESSAGE_CHANNEL;
|
||||
}
|
||||
|
||||
if (isGatewayMessageProvider(requestedProvider)) return requestedProvider;
|
||||
if (isGatewayMessageChannel(requestedChannel)) return requestedChannel;
|
||||
|
||||
if (lastProvider && lastProvider !== INTERNAL_MESSAGE_PROVIDER) {
|
||||
return lastProvider;
|
||||
if (lastChannel && lastChannel !== INTERNAL_MESSAGE_CHANNEL) {
|
||||
return lastChannel;
|
||||
}
|
||||
return wantsDelivery ? DEFAULT_CHAT_PROVIDER : INTERNAL_MESSAGE_PROVIDER;
|
||||
return wantsDelivery ? DEFAULT_CHAT_CHANNEL : INTERNAL_MESSAGE_CHANNEL;
|
||||
})();
|
||||
|
||||
const explicitTo =
|
||||
@@ -250,18 +246,18 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
: undefined;
|
||||
const deliveryTargetMode = explicitTo
|
||||
? "explicit"
|
||||
: isDeliverableMessageProvider(resolvedProvider)
|
||||
: isDeliverableMessageChannel(resolvedChannel)
|
||||
? "implicit"
|
||||
: undefined;
|
||||
let resolvedTo =
|
||||
explicitTo ||
|
||||
(isDeliverableMessageProvider(resolvedProvider)
|
||||
(isDeliverableMessageChannel(resolvedChannel)
|
||||
? lastTo || undefined
|
||||
: undefined);
|
||||
if (!resolvedTo && isDeliverableMessageProvider(resolvedProvider)) {
|
||||
if (!resolvedTo && isDeliverableMessageChannel(resolvedChannel)) {
|
||||
const cfg = cfgForAgent ?? loadConfig();
|
||||
const fallback = resolveOutboundTarget({
|
||||
provider: resolvedProvider,
|
||||
channel: resolvedChannel,
|
||||
cfg,
|
||||
accountId: sessionEntry?.lastAccountId ?? undefined,
|
||||
mode: "implicit",
|
||||
@@ -272,8 +268,7 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
}
|
||||
|
||||
const deliver =
|
||||
request.deliver === true &&
|
||||
resolvedProvider !== INTERNAL_MESSAGE_PROVIDER;
|
||||
request.deliver === true && resolvedChannel !== INTERNAL_MESSAGE_CHANNEL;
|
||||
|
||||
const accepted = {
|
||||
runId,
|
||||
@@ -298,10 +293,10 @@ export const agentHandlers: GatewayRequestHandlers = {
|
||||
thinking: request.thinking,
|
||||
deliver,
|
||||
deliveryTargetMode,
|
||||
provider: resolvedProvider,
|
||||
channel: resolvedChannel,
|
||||
timeout: request.timeout?.toString(),
|
||||
bestEffortDeliver,
|
||||
messageProvider: resolvedProvider,
|
||||
messageChannel: resolvedChannel,
|
||||
runId,
|
||||
lane: request.lane,
|
||||
extraSystemPrompt: request.extraSystemPrompt,
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js";
|
||||
import {
|
||||
type ChannelId,
|
||||
getChannelPlugin,
|
||||
listChannelPlugins,
|
||||
normalizeChannelId,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import { buildChannelAccountSnapshot } from "../../channels/plugins/status.js";
|
||||
import type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelPlugin,
|
||||
} from "../../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { loadConfig, readConfigFileSnapshot } from "../../config/config.js";
|
||||
import { getProviderActivity } from "../../infra/provider-activity.js";
|
||||
import { resolveProviderDefaultAccountId } from "../../providers/plugins/helpers.js";
|
||||
import {
|
||||
getProviderPlugin,
|
||||
listProviderPlugins,
|
||||
normalizeProviderId,
|
||||
type ProviderId,
|
||||
} from "../../providers/plugins/index.js";
|
||||
import { buildProviderAccountSnapshot } from "../../providers/plugins/status.js";
|
||||
import type {
|
||||
ProviderAccountSnapshot,
|
||||
ProviderPlugin,
|
||||
} from "../../providers/plugins/types.js";
|
||||
import { getChannelActivity } from "../../infra/channel-activity.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
formatValidationErrors,
|
||||
validateProvidersLogoutParams,
|
||||
validateProvidersStatusParams,
|
||||
validateChannelsLogoutParams,
|
||||
validateChannelsStatusParams,
|
||||
} from "../protocol/index.js";
|
||||
import { formatForLog } from "../ws-log.js";
|
||||
import type { GatewayRequestContext, GatewayRequestHandlers } from "./types.js";
|
||||
|
||||
type ProviderLogoutPayload = {
|
||||
provider: ProviderId;
|
||||
type ChannelLogoutPayload = {
|
||||
channel: ChannelId;
|
||||
accountId: string;
|
||||
cleared: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export async function logoutProviderAccount(params: {
|
||||
providerId: ProviderId;
|
||||
export async function logoutChannelAccount(params: {
|
||||
channelId: ChannelId;
|
||||
accountId?: string | null;
|
||||
cfg: ClawdbotConfig;
|
||||
context: GatewayRequestContext;
|
||||
plugin: ProviderPlugin;
|
||||
}): Promise<ProviderLogoutPayload> {
|
||||
plugin: ChannelPlugin;
|
||||
}): Promise<ChannelLogoutPayload> {
|
||||
const resolvedAccountId =
|
||||
params.accountId?.trim() ||
|
||||
params.plugin.config.defaultAccountId?.(params.cfg) ||
|
||||
@@ -48,7 +48,7 @@ export async function logoutProviderAccount(params: {
|
||||
params.cfg,
|
||||
resolvedAccountId,
|
||||
);
|
||||
await params.context.stopProvider(params.providerId, resolvedAccountId);
|
||||
await params.context.stopChannel(params.channelId, resolvedAccountId);
|
||||
const result = await params.plugin.gateway?.logoutAccount?.({
|
||||
cfg: params.cfg,
|
||||
accountId: resolvedAccountId,
|
||||
@@ -56,35 +56,35 @@ export async function logoutProviderAccount(params: {
|
||||
runtime: defaultRuntime,
|
||||
});
|
||||
if (!result) {
|
||||
throw new Error(`Provider ${params.providerId} does not support logout`);
|
||||
throw new Error(`Channel ${params.channelId} does not support logout`);
|
||||
}
|
||||
const cleared = Boolean(result.cleared);
|
||||
const loggedOut =
|
||||
typeof result.loggedOut === "boolean" ? result.loggedOut : cleared;
|
||||
if (loggedOut) {
|
||||
params.context.markProviderLoggedOut(
|
||||
params.providerId,
|
||||
params.context.markChannelLoggedOut(
|
||||
params.channelId,
|
||||
true,
|
||||
resolvedAccountId,
|
||||
);
|
||||
}
|
||||
return {
|
||||
provider: params.providerId,
|
||||
channel: params.channelId,
|
||||
accountId: resolvedAccountId,
|
||||
...result,
|
||||
cleared,
|
||||
};
|
||||
}
|
||||
|
||||
export const providersHandlers: GatewayRequestHandlers = {
|
||||
"providers.status": async ({ params, respond, context }) => {
|
||||
if (!validateProvidersStatusParams(params)) {
|
||||
export const channelsHandlers: GatewayRequestHandlers = {
|
||||
"channels.status": async ({ params, respond, context }) => {
|
||||
if (!validateChannelsStatusParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid providers.status params: ${formatValidationErrors(validateProvidersStatusParams.errors)}`,
|
||||
`invalid channels.status params: ${formatValidationErrors(validateChannelsStatusParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -95,18 +95,18 @@ export const providersHandlers: GatewayRequestHandlers = {
|
||||
typeof timeoutMsRaw === "number" ? Math.max(1000, timeoutMsRaw) : 10_000;
|
||||
const cfg = loadConfig();
|
||||
const runtime = context.getRuntimeSnapshot();
|
||||
const plugins = listProviderPlugins();
|
||||
const pluginMap = new Map<ProviderId, ProviderPlugin>(
|
||||
const plugins = listChannelPlugins();
|
||||
const pluginMap = new Map<ChannelId, ChannelPlugin>(
|
||||
plugins.map((plugin) => [plugin.id, plugin]),
|
||||
);
|
||||
|
||||
const resolveRuntimeSnapshot = (
|
||||
providerId: ProviderId,
|
||||
channelId: ChannelId,
|
||||
accountId: string,
|
||||
defaultAccountId: string,
|
||||
): ProviderAccountSnapshot | undefined => {
|
||||
const accounts = runtime.providerAccounts[providerId];
|
||||
const defaultRuntime = runtime.providers[providerId];
|
||||
): ChannelAccountSnapshot | undefined => {
|
||||
const accounts = runtime.channelAccounts[channelId];
|
||||
const defaultRuntime = runtime.channels[channelId];
|
||||
const raw =
|
||||
accounts?.[accountId] ??
|
||||
(accountId === defaultAccountId ? defaultRuntime : undefined);
|
||||
@@ -114,30 +114,30 @@ export const providersHandlers: GatewayRequestHandlers = {
|
||||
return raw;
|
||||
};
|
||||
|
||||
const isAccountEnabled = (plugin: ProviderPlugin, account: unknown) =>
|
||||
const isAccountEnabled = (plugin: ChannelPlugin, account: unknown) =>
|
||||
plugin.config.isEnabled
|
||||
? plugin.config.isEnabled(account, cfg)
|
||||
: !account ||
|
||||
typeof account !== "object" ||
|
||||
(account as { enabled?: boolean }).enabled !== false;
|
||||
|
||||
const buildProviderAccounts = async (providerId: ProviderId) => {
|
||||
const plugin = pluginMap.get(providerId);
|
||||
const buildChannelAccounts = async (channelId: ChannelId) => {
|
||||
const plugin = pluginMap.get(channelId);
|
||||
if (!plugin) {
|
||||
return {
|
||||
accounts: [] as ProviderAccountSnapshot[],
|
||||
accounts: [] as ChannelAccountSnapshot[],
|
||||
defaultAccountId: DEFAULT_ACCOUNT_ID,
|
||||
defaultAccount: undefined as ProviderAccountSnapshot | undefined,
|
||||
defaultAccount: undefined as ChannelAccountSnapshot | undefined,
|
||||
resolvedAccounts: {} as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
const accountIds = plugin.config.listAccountIds(cfg);
|
||||
const defaultAccountId = resolveProviderDefaultAccountId({
|
||||
const defaultAccountId = resolveChannelDefaultAccountId({
|
||||
plugin,
|
||||
cfg,
|
||||
accountIds,
|
||||
});
|
||||
const accounts: ProviderAccountSnapshot[] = [];
|
||||
const accounts: ChannelAccountSnapshot[] = [];
|
||||
const resolvedAccounts: Record<string, unknown> = {};
|
||||
for (const accountId of accountIds) {
|
||||
const account = plugin.config.resolveAccount(cfg, accountId);
|
||||
@@ -175,11 +175,11 @@ export const providersHandlers: GatewayRequestHandlers = {
|
||||
}
|
||||
}
|
||||
const runtimeSnapshot = resolveRuntimeSnapshot(
|
||||
providerId,
|
||||
channelId,
|
||||
accountId,
|
||||
defaultAccountId,
|
||||
);
|
||||
const snapshot = await buildProviderAccountSnapshot({
|
||||
const snapshot = await buildChannelAccountSnapshot({
|
||||
plugin,
|
||||
cfg,
|
||||
accountId,
|
||||
@@ -188,8 +188,8 @@ export const providersHandlers: GatewayRequestHandlers = {
|
||||
audit: auditResult,
|
||||
});
|
||||
if (lastProbeAt) snapshot.lastProbeAt = lastProbeAt;
|
||||
const activity = getProviderActivity({
|
||||
provider: providerId as never,
|
||||
const activity = getChannelActivity({
|
||||
channel: channelId as never,
|
||||
accountId,
|
||||
});
|
||||
if (snapshot.lastInboundAt == null) {
|
||||
@@ -208,28 +208,28 @@ export const providersHandlers: GatewayRequestHandlers = {
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
ts: Date.now(),
|
||||
providerOrder: plugins.map((plugin) => plugin.id),
|
||||
providerLabels: Object.fromEntries(
|
||||
channelOrder: plugins.map((plugin) => plugin.id),
|
||||
channelLabels: Object.fromEntries(
|
||||
plugins.map((plugin) => [plugin.id, plugin.meta.label]),
|
||||
),
|
||||
providers: {} as Record<string, unknown>,
|
||||
providerAccounts: {} as Record<string, unknown>,
|
||||
providerDefaultAccountId: {} as Record<string, unknown>,
|
||||
channels: {} as Record<string, unknown>,
|
||||
channelAccounts: {} as Record<string, unknown>,
|
||||
channelDefaultAccountId: {} as Record<string, unknown>,
|
||||
};
|
||||
const providersMap = payload.providers as Record<string, unknown>;
|
||||
const accountsMap = payload.providerAccounts as Record<string, unknown>;
|
||||
const defaultAccountIdMap = payload.providerDefaultAccountId as Record<
|
||||
const channelsMap = payload.channels as Record<string, unknown>;
|
||||
const accountsMap = payload.channelAccounts as Record<string, unknown>;
|
||||
const defaultAccountIdMap = payload.channelDefaultAccountId as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
for (const plugin of plugins) {
|
||||
const { accounts, defaultAccountId, defaultAccount, resolvedAccounts } =
|
||||
await buildProviderAccounts(plugin.id);
|
||||
await buildChannelAccounts(plugin.id);
|
||||
const fallbackAccount =
|
||||
resolvedAccounts[defaultAccountId] ??
|
||||
plugin.config.resolveAccount(cfg, defaultAccountId);
|
||||
const summary = plugin.status?.buildProviderSummary
|
||||
? await plugin.status.buildProviderSummary({
|
||||
const summary = plugin.status?.buildChannelSummary
|
||||
? await plugin.status.buildChannelSummary({
|
||||
account: fallbackAccount,
|
||||
cfg,
|
||||
defaultAccountId,
|
||||
@@ -237,52 +237,52 @@ export const providersHandlers: GatewayRequestHandlers = {
|
||||
defaultAccount ??
|
||||
({
|
||||
accountId: defaultAccountId,
|
||||
} as ProviderAccountSnapshot),
|
||||
} as ChannelAccountSnapshot),
|
||||
})
|
||||
: {
|
||||
configured: defaultAccount?.configured ?? false,
|
||||
};
|
||||
providersMap[plugin.id] = summary;
|
||||
channelsMap[plugin.id] = summary;
|
||||
accountsMap[plugin.id] = accounts;
|
||||
defaultAccountIdMap[plugin.id] = defaultAccountId;
|
||||
}
|
||||
|
||||
respond(true, payload, undefined);
|
||||
},
|
||||
"providers.logout": async ({ params, respond, context }) => {
|
||||
if (!validateProvidersLogoutParams(params)) {
|
||||
"channels.logout": async ({ params, respond, context }) => {
|
||||
if (!validateChannelsLogoutParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid providers.logout params: ${formatValidationErrors(validateProvidersLogoutParams.errors)}`,
|
||||
`invalid channels.logout params: ${formatValidationErrors(validateChannelsLogoutParams.errors)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const rawProvider = (params as { provider?: unknown }).provider;
|
||||
const providerId =
|
||||
typeof rawProvider === "string" ? normalizeProviderId(rawProvider) : null;
|
||||
if (!providerId) {
|
||||
const rawChannel = (params as { channel?: unknown }).channel;
|
||||
const channelId =
|
||||
typeof rawChannel === "string" ? normalizeChannelId(rawChannel) : null;
|
||||
if (!channelId) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
"invalid providers.logout provider",
|
||||
"invalid channels.logout channel",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const plugin = getProviderPlugin(providerId);
|
||||
const plugin = getChannelPlugin(channelId);
|
||||
if (!plugin?.gateway?.logoutAccount) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`provider ${providerId} does not support logout`,
|
||||
`channel ${channelId} does not support logout`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -303,8 +303,8 @@ export const providersHandlers: GatewayRequestHandlers = {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload = await logoutProviderAccount({
|
||||
providerId,
|
||||
const payload = await logoutChannelAccount({
|
||||
channelId,
|
||||
accountId,
|
||||
cfg: snapshot.config ?? {},
|
||||
context,
|
||||
@@ -7,7 +7,7 @@ import { mergeSessionEntry, saveSessionStore } from "../../config/sessions.js";
|
||||
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
||||
import { INTERNAL_MESSAGE_PROVIDER } from "../../utils/message-provider.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
||||
import {
|
||||
abortChatRunById,
|
||||
abortChatRunsForSessionKey,
|
||||
@@ -242,7 +242,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
cfg,
|
||||
entry,
|
||||
sessionKey: p.sessionKey,
|
||||
provider: entry?.provider,
|
||||
channel: entry?.channel,
|
||||
chatType: entry?.chatType,
|
||||
});
|
||||
if (sendPolicy === "deny") {
|
||||
@@ -331,7 +331,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
thinking: p.thinking,
|
||||
deliver: p.deliver,
|
||||
timeout: Math.ceil(timeoutMs / 1000).toString(),
|
||||
messageProvider: INTERNAL_MESSAGE_PROVIDER,
|
||||
messageChannel: INTERNAL_MESSAGE_CHANNEL,
|
||||
abortSignal: abortController.signal,
|
||||
},
|
||||
defaultRuntime,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {
|
||||
getChannelPlugin,
|
||||
normalizeChannelId,
|
||||
} from "../../channels/plugins/index.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.js";
|
||||
import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import { deliverOutboundPayloads } from "../../infra/outbound/deliver.js";
|
||||
import type { OutboundProvider } from "../../infra/outbound/targets.js";
|
||||
import type { OutboundChannel } from "../../infra/outbound/targets.js";
|
||||
import { resolveOutboundTarget } from "../../infra/outbound/targets.js";
|
||||
import { normalizePollInput } from "../../polls.js";
|
||||
import {
|
||||
getProviderPlugin,
|
||||
normalizeProviderId,
|
||||
} from "../../providers/plugins/index.js";
|
||||
import type { ProviderId } from "../../providers/plugins/types.js";
|
||||
import { DEFAULT_CHAT_PROVIDER } from "../../providers/registry.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
@@ -38,7 +38,7 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
message: string;
|
||||
mediaUrl?: string;
|
||||
gifPlayback?: boolean;
|
||||
provider?: string;
|
||||
channel?: string;
|
||||
accountId?: string;
|
||||
idempotencyKey: string;
|
||||
};
|
||||
@@ -52,44 +52,44 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
}
|
||||
const to = request.to.trim();
|
||||
const message = request.message.trim();
|
||||
const providerInput =
|
||||
typeof request.provider === "string" ? request.provider : undefined;
|
||||
const normalizedProvider = providerInput
|
||||
? normalizeProviderId(providerInput)
|
||||
const channelInput =
|
||||
typeof request.channel === "string" ? request.channel : undefined;
|
||||
const normalizedChannel = channelInput
|
||||
? normalizeChannelId(channelInput)
|
||||
: null;
|
||||
if (providerInput && !normalizedProvider) {
|
||||
if (channelInput && !normalizedChannel) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`unsupported provider: ${providerInput}`,
|
||||
`unsupported channel: ${channelInput}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const provider = normalizedProvider ?? DEFAULT_CHAT_PROVIDER;
|
||||
const channel = normalizedChannel ?? DEFAULT_CHAT_CHANNEL;
|
||||
const accountId =
|
||||
typeof request.accountId === "string" && request.accountId.trim().length
|
||||
? request.accountId.trim()
|
||||
: undefined;
|
||||
try {
|
||||
const outboundProvider = provider as Exclude<OutboundProvider, "none">;
|
||||
const plugin = getProviderPlugin(provider as ProviderId);
|
||||
const outboundChannel = channel as Exclude<OutboundChannel, "none">;
|
||||
const plugin = getChannelPlugin(channel as ChannelId);
|
||||
if (!plugin) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`unsupported provider: ${provider}`,
|
||||
`unsupported channel: ${channel}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const cfg = loadConfig();
|
||||
const resolved = resolveOutboundTarget({
|
||||
provider: outboundProvider,
|
||||
channel: outboundChannel,
|
||||
to,
|
||||
cfg,
|
||||
accountId,
|
||||
@@ -105,7 +105,7 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
}
|
||||
const results = await deliverOutboundPayloads({
|
||||
cfg,
|
||||
provider: outboundProvider,
|
||||
channel: outboundChannel,
|
||||
to: resolved.to,
|
||||
accountId,
|
||||
payloads: [{ text: message, mediaUrl: request.mediaUrl }],
|
||||
@@ -118,7 +118,7 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
const payload: Record<string, unknown> = {
|
||||
runId: idem,
|
||||
messageId: result.messageId,
|
||||
provider,
|
||||
channel,
|
||||
};
|
||||
if ("chatId" in result) payload.chatId = result.chatId;
|
||||
if ("channelId" in result) payload.channelId = result.channelId;
|
||||
@@ -131,7 +131,7 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
ok: true,
|
||||
payload,
|
||||
});
|
||||
respond(true, payload, undefined, { provider });
|
||||
respond(true, payload, undefined, { channel });
|
||||
} catch (err) {
|
||||
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
|
||||
context.dedupe.set(`send:${idem}`, {
|
||||
@@ -139,10 +139,7 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
ok: false,
|
||||
error,
|
||||
});
|
||||
respond(false, undefined, error, {
|
||||
provider,
|
||||
error: formatForLog(err),
|
||||
});
|
||||
respond(false, undefined, error, { channel, error: formatForLog(err) });
|
||||
}
|
||||
},
|
||||
poll: async ({ params, respond, context }) => {
|
||||
@@ -164,7 +161,7 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
options: string[];
|
||||
maxSelections?: number;
|
||||
durationHours?: number;
|
||||
provider?: string;
|
||||
channel?: string;
|
||||
accountId?: string;
|
||||
idempotencyKey: string;
|
||||
};
|
||||
@@ -177,23 +174,23 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
return;
|
||||
}
|
||||
const to = request.to.trim();
|
||||
const providerInput =
|
||||
typeof request.provider === "string" ? request.provider : undefined;
|
||||
const normalizedProvider = providerInput
|
||||
? normalizeProviderId(providerInput)
|
||||
const channelInput =
|
||||
typeof request.channel === "string" ? request.channel : undefined;
|
||||
const normalizedChannel = channelInput
|
||||
? normalizeChannelId(channelInput)
|
||||
: null;
|
||||
if (providerInput && !normalizedProvider) {
|
||||
if (channelInput && !normalizedChannel) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`unsupported poll provider: ${providerInput}`,
|
||||
`unsupported poll channel: ${channelInput}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const provider = normalizedProvider ?? DEFAULT_CHAT_PROVIDER;
|
||||
const channel = normalizedChannel ?? DEFAULT_CHAT_CHANNEL;
|
||||
const poll = {
|
||||
question: request.question,
|
||||
options: request.options,
|
||||
@@ -205,7 +202,7 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
? request.accountId.trim()
|
||||
: undefined;
|
||||
try {
|
||||
const plugin = getProviderPlugin(provider as ProviderId);
|
||||
const plugin = getChannelPlugin(channel as ChannelId);
|
||||
const outbound = plugin?.outbound;
|
||||
if (!outbound?.sendPoll) {
|
||||
respond(
|
||||
@@ -213,14 +210,14 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`unsupported poll provider: ${provider}`,
|
||||
`unsupported poll channel: ${channel}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const cfg = loadConfig();
|
||||
const resolved = resolveOutboundTarget({
|
||||
provider: provider as Exclude<OutboundProvider, "none">,
|
||||
channel: channel as Exclude<OutboundChannel, "none">,
|
||||
to,
|
||||
cfg,
|
||||
accountId,
|
||||
@@ -246,7 +243,7 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
const payload: Record<string, unknown> = {
|
||||
runId: idem,
|
||||
messageId: result.messageId,
|
||||
provider,
|
||||
channel,
|
||||
};
|
||||
if (result.toJid) payload.toJid = result.toJid;
|
||||
if (result.channelId) payload.channelId = result.channelId;
|
||||
@@ -257,7 +254,7 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
ok: true,
|
||||
payload,
|
||||
});
|
||||
respond(true, payload, undefined, { provider });
|
||||
respond(true, payload, undefined, { channel });
|
||||
} catch (err) {
|
||||
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
|
||||
context.dedupe.set(`poll:${idem}`, {
|
||||
@@ -266,7 +263,7 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
error,
|
||||
});
|
||||
respond(false, undefined, error, {
|
||||
provider,
|
||||
channel,
|
||||
error: formatForLog(err),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
||||
contextTokens: entry?.contextTokens,
|
||||
sendPolicy: entry?.sendPolicy,
|
||||
label: entry?.label,
|
||||
lastProvider: entry?.lastProvider,
|
||||
lastChannel: entry?.lastChannel,
|
||||
lastTo: entry?.lastTo,
|
||||
skillsSnapshot: entry?.skillsSnapshot,
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
ErrorShape,
|
||||
RequestFrame,
|
||||
} from "../protocol/index.js";
|
||||
import type { ProviderRuntimeSnapshot } from "../server-providers.js";
|
||||
import type { ChannelRuntimeSnapshot } from "../server-channels.js";
|
||||
import type { DedupeEntry } from "../server-shared.js";
|
||||
|
||||
export type GatewayClient = {
|
||||
@@ -68,17 +68,17 @@ export type GatewayRequestContext = {
|
||||
wizardSessions: Map<string, WizardSession>;
|
||||
findRunningWizard: () => string | null;
|
||||
purgeWizardSession: (id: string) => void;
|
||||
getRuntimeSnapshot: () => ProviderRuntimeSnapshot;
|
||||
startProvider: (
|
||||
provider: import("../../providers/plugins/types.js").ProviderId,
|
||||
getRuntimeSnapshot: () => ChannelRuntimeSnapshot;
|
||||
startChannel: (
|
||||
channel: import("../../channels/plugins/types.js").ChannelId,
|
||||
accountId?: string,
|
||||
) => Promise<void>;
|
||||
stopProvider: (
|
||||
provider: import("../../providers/plugins/types.js").ProviderId,
|
||||
stopChannel: (
|
||||
channel: import("../../channels/plugins/types.js").ChannelId,
|
||||
accountId?: string,
|
||||
) => Promise<void>;
|
||||
markProviderLoggedOut: (
|
||||
providerId: import("../../providers/plugins/types.js").ProviderId,
|
||||
markChannelLoggedOut: (
|
||||
channelId: import("../../channels/plugins/types.js").ChannelId,
|
||||
cleared: boolean,
|
||||
accountId?: string,
|
||||
) => void;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { listProviderPlugins } from "../../providers/plugins/index.js";
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
@@ -12,7 +12,7 @@ import type { GatewayRequestHandlers } from "./types.js";
|
||||
const WEB_LOGIN_METHODS = new Set(["web.login.start", "web.login.wait"]);
|
||||
|
||||
const resolveWebLoginProvider = () =>
|
||||
listProviderPlugins().find((plugin) =>
|
||||
listChannelPlugins().find((plugin) =>
|
||||
(plugin.gatewayMethods ?? []).some((method) =>
|
||||
WEB_LOGIN_METHODS.has(method),
|
||||
),
|
||||
@@ -48,7 +48,7 @@ export const webHandlers: GatewayRequestHandlers = {
|
||||
);
|
||||
return;
|
||||
}
|
||||
await context.stopProvider(provider.id, accountId);
|
||||
await context.stopChannel(provider.id, accountId);
|
||||
if (!provider.gateway?.loginWithQrStart) {
|
||||
respond(
|
||||
false,
|
||||
@@ -126,7 +126,7 @@ export const webHandlers: GatewayRequestHandlers = {
|
||||
accountId,
|
||||
});
|
||||
if (result.connected) {
|
||||
await context.startProvider(provider.id, accountId);
|
||||
await context.startChannel(provider.id, accountId);
|
||||
}
|
||||
respond(true, result, undefined);
|
||||
} catch (err) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
import {
|
||||
agentCommand,
|
||||
connectOk,
|
||||
@@ -28,9 +28,9 @@ installGatewayTestHooks();
|
||||
const BASE_IMAGE_PNG =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X3mIAAAAASUVORK5CYII=";
|
||||
|
||||
function expectProviders(call: Record<string, unknown>, provider: string) {
|
||||
expect(call.provider).toBe(provider);
|
||||
expect(call.messageProvider).toBe(provider);
|
||||
function expectChannels(call: Record<string, unknown>, channel: string) {
|
||||
expect(call.channel).toBe(channel);
|
||||
expect(call.messageChannel).toBe(channel);
|
||||
}
|
||||
|
||||
describe("gateway server agent", () => {
|
||||
@@ -45,7 +45,7 @@ describe("gateway server agent", () => {
|
||||
main: {
|
||||
sessionId: "sess-main-stale",
|
||||
updatedAt: Date.now(),
|
||||
lastProvider: "whatsapp",
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
},
|
||||
},
|
||||
@@ -61,7 +61,7 @@ describe("gateway server agent", () => {
|
||||
const res = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "last",
|
||||
channel: "last",
|
||||
deliver: true,
|
||||
idempotencyKey: "idem-agent-last-stale",
|
||||
});
|
||||
@@ -69,7 +69,7 @@ describe("gateway server agent", () => {
|
||||
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expectProviders(call, "whatsapp");
|
||||
expectChannels(call, "whatsapp");
|
||||
expect(call.to).toBe("+1555");
|
||||
expect(call.deliveryTargetMode).toBe("implicit");
|
||||
expect(call.sessionId).toBe("sess-main-stale");
|
||||
@@ -111,7 +111,7 @@ describe("gateway server agent", () => {
|
||||
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expect(call.sessionKey).toBe("agent:main:subagent:abc");
|
||||
expect(call.sessionId).toBe("sess-sub");
|
||||
expectProviders(call, "webchat");
|
||||
expectChannels(call, "webchat");
|
||||
expect(call.deliver).toBe(false);
|
||||
expect(call.to).toBeUndefined();
|
||||
|
||||
@@ -157,7 +157,7 @@ describe("gateway server agent", () => {
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expect(call.sessionKey).toBe("main");
|
||||
expectProviders(call, "webchat");
|
||||
expectChannels(call, "webchat");
|
||||
expect(call.message).toBe("what is in the image?");
|
||||
|
||||
const images = call.images as Array<Record<string, unknown>>;
|
||||
@@ -171,7 +171,7 @@ describe("gateway server agent", () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
test("agent falls back to whatsapp when delivery requested and no last provider exists", async () => {
|
||||
test("agent falls back to whatsapp when delivery requested and no last channel exists", async () => {
|
||||
testState.allowFrom = ["+1555"];
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-"));
|
||||
testState.sessionStorePath = path.join(dir, "sessions.json");
|
||||
@@ -203,7 +203,7 @@ describe("gateway server agent", () => {
|
||||
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expectProviders(call, "whatsapp");
|
||||
expectChannels(call, "whatsapp");
|
||||
expect(call.to).toBe("+1555");
|
||||
expect(call.deliver).toBe(true);
|
||||
expect(call.sessionId).toBe("sess-main-missing-provider");
|
||||
@@ -223,7 +223,7 @@ describe("gateway server agent", () => {
|
||||
main: {
|
||||
sessionId: "sess-main-whatsapp",
|
||||
updatedAt: Date.now(),
|
||||
lastProvider: "whatsapp",
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
},
|
||||
},
|
||||
@@ -239,7 +239,7 @@ describe("gateway server agent", () => {
|
||||
const res = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "last",
|
||||
channel: "last",
|
||||
deliver: true,
|
||||
idempotencyKey: "idem-agent-last-whatsapp",
|
||||
});
|
||||
@@ -247,8 +247,8 @@ describe("gateway server agent", () => {
|
||||
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expectProviders(call, "whatsapp");
|
||||
expect(call.messageProvider).toBe("whatsapp");
|
||||
expectChannels(call, "whatsapp");
|
||||
expect(call.messageChannel).toBe("whatsapp");
|
||||
expect(call.to).toBe("+1555");
|
||||
expect(call.deliver).toBe(true);
|
||||
expect(call.bestEffortDeliver).toBe(true);
|
||||
@@ -268,7 +268,7 @@ describe("gateway server agent", () => {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
lastProvider: "telegram",
|
||||
lastChannel: "telegram",
|
||||
lastTo: "123",
|
||||
},
|
||||
},
|
||||
@@ -284,7 +284,7 @@ describe("gateway server agent", () => {
|
||||
const res = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "last",
|
||||
channel: "last",
|
||||
deliver: true,
|
||||
idempotencyKey: "idem-agent-last",
|
||||
});
|
||||
@@ -292,7 +292,7 @@ describe("gateway server agent", () => {
|
||||
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expectProviders(call, "telegram");
|
||||
expectChannels(call, "telegram");
|
||||
expect(call.to).toBe("123");
|
||||
expect(call.deliver).toBe(true);
|
||||
expect(call.bestEffortDeliver).toBe(true);
|
||||
@@ -312,7 +312,7 @@ describe("gateway server agent", () => {
|
||||
main: {
|
||||
sessionId: "sess-discord",
|
||||
updatedAt: Date.now(),
|
||||
lastProvider: "discord",
|
||||
lastChannel: "discord",
|
||||
lastTo: "channel:discord-123",
|
||||
},
|
||||
},
|
||||
@@ -328,7 +328,7 @@ describe("gateway server agent", () => {
|
||||
const res = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "last",
|
||||
channel: "last",
|
||||
deliver: true,
|
||||
idempotencyKey: "idem-agent-last-discord",
|
||||
});
|
||||
@@ -336,7 +336,7 @@ describe("gateway server agent", () => {
|
||||
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expectProviders(call, "discord");
|
||||
expectChannels(call, "discord");
|
||||
expect(call.to).toBe("channel:discord-123");
|
||||
expect(call.deliver).toBe(true);
|
||||
expect(call.bestEffortDeliver).toBe(true);
|
||||
@@ -356,7 +356,7 @@ describe("gateway server agent", () => {
|
||||
main: {
|
||||
sessionId: "sess-slack",
|
||||
updatedAt: Date.now(),
|
||||
lastProvider: "slack",
|
||||
lastChannel: "slack",
|
||||
lastTo: "channel:slack-123",
|
||||
},
|
||||
},
|
||||
@@ -372,7 +372,7 @@ describe("gateway server agent", () => {
|
||||
const res = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "last",
|
||||
channel: "last",
|
||||
deliver: true,
|
||||
idempotencyKey: "idem-agent-last-slack",
|
||||
});
|
||||
@@ -380,7 +380,7 @@ describe("gateway server agent", () => {
|
||||
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expectProviders(call, "slack");
|
||||
expectChannels(call, "slack");
|
||||
expect(call.to).toBe("channel:slack-123");
|
||||
expect(call.deliver).toBe(true);
|
||||
expect(call.bestEffortDeliver).toBe(true);
|
||||
@@ -400,7 +400,7 @@ describe("gateway server agent", () => {
|
||||
main: {
|
||||
sessionId: "sess-signal",
|
||||
updatedAt: Date.now(),
|
||||
lastProvider: "signal",
|
||||
lastChannel: "signal",
|
||||
lastTo: "+15551234567",
|
||||
},
|
||||
},
|
||||
@@ -416,7 +416,7 @@ describe("gateway server agent", () => {
|
||||
const res = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "last",
|
||||
channel: "last",
|
||||
deliver: true,
|
||||
idempotencyKey: "idem-agent-last-signal",
|
||||
});
|
||||
@@ -424,7 +424,7 @@ describe("gateway server agent", () => {
|
||||
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expectProviders(call, "signal");
|
||||
expectChannels(call, "signal");
|
||||
expect(call.to).toBe("+15551234567");
|
||||
expect(call.deliver).toBe(true);
|
||||
expect(call.bestEffortDeliver).toBe(true);
|
||||
@@ -444,7 +444,7 @@ describe("gateway server agent", () => {
|
||||
main: {
|
||||
sessionId: "sess-teams",
|
||||
updatedAt: Date.now(),
|
||||
lastProvider: "msteams",
|
||||
lastChannel: "msteams",
|
||||
lastTo: "conversation:teams-123",
|
||||
},
|
||||
},
|
||||
@@ -460,7 +460,7 @@ describe("gateway server agent", () => {
|
||||
const res = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "last",
|
||||
channel: "last",
|
||||
deliver: true,
|
||||
idempotencyKey: "idem-agent-last-msteams",
|
||||
});
|
||||
@@ -468,7 +468,7 @@ describe("gateway server agent", () => {
|
||||
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expectProviders(call, "msteams");
|
||||
expectChannels(call, "msteams");
|
||||
expect(call.to).toBe("conversation:teams-123");
|
||||
expect(call.deliver).toBe(true);
|
||||
expect(call.bestEffortDeliver).toBe(true);
|
||||
@@ -478,7 +478,7 @@ describe("gateway server agent", () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
test("agent accepts provider aliases (imsg/teams)", async () => {
|
||||
test("agent accepts channel 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(
|
||||
@@ -488,7 +488,7 @@ describe("gateway server agent", () => {
|
||||
main: {
|
||||
sessionId: "sess-alias",
|
||||
updatedAt: Date.now(),
|
||||
lastProvider: "imessage",
|
||||
lastChannel: "imessage",
|
||||
lastTo: "chat_id:123",
|
||||
},
|
||||
},
|
||||
@@ -504,7 +504,7 @@ describe("gateway server agent", () => {
|
||||
const resIMessage = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "imsg",
|
||||
channel: "imsg",
|
||||
deliver: true,
|
||||
idempotencyKey: "idem-agent-imsg",
|
||||
});
|
||||
@@ -513,7 +513,7 @@ describe("gateway server agent", () => {
|
||||
const resTeams = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "teams",
|
||||
channel: "teams",
|
||||
to: "conversation:teams-abc",
|
||||
deliver: false,
|
||||
idempotencyKey: "idem-agent-teams",
|
||||
@@ -525,26 +525,26 @@ describe("gateway server agent", () => {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
expectProviders(lastIMessageCall, "imessage");
|
||||
expectChannels(lastIMessageCall, "imessage");
|
||||
expect(lastIMessageCall.to).toBe("chat_id:123");
|
||||
|
||||
const lastTeamsCall = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expectProviders(lastTeamsCall, "msteams");
|
||||
expectChannels(lastTeamsCall, "msteams");
|
||||
expect(lastTeamsCall.to).toBe("conversation:teams-abc");
|
||||
|
||||
ws.close();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
test("agent rejects unknown provider", async () => {
|
||||
test("agent rejects unknown channel", 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",
|
||||
channel: "sms",
|
||||
idempotencyKey: "idem-agent-bad-channel",
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
expect(res.error?.code).toBe("INVALID_REQUEST");
|
||||
@@ -564,7 +564,7 @@ describe("gateway server agent", () => {
|
||||
main: {
|
||||
sessionId: "sess-main-webchat",
|
||||
updatedAt: Date.now(),
|
||||
lastProvider: "webchat",
|
||||
lastChannel: "webchat",
|
||||
lastTo: "+1555",
|
||||
},
|
||||
},
|
||||
@@ -580,7 +580,7 @@ describe("gateway server agent", () => {
|
||||
const res = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "last",
|
||||
channel: "last",
|
||||
deliver: true,
|
||||
idempotencyKey: "idem-agent-webchat",
|
||||
});
|
||||
@@ -588,7 +588,7 @@ describe("gateway server agent", () => {
|
||||
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expectProviders(call, "whatsapp");
|
||||
expectChannels(call, "whatsapp");
|
||||
expect(call.to).toBe("+1555");
|
||||
expect(call.deliver).toBe(true);
|
||||
expect(call.bestEffortDeliver).toBe(true);
|
||||
@@ -608,7 +608,7 @@ describe("gateway server agent", () => {
|
||||
main: {
|
||||
sessionId: "sess-main-webchat-internal",
|
||||
updatedAt: Date.now(),
|
||||
lastProvider: "webchat",
|
||||
lastChannel: "webchat",
|
||||
lastTo: "+1555",
|
||||
},
|
||||
},
|
||||
@@ -624,7 +624,7 @@ describe("gateway server agent", () => {
|
||||
const res = await rpcReq(ws, "agent", {
|
||||
message: "hi",
|
||||
sessionKey: "main",
|
||||
provider: "last",
|
||||
channel: "last",
|
||||
deliver: false,
|
||||
idempotencyKey: "idem-agent-webchat-internal",
|
||||
});
|
||||
@@ -632,7 +632,7 @@ describe("gateway server agent", () => {
|
||||
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const call = spy.mock.calls.at(-1)?.[0] as Record<string, unknown>;
|
||||
expectProviders(call, "webchat");
|
||||
expectChannels(call, "webchat");
|
||||
expect(call.to).toBeUndefined();
|
||||
expect(call.deliver).toBe(false);
|
||||
expect(call.bestEffortDeliver).toBe(true);
|
||||
|
||||
@@ -10,15 +10,15 @@ const loadConfigHelpers = async () => await import("../config/config.js");
|
||||
|
||||
installGatewayTestHooks();
|
||||
|
||||
describe("gateway server providers", () => {
|
||||
test("providers.status returns snapshot without probe", async () => {
|
||||
describe("gateway server channels", () => {
|
||||
test("channels.status returns snapshot without probe", async () => {
|
||||
const prevToken = process.env.TELEGRAM_BOT_TOKEN;
|
||||
delete process.env.TELEGRAM_BOT_TOKEN;
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
const res = await rpcReq<{
|
||||
providers?: Record<
|
||||
channels?: Record<
|
||||
string,
|
||||
| {
|
||||
configured?: boolean;
|
||||
@@ -28,11 +28,11 @@ describe("gateway server providers", () => {
|
||||
}
|
||||
| { linked?: boolean }
|
||||
>;
|
||||
}>(ws, "providers.status", { probe: false, timeoutMs: 2000 });
|
||||
}>(ws, "channels.status", { probe: false, timeoutMs: 2000 });
|
||||
expect(res.ok).toBe(true);
|
||||
const telegram = res.payload?.providers?.telegram;
|
||||
const signal = res.payload?.providers?.signal;
|
||||
expect(res.payload?.providers?.whatsapp).toBeTruthy();
|
||||
const telegram = res.payload?.channels?.telegram;
|
||||
const signal = res.payload?.channels?.signal;
|
||||
expect(res.payload?.channels?.whatsapp).toBeTruthy();
|
||||
expect(telegram?.configured).toBe(false);
|
||||
expect(telegram?.tokenSource).toBe("none");
|
||||
expect(telegram?.probe).toBeUndefined();
|
||||
@@ -50,32 +50,34 @@ describe("gateway server providers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("providers.logout reports no session when missing", async () => {
|
||||
test("channels.logout reports no session when missing", async () => {
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
const res = await rpcReq<{ cleared?: boolean; provider?: string }>(
|
||||
const res = await rpcReq<{ cleared?: boolean; channel?: string }>(
|
||||
ws,
|
||||
"providers.logout",
|
||||
{ provider: "whatsapp" },
|
||||
"channels.logout",
|
||||
{ channel: "whatsapp" },
|
||||
);
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.payload?.provider).toBe("whatsapp");
|
||||
expect(res.payload?.channel).toBe("whatsapp");
|
||||
expect(res.payload?.cleared).toBe(false);
|
||||
|
||||
ws.close();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
test("providers.logout clears telegram bot token from config", async () => {
|
||||
test("channels.logout clears telegram bot token from config", async () => {
|
||||
const prevToken = process.env.TELEGRAM_BOT_TOKEN;
|
||||
delete process.env.TELEGRAM_BOT_TOKEN;
|
||||
const { readConfigFileSnapshot, writeConfigFile } =
|
||||
await loadConfigHelpers();
|
||||
await writeConfigFile({
|
||||
telegram: {
|
||||
botToken: "123:abc",
|
||||
groups: { "*": { requireMention: false } },
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "123:abc",
|
||||
groups: { "*": { requireMention: false } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -85,17 +87,19 @@ describe("gateway server providers", () => {
|
||||
const res = await rpcReq<{
|
||||
cleared?: boolean;
|
||||
envToken?: boolean;
|
||||
provider?: string;
|
||||
}>(ws, "providers.logout", { provider: "telegram" });
|
||||
channel?: string;
|
||||
}>(ws, "channels.logout", { channel: "telegram" });
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.payload?.provider).toBe("telegram");
|
||||
expect(res.payload?.channel).toBe("telegram");
|
||||
expect(res.payload?.cleared).toBe(true);
|
||||
expect(res.payload?.envToken).toBe(false);
|
||||
|
||||
const snap = await readConfigFileSnapshot();
|
||||
expect(snap.valid).toBe(true);
|
||||
expect(snap.config?.telegram?.botToken).toBeUndefined();
|
||||
expect(snap.config?.telegram?.groups?.["*"]?.requireMention).toBe(false);
|
||||
expect(snap.config?.channels?.telegram?.botToken).toBeUndefined();
|
||||
expect(snap.config?.channels?.telegram?.groups?.["*"]?.requireMention).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
ws.close();
|
||||
await server.close();
|
||||
@@ -6,7 +6,7 @@ import { emitAgentEvent } from "../infra/agent-events.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
import {
|
||||
agentCommand,
|
||||
connectOk,
|
||||
@@ -107,7 +107,7 @@ describe("gateway server chat", () => {
|
||||
rules: [
|
||||
{
|
||||
action: "deny",
|
||||
match: { provider: "discord", chatType: "group" },
|
||||
match: { channel: "discord", chatType: "group" },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -121,7 +121,7 @@ describe("gateway server chat", () => {
|
||||
sessionId: "sess-discord",
|
||||
updatedAt: Date.now(),
|
||||
chatType: "group",
|
||||
provider: "discord",
|
||||
channel: "discord",
|
||||
},
|
||||
},
|
||||
null,
|
||||
@@ -534,7 +534,7 @@ describe("gateway server chat", () => {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
lastProvider: "whatsapp",
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
},
|
||||
},
|
||||
@@ -557,9 +557,9 @@ describe("gateway server chat", () => {
|
||||
const stored = JSON.parse(
|
||||
await fs.readFile(testState.sessionStorePath, "utf-8"),
|
||||
) as {
|
||||
main?: { lastProvider?: string; lastTo?: string };
|
||||
main?: { lastChannel?: string; lastTo?: string };
|
||||
};
|
||||
expect(stored.main?.lastProvider).toBe("whatsapp");
|
||||
expect(stored.main?.lastChannel).toBe("whatsapp");
|
||||
expect(stored.main?.lastTo).toBe("+1555");
|
||||
|
||||
ws.close();
|
||||
|
||||
@@ -6,7 +6,7 @@ import { emitHeartbeatEvent } from "../infra/heartbeat-events.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
import {
|
||||
connectOk,
|
||||
getFreePort,
|
||||
@@ -38,9 +38,9 @@ describe("gateway server health/presence", () => {
|
||||
ws,
|
||||
(o) => o.type === "res" && o.id === "presence1",
|
||||
);
|
||||
const providersP = onceMessage(
|
||||
const channelsP = onceMessage(
|
||||
ws,
|
||||
(o) => o.type === "res" && o.id === "providers1",
|
||||
(o) => o.type === "res" && o.id === "channels1",
|
||||
);
|
||||
|
||||
const sendReq = (id: string, method: string) =>
|
||||
@@ -48,16 +48,16 @@ describe("gateway server health/presence", () => {
|
||||
sendReq("health1", "health");
|
||||
sendReq("status1", "status");
|
||||
sendReq("presence1", "system-presence");
|
||||
sendReq("providers1", "providers.status");
|
||||
sendReq("channels1", "channels.status");
|
||||
|
||||
const health = await healthP;
|
||||
const status = await statusP;
|
||||
const presence = await presenceP;
|
||||
const providers = await providersP;
|
||||
const channels = await channelsP;
|
||||
expect(health.ok).toBe(true);
|
||||
expect(status.ok).toBe(true);
|
||||
expect(presence.ok).toBe(true);
|
||||
expect(providers.ok).toBe(true);
|
||||
expect(channels.ok).toBe(true);
|
||||
expect(Array.isArray(presence.payload)).toBe(true);
|
||||
|
||||
ws.close();
|
||||
|
||||
@@ -120,7 +120,7 @@ describe("gateway server hooks", () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
test("hooks agent rejects invalid provider", async () => {
|
||||
test("hooks agent rejects invalid channel", async () => {
|
||||
testState.hooksConfig = { enabled: true, token: "hook-secret" };
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port);
|
||||
@@ -130,7 +130,7 @@ describe("gateway server hooks", () => {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer hook-secret",
|
||||
},
|
||||
body: JSON.stringify({ message: "Nope", provider: "sms" }),
|
||||
body: JSON.stringify({ message: "Nope", channel: "sms" }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
expect(peekSystemEvents(resolveMainKey()).length).toBe(0);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { emitAgentEvent } from "../infra/agent-events.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
import {
|
||||
agentCommand,
|
||||
bridgeInvoke,
|
||||
@@ -814,7 +814,7 @@ describe("gateway server node/bridge", () => {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
lastProvider: "whatsapp",
|
||||
lastChannel: "whatsapp",
|
||||
lastTo: "+1555",
|
||||
},
|
||||
},
|
||||
@@ -842,7 +842,7 @@ describe("gateway server node/bridge", () => {
|
||||
expect(call.sessionId).toBe("sess-main");
|
||||
expect(call.sessionKey).toBe("main");
|
||||
expect(call.deliver).toBe(false);
|
||||
expect(call.messageProvider).toBe("node");
|
||||
expect(call.messageChannel).toBe("node");
|
||||
|
||||
const stored = JSON.parse(
|
||||
await fs.readFile(testState.sessionStorePath, "utf-8"),
|
||||
|
||||
@@ -94,13 +94,13 @@ const hoisted = vi.hoisted(() => {
|
||||
msteams: {},
|
||||
},
|
||||
})),
|
||||
startProviders: vi.fn(async () => {}),
|
||||
startProvider: vi.fn(async () => {}),
|
||||
stopProvider: vi.fn(async () => {}),
|
||||
markProviderLoggedOut: vi.fn(),
|
||||
startChannels: vi.fn(async () => {}),
|
||||
startChannel: vi.fn(async () => {}),
|
||||
stopChannel: vi.fn(async () => {}),
|
||||
markChannelLoggedOut: vi.fn(),
|
||||
};
|
||||
|
||||
const createProviderManager = vi.fn(() => providerManager);
|
||||
const createChannelManager = vi.fn(() => providerManager);
|
||||
|
||||
const reloaderStop = vi.fn(async () => {});
|
||||
let onHotReload:
|
||||
@@ -129,7 +129,7 @@ const hoisted = vi.hoisted(() => {
|
||||
startGmailWatcher,
|
||||
stopGmailWatcher,
|
||||
providerManager,
|
||||
createProviderManager,
|
||||
createChannelManager,
|
||||
startGatewayConfigReloader,
|
||||
reloaderStop,
|
||||
getOnHotReload: () => onHotReload,
|
||||
@@ -155,8 +155,8 @@ vi.mock("../hooks/gmail-watcher.js", () => ({
|
||||
stopGmailWatcher: hoisted.stopGmailWatcher,
|
||||
}));
|
||||
|
||||
vi.mock("./server-providers.js", () => ({
|
||||
createProviderManager: hoisted.createProviderManager,
|
||||
vi.mock("./server-channels.js", () => ({
|
||||
createChannelManager: hoisted.createChannelManager,
|
||||
}));
|
||||
|
||||
vi.mock("./config-reload.js", () => ({
|
||||
@@ -206,10 +206,12 @@ describe("gateway hot reload", () => {
|
||||
agents: { defaults: { heartbeat: { every: "1m" }, maxConcurrent: 2 } },
|
||||
browser: { enabled: true, controlUrl: "http://127.0.0.1:18791" },
|
||||
web: { enabled: true },
|
||||
telegram: { botToken: "token" },
|
||||
discord: { token: "token" },
|
||||
signal: { account: "+15550000000" },
|
||||
imessage: { enabled: true },
|
||||
channels: {
|
||||
telegram: { botToken: "token" },
|
||||
discord: { token: "token" },
|
||||
signal: { account: "+15550000000" },
|
||||
imessage: { enabled: true },
|
||||
},
|
||||
};
|
||||
|
||||
await onHotReload?.(
|
||||
@@ -220,10 +222,10 @@ describe("gateway hot reload", () => {
|
||||
"agents.defaults.heartbeat.every",
|
||||
"browser.enabled",
|
||||
"web.enabled",
|
||||
"telegram.botToken",
|
||||
"discord.token",
|
||||
"signal.account",
|
||||
"imessage.enabled",
|
||||
"channels.telegram.botToken",
|
||||
"channels.discord.token",
|
||||
"channels.signal.account",
|
||||
"channels.imessage.enabled",
|
||||
],
|
||||
restartGateway: false,
|
||||
restartReasons: [],
|
||||
@@ -233,7 +235,7 @@ describe("gateway hot reload", () => {
|
||||
restartBrowserControl: true,
|
||||
restartCron: true,
|
||||
restartHeartbeat: true,
|
||||
restartProviders: new Set([
|
||||
restartChannels: new Set([
|
||||
"whatsapp",
|
||||
"telegram",
|
||||
"discord",
|
||||
@@ -258,34 +260,30 @@ describe("gateway hot reload", () => {
|
||||
expect(hoisted.cronInstances[0].stop).toHaveBeenCalledTimes(1);
|
||||
expect(hoisted.cronInstances[1].start).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(hoisted.providerManager.stopProvider).toHaveBeenCalledTimes(5);
|
||||
expect(hoisted.providerManager.startProvider).toHaveBeenCalledTimes(5);
|
||||
expect(hoisted.providerManager.stopProvider).toHaveBeenCalledWith(
|
||||
expect(hoisted.providerManager.stopChannel).toHaveBeenCalledTimes(5);
|
||||
expect(hoisted.providerManager.startChannel).toHaveBeenCalledTimes(5);
|
||||
expect(hoisted.providerManager.stopChannel).toHaveBeenCalledWith(
|
||||
"whatsapp",
|
||||
);
|
||||
expect(hoisted.providerManager.startProvider).toHaveBeenCalledWith(
|
||||
expect(hoisted.providerManager.startChannel).toHaveBeenCalledWith(
|
||||
"whatsapp",
|
||||
);
|
||||
expect(hoisted.providerManager.stopProvider).toHaveBeenCalledWith(
|
||||
expect(hoisted.providerManager.stopChannel).toHaveBeenCalledWith(
|
||||
"telegram",
|
||||
);
|
||||
expect(hoisted.providerManager.startProvider).toHaveBeenCalledWith(
|
||||
expect(hoisted.providerManager.startChannel).toHaveBeenCalledWith(
|
||||
"telegram",
|
||||
);
|
||||
expect(hoisted.providerManager.stopProvider).toHaveBeenCalledWith(
|
||||
expect(hoisted.providerManager.stopChannel).toHaveBeenCalledWith("discord");
|
||||
expect(hoisted.providerManager.startChannel).toHaveBeenCalledWith(
|
||||
"discord",
|
||||
);
|
||||
expect(hoisted.providerManager.startProvider).toHaveBeenCalledWith(
|
||||
"discord",
|
||||
);
|
||||
expect(hoisted.providerManager.stopProvider).toHaveBeenCalledWith("signal");
|
||||
expect(hoisted.providerManager.startProvider).toHaveBeenCalledWith(
|
||||
"signal",
|
||||
);
|
||||
expect(hoisted.providerManager.stopProvider).toHaveBeenCalledWith(
|
||||
expect(hoisted.providerManager.stopChannel).toHaveBeenCalledWith("signal");
|
||||
expect(hoisted.providerManager.startChannel).toHaveBeenCalledWith("signal");
|
||||
expect(hoisted.providerManager.stopChannel).toHaveBeenCalledWith(
|
||||
"imessage",
|
||||
);
|
||||
expect(hoisted.providerManager.startProvider).toHaveBeenCalledWith(
|
||||
expect(hoisted.providerManager.startChannel).toHaveBeenCalledWith(
|
||||
"imessage",
|
||||
);
|
||||
|
||||
@@ -313,7 +311,7 @@ describe("gateway hot reload", () => {
|
||||
restartBrowserControl: false,
|
||||
restartCron: false,
|
||||
restartHeartbeat: false,
|
||||
restartProviders: new Set(),
|
||||
restartChannels: new Set(),
|
||||
noopPaths: [],
|
||||
},
|
||||
{},
|
||||
|
||||
@@ -27,6 +27,11 @@ import {
|
||||
createCanvasHostHandler,
|
||||
startCanvasHost,
|
||||
} from "../canvas-host/server.js";
|
||||
import {
|
||||
type ChannelId,
|
||||
listChannelPlugins,
|
||||
normalizeChannelId,
|
||||
} from "../channels/plugins/index.js";
|
||||
import { createDefaultDeps } from "../cli/deps.js";
|
||||
import { agentCommand } from "../commands/agent.js";
|
||||
import { getHealthSnapshot, type HealthSummary } from "../commands/health.js";
|
||||
@@ -114,17 +119,12 @@ import {
|
||||
startPluginServices,
|
||||
} from "../plugins/services.js";
|
||||
import { setCommandLaneConcurrency } from "../process/command-queue.js";
|
||||
import {
|
||||
listProviderPlugins,
|
||||
normalizeProviderId,
|
||||
type ProviderId,
|
||||
} from "../providers/plugins/index.js";
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import {
|
||||
isGatewayCliClient,
|
||||
isWebchatClient,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
import { runOnboardingWizard } from "../wizard/onboarding.js";
|
||||
import type { WizardSession } from "../wizard/session.js";
|
||||
import {
|
||||
@@ -138,12 +138,12 @@ import {
|
||||
type ChatAbortControllerEntry,
|
||||
} from "./chat-abort.js";
|
||||
import {
|
||||
type ChannelKind,
|
||||
type GatewayReloadPlan,
|
||||
type ProviderKind,
|
||||
startGatewayConfigReloader,
|
||||
} from "./config-reload.js";
|
||||
import { normalizeControlUiBasePath } from "./control-ui.js";
|
||||
import { type HookMessageProvider, resolveHooksConfig } from "./hooks.js";
|
||||
import { type HookMessageChannel, resolveHooksConfig } from "./hooks.js";
|
||||
import {
|
||||
isLoopbackAddress,
|
||||
isLoopbackHost,
|
||||
@@ -168,6 +168,7 @@ import {
|
||||
createBridgeSubscriptionManager,
|
||||
} from "./server-bridge-subscriptions.js";
|
||||
import { startBrowserControlServerIfEnabled } from "./server-browser.js";
|
||||
import { createChannelManager } from "./server-channels.js";
|
||||
import { createAgentEventHandler, createChatRunState } from "./server-chat.js";
|
||||
import {
|
||||
DEDUPE_MAX,
|
||||
@@ -189,7 +190,6 @@ import {
|
||||
createHooksRequestHandler,
|
||||
} from "./server-http.js";
|
||||
import { coreGatewayHandlers, handleGatewayRequest } from "./server-methods.js";
|
||||
import { createProviderManager } from "./server-providers.js";
|
||||
import type { DedupeEntry } from "./server-shared.js";
|
||||
import { formatError } from "./server-utils.js";
|
||||
import { loadSessionEntry } from "./session-utils.js";
|
||||
@@ -202,7 +202,7 @@ const logCanvas = log.child("canvas");
|
||||
const logBridge = log.child("bridge");
|
||||
const logDiscovery = log.child("discovery");
|
||||
const logTailscale = log.child("tailscale");
|
||||
const logProviders = log.child("providers");
|
||||
const logChannels = log.child("channels");
|
||||
const logBrowser = log.child("browser");
|
||||
const logHealth = log.child("health");
|
||||
const logCron = log.child("cron");
|
||||
@@ -210,18 +210,18 @@ const logReload = log.child("reload");
|
||||
const logHooks = log.child("hooks");
|
||||
const logWsControl = log.child("ws");
|
||||
const canvasRuntime = runtimeForLogger(logCanvas);
|
||||
const providerLogs = Object.fromEntries(
|
||||
listProviderPlugins().map((plugin) => [
|
||||
const channelLogs = Object.fromEntries(
|
||||
listChannelPlugins().map((plugin) => [
|
||||
plugin.id,
|
||||
logProviders.child(plugin.id),
|
||||
logChannels.child(plugin.id),
|
||||
]),
|
||||
) as Record<ProviderId, ReturnType<typeof createSubsystemLogger>>;
|
||||
const providerRuntimeEnvs = Object.fromEntries(
|
||||
Object.entries(providerLogs).map(([id, logger]) => [
|
||||
) as Record<ChannelId, ReturnType<typeof createSubsystemLogger>>;
|
||||
const channelRuntimeEnvs = Object.fromEntries(
|
||||
Object.entries(channelLogs).map(([id, logger]) => [
|
||||
id,
|
||||
runtimeForLogger(logger),
|
||||
]),
|
||||
) as Record<ProviderId, RuntimeEnv>;
|
||||
) as Record<ChannelId, RuntimeEnv>;
|
||||
|
||||
type GatewayModelChoice = ModelCatalogEntry;
|
||||
|
||||
@@ -246,8 +246,8 @@ type Client = {
|
||||
const BASE_METHODS = [
|
||||
"health",
|
||||
"logs.tail",
|
||||
"providers.status",
|
||||
"providers.logout",
|
||||
"channels.status",
|
||||
"channels.logout",
|
||||
"status",
|
||||
"usage.status",
|
||||
"config.get",
|
||||
@@ -302,11 +302,11 @@ const BASE_METHODS = [
|
||||
"chat.send",
|
||||
];
|
||||
|
||||
const PROVIDER_METHODS = listProviderPlugins().flatMap(
|
||||
const CHANNEL_METHODS = listChannelPlugins().flatMap(
|
||||
(plugin) => plugin.gatewayMethods ?? [],
|
||||
);
|
||||
|
||||
const METHODS = Array.from(new Set([...BASE_METHODS, ...PROVIDER_METHODS]));
|
||||
const METHODS = Array.from(new Set([...BASE_METHODS, ...CHANNEL_METHODS]));
|
||||
|
||||
const EVENTS = [
|
||||
"agent",
|
||||
@@ -576,7 +576,7 @@ export async function startGatewayServer(
|
||||
wakeMode: "now" | "next-heartbeat";
|
||||
sessionKey: string;
|
||||
deliver: boolean;
|
||||
provider: HookMessageProvider;
|
||||
channel: HookMessageChannel;
|
||||
to?: string;
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
@@ -604,7 +604,7 @@ export async function startGatewayServer(
|
||||
thinking: value.thinking,
|
||||
timeoutSeconds: value.timeoutSeconds,
|
||||
deliver: value.deliver,
|
||||
provider: value.provider,
|
||||
channel: value.channel,
|
||||
to: value.to,
|
||||
},
|
||||
state: { nextRunAtMs: now },
|
||||
@@ -864,18 +864,18 @@ export async function startGatewayServer(
|
||||
|
||||
let { cron, storePath: cronStorePath } = buildCronService(cfgAtStart);
|
||||
|
||||
const providerManager = createProviderManager({
|
||||
const channelManager = createChannelManager({
|
||||
loadConfig,
|
||||
providerLogs,
|
||||
providerRuntimeEnvs,
|
||||
channelLogs,
|
||||
channelRuntimeEnvs,
|
||||
});
|
||||
const {
|
||||
getRuntimeSnapshot,
|
||||
startProviders,
|
||||
startProvider,
|
||||
stopProvider,
|
||||
markProviderLoggedOut,
|
||||
} = providerManager;
|
||||
startChannels,
|
||||
startChannel,
|
||||
stopChannel,
|
||||
markChannelLoggedOut,
|
||||
} = channelManager;
|
||||
|
||||
const broadcast = (
|
||||
event: string,
|
||||
@@ -1791,9 +1791,9 @@ export async function startGatewayServer(
|
||||
findRunningWizard,
|
||||
purgeWizardSession,
|
||||
getRuntimeSnapshot,
|
||||
startProvider,
|
||||
stopProvider,
|
||||
markProviderLoggedOut,
|
||||
startChannel,
|
||||
stopChannel,
|
||||
markChannelLoggedOut,
|
||||
wizardRunner,
|
||||
broadcastVoiceWakeChanged,
|
||||
},
|
||||
@@ -1933,16 +1933,21 @@ export async function startGatewayServer(
|
||||
}
|
||||
}
|
||||
|
||||
// Launch configured providers so gateway replies via the surface the message came from.
|
||||
// Tests can opt out via CLAWDBOT_SKIP_PROVIDERS.
|
||||
if (process.env.CLAWDBOT_SKIP_PROVIDERS !== "1") {
|
||||
// Launch configured channels so gateway replies via the surface the message came from.
|
||||
// Tests can opt out via CLAWDBOT_SKIP_CHANNELS (or legacy CLAWDBOT_SKIP_PROVIDERS).
|
||||
const skipChannels =
|
||||
process.env.CLAWDBOT_SKIP_CHANNELS === "1" ||
|
||||
process.env.CLAWDBOT_SKIP_PROVIDERS === "1";
|
||||
if (!skipChannels) {
|
||||
try {
|
||||
await startProviders();
|
||||
await startChannels();
|
||||
} catch (err) {
|
||||
logProviders.error(`provider startup failed: ${String(err)}`);
|
||||
logChannels.error(`channel startup failed: ${String(err)}`);
|
||||
}
|
||||
} else {
|
||||
logProviders.info("skipping provider start (CLAWDBOT_SKIP_PROVIDERS=1)");
|
||||
logChannels.info(
|
||||
"skipping channel start (CLAWDBOT_SKIP_CHANNELS=1 or CLAWDBOT_SKIP_PROVIDERS=1)",
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1970,19 +1975,19 @@ export async function startGatewayServer(
|
||||
}
|
||||
|
||||
const { cfg, entry } = loadSessionEntry(sessionKey);
|
||||
const lastProvider = entry?.lastProvider;
|
||||
const lastChannel = entry?.lastChannel;
|
||||
const lastTo = entry?.lastTo?.trim();
|
||||
const parsedTarget = resolveAnnounceTargetFromKey(sessionKey);
|
||||
const providerRaw = lastProvider ?? parsedTarget?.provider;
|
||||
const provider = providerRaw ? normalizeProviderId(providerRaw) : null;
|
||||
const channelRaw = lastChannel ?? parsedTarget?.channel;
|
||||
const channel = channelRaw ? normalizeChannelId(channelRaw) : null;
|
||||
const to = lastTo || parsedTarget?.to;
|
||||
if (!provider || !to) {
|
||||
if (!channel || !to) {
|
||||
enqueueSystemEvent(message, { sessionKey });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolved = resolveOutboundTarget({
|
||||
provider,
|
||||
channel,
|
||||
to,
|
||||
cfg,
|
||||
accountId: parsedTarget?.accountId ?? entry?.lastAccountId,
|
||||
@@ -1999,10 +2004,10 @@ export async function startGatewayServer(
|
||||
message,
|
||||
sessionKey,
|
||||
to: resolved.to,
|
||||
provider,
|
||||
channel,
|
||||
deliver: true,
|
||||
bestEffortDeliver: true,
|
||||
messageProvider: provider,
|
||||
messageChannel: channel,
|
||||
},
|
||||
defaultRuntime,
|
||||
deps,
|
||||
@@ -2082,19 +2087,22 @@ export async function startGatewayServer(
|
||||
}
|
||||
}
|
||||
|
||||
if (plan.restartProviders.size > 0) {
|
||||
if (process.env.CLAWDBOT_SKIP_PROVIDERS === "1") {
|
||||
logProviders.info(
|
||||
"skipping provider reload (CLAWDBOT_SKIP_PROVIDERS=1)",
|
||||
if (plan.restartChannels.size > 0) {
|
||||
if (
|
||||
process.env.CLAWDBOT_SKIP_CHANNELS === "1" ||
|
||||
process.env.CLAWDBOT_SKIP_PROVIDERS === "1"
|
||||
) {
|
||||
logChannels.info(
|
||||
"skipping channel reload (CLAWDBOT_SKIP_CHANNELS=1 or CLAWDBOT_SKIP_PROVIDERS=1)",
|
||||
);
|
||||
} else {
|
||||
const restartProvider = async (name: ProviderKind) => {
|
||||
logProviders.info(`restarting ${name} provider`);
|
||||
await stopProvider(name);
|
||||
await startProvider(name);
|
||||
const restartChannel = async (name: ChannelKind) => {
|
||||
logChannels.info(`restarting ${name} channel`);
|
||||
await stopChannel(name);
|
||||
await startChannel(name);
|
||||
};
|
||||
for (const provider of plan.restartProviders) {
|
||||
await restartProvider(provider);
|
||||
for (const channel of plan.restartChannels) {
|
||||
await restartChannel(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2189,8 +2197,8 @@ export async function startGatewayServer(
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
for (const plugin of listProviderPlugins()) {
|
||||
await stopProvider(plugin.id);
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
await stopChannel(plugin.id);
|
||||
}
|
||||
if (pluginServices) {
|
||||
await pluginServices.stop().catch(() => {});
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("gateway session utils", () => {
|
||||
test("parseGroupKey handles group prefixes", () => {
|
||||
expect(parseGroupKey("group:abc")).toEqual({ id: "abc" });
|
||||
expect(parseGroupKey("discord:group:dev")).toEqual({
|
||||
provider: "discord",
|
||||
channel: "discord",
|
||||
kind: "group",
|
||||
id: "dev",
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ export type GatewaySessionRow = {
|
||||
kind: "direct" | "group" | "global" | "unknown";
|
||||
label?: string;
|
||||
displayName?: string;
|
||||
provider?: string;
|
||||
channel?: string;
|
||||
subject?: string;
|
||||
room?: string;
|
||||
space?: string;
|
||||
@@ -58,7 +58,7 @@ export type GatewaySessionRow = {
|
||||
modelProvider?: string;
|
||||
model?: string;
|
||||
contextTokens?: number;
|
||||
lastProvider?: SessionEntry["lastProvider"];
|
||||
lastChannel?: SessionEntry["lastChannel"];
|
||||
lastTo?: string;
|
||||
lastAccountId?: string;
|
||||
};
|
||||
@@ -201,7 +201,7 @@ export function classifySessionKey(
|
||||
|
||||
export function parseGroupKey(
|
||||
key: string,
|
||||
): { provider?: string; kind?: "group" | "channel"; id?: string } | null {
|
||||
): { channel?: string; kind?: "group" | "channel"; id?: string } | null {
|
||||
const agentParsed = parseAgentSessionKey(key);
|
||||
const rawKey = agentParsed?.rest ?? key;
|
||||
if (rawKey.startsWith("group:")) {
|
||||
@@ -210,10 +210,10 @@ export function parseGroupKey(
|
||||
}
|
||||
const parts = rawKey.split(":").filter(Boolean);
|
||||
if (parts.length >= 3) {
|
||||
const [provider, kind, ...rest] = parts;
|
||||
const [channel, kind, ...rest] = parts;
|
||||
if (kind === "group" || kind === "channel") {
|
||||
const id = rest.join(":");
|
||||
return { provider, kind, id };
|
||||
return { channel, kind, id };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -533,16 +533,16 @@ export function listSessionsFromStore(params: {
|
||||
const output = entry?.outputTokens ?? 0;
|
||||
const total = entry?.totalTokens ?? input + output;
|
||||
const parsed = parseGroupKey(key);
|
||||
const provider = entry?.provider ?? parsed?.provider;
|
||||
const channel = entry?.channel ?? parsed?.channel;
|
||||
const subject = entry?.subject;
|
||||
const room = entry?.room;
|
||||
const space = entry?.space;
|
||||
const id = parsed?.id;
|
||||
const displayName =
|
||||
entry?.displayName ??
|
||||
(provider
|
||||
(channel
|
||||
? buildGroupDisplayName({
|
||||
provider,
|
||||
provider: channel,
|
||||
subject,
|
||||
room,
|
||||
space,
|
||||
@@ -555,7 +555,7 @@ export function listSessionsFromStore(params: {
|
||||
kind: classifySessionKey(key, entry),
|
||||
label: entry?.label,
|
||||
displayName,
|
||||
provider,
|
||||
channel,
|
||||
subject,
|
||||
room,
|
||||
space,
|
||||
@@ -576,7 +576,7 @@ export function listSessionsFromStore(params: {
|
||||
modelProvider: entry?.modelProvider,
|
||||
model: entry?.model,
|
||||
contextTokens: entry?.contextTokens,
|
||||
lastProvider: entry?.lastProvider,
|
||||
lastChannel: entry?.lastChannel,
|
||||
lastTo: entry?.lastTo,
|
||||
lastAccountId: entry?.lastAccountId,
|
||||
} satisfies GatewaySessionRow;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { resetLogger, setLoggerOverride } from "../logging.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../utils/message-provider.js";
|
||||
} from "../utils/message-channel.js";
|
||||
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
||||
import type { GatewayServerOptions } from "./server.js";
|
||||
|
||||
@@ -261,8 +261,10 @@ vi.mock("../config/config.js", async () => {
|
||||
return { defaults };
|
||||
})(),
|
||||
bindings: testState.bindingsConfig,
|
||||
whatsapp: {
|
||||
allowFrom: testState.allowFrom,
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: testState.allowFrom,
|
||||
},
|
||||
},
|
||||
session: {
|
||||
mainKey: "main",
|
||||
|
||||
Reference in New Issue
Block a user