mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-12 06:13:43 +00:00
fix(channels): add optional defaultAccount routing
This commit is contained in:
@@ -5,14 +5,25 @@ import { createAccountListHelpers } from "./account-helpers.js";
|
||||
const { listConfiguredAccountIds, listAccountIds, resolveDefaultAccountId } =
|
||||
createAccountListHelpers("testchannel");
|
||||
|
||||
function cfg(accounts?: Record<string, unknown> | null): OpenClawConfig {
|
||||
function cfg(accounts?: Record<string, unknown> | null, defaultAccount?: string): OpenClawConfig {
|
||||
if (accounts === null) {
|
||||
return { channels: { testchannel: {} } } as unknown as OpenClawConfig;
|
||||
return {
|
||||
channels: {
|
||||
testchannel: defaultAccount ? { defaultAccount } : {},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
}
|
||||
if (accounts === undefined) {
|
||||
if (accounts === undefined && !defaultAccount) {
|
||||
return {} as unknown as OpenClawConfig;
|
||||
}
|
||||
return { channels: { testchannel: { accounts } } } as unknown as OpenClawConfig;
|
||||
return {
|
||||
channels: {
|
||||
testchannel: {
|
||||
...(accounts === undefined ? {} : { accounts }),
|
||||
...(defaultAccount ? { defaultAccount } : {}),
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
}
|
||||
|
||||
describe("createAccountListHelpers", () => {
|
||||
@@ -56,6 +67,18 @@ describe("createAccountListHelpers", () => {
|
||||
});
|
||||
|
||||
describe("resolveDefaultAccountId", () => {
|
||||
it("prefers configured defaultAccount when it matches a configured account id", () => {
|
||||
expect(resolveDefaultAccountId(cfg({ alpha: {}, beta: {} }, "beta"))).toBe("beta");
|
||||
});
|
||||
|
||||
it("normalizes configured defaultAccount before matching", () => {
|
||||
expect(resolveDefaultAccountId(cfg({ "router-d": {} }, "Router D"))).toBe("router-d");
|
||||
});
|
||||
|
||||
it("falls back when configured defaultAccount is missing", () => {
|
||||
expect(resolveDefaultAccountId(cfg({ beta: {}, alpha: {} }, "missing"))).toBe("alpha");
|
||||
});
|
||||
|
||||
it('returns "default" when present', () => {
|
||||
expect(resolveDefaultAccountId(cfg({ default: {}, other: {} }))).toBe("default");
|
||||
});
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "../../routing/session-key.js";
|
||||
|
||||
export function createAccountListHelpers(channelKey: string) {
|
||||
function resolveConfiguredDefaultAccountId(cfg: OpenClawConfig): string | undefined {
|
||||
const channel = cfg.channels?.[channelKey] as Record<string, unknown> | undefined;
|
||||
const preferred = normalizeOptionalAccountId(
|
||||
typeof channel?.defaultAccount === "string" ? channel.defaultAccount : undefined,
|
||||
);
|
||||
if (!preferred) {
|
||||
return undefined;
|
||||
}
|
||||
const ids = listAccountIds(cfg);
|
||||
if (ids.some((id) => normalizeAccountId(id) === preferred)) {
|
||||
return preferred;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
|
||||
const channel = cfg.channels?.[channelKey];
|
||||
const accounts = (channel as Record<string, unknown> | undefined)?.accounts;
|
||||
@@ -20,6 +39,10 @@ export function createAccountListHelpers(channelKey: string) {
|
||||
}
|
||||
|
||||
function resolveDefaultAccountId(cfg: OpenClawConfig): string {
|
||||
const preferred = resolveConfiguredDefaultAccountId(cfg);
|
||||
if (preferred) {
|
||||
return preferred;
|
||||
}
|
||||
const ids = listAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
|
||||
@@ -35,6 +35,8 @@ export type ExtensionChannelConfig = {
|
||||
allowFrom?: string | string[];
|
||||
/** Default delivery target for CLI --deliver when no explicit --reply-to is provided. */
|
||||
defaultTo?: string;
|
||||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
dmPolicy?: string;
|
||||
groupPolicy?: GroupPolicy;
|
||||
accounts?: Record<string, unknown>;
|
||||
|
||||
@@ -321,4 +321,6 @@ export type DiscordAccountConfig = {
|
||||
export type DiscordConfig = {
|
||||
/** Optional per-account Discord configuration (multi-account). */
|
||||
accounts?: Record<string, DiscordAccountConfig>;
|
||||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
} & DiscordAccountConfig;
|
||||
|
||||
@@ -84,4 +84,6 @@ export type IMessageAccountConfig = {
|
||||
export type IMessageConfig = {
|
||||
/** Optional per-account iMessage configuration (multi-account). */
|
||||
accounts?: Record<string, IMessageAccountConfig>;
|
||||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
} & IMessageAccountConfig;
|
||||
|
||||
@@ -56,4 +56,6 @@ export type IrcAccountConfig = CommonChannelMessagingConfig & {
|
||||
export type IrcConfig = {
|
||||
/** Optional per-account IRC configuration (multi-account). */
|
||||
accounts?: Record<string, IrcAccountConfig>;
|
||||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
} & IrcAccountConfig;
|
||||
|
||||
@@ -48,4 +48,6 @@ export type SignalAccountConfig = CommonChannelMessagingConfig & {
|
||||
export type SignalConfig = {
|
||||
/** Optional per-account Signal configuration (multi-account). */
|
||||
accounts?: Record<string, SignalAccountConfig>;
|
||||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
} & SignalAccountConfig;
|
||||
|
||||
@@ -192,4 +192,6 @@ export type SlackAccountConfig = {
|
||||
export type SlackConfig = {
|
||||
/** Optional per-account Slack configuration (multi-account). */
|
||||
accounts?: Record<string, SlackAccountConfig>;
|
||||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
} & SlackAccountConfig;
|
||||
|
||||
@@ -229,4 +229,6 @@ export type TelegramDirectConfig = {
|
||||
export type TelegramConfig = {
|
||||
/** Optional per-account Telegram configuration (multi-account). */
|
||||
accounts?: Record<string, TelegramAccountConfig>;
|
||||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
} & TelegramAccountConfig;
|
||||
|
||||
@@ -99,6 +99,8 @@ export type WhatsAppConfig = WhatsAppConfigCore &
|
||||
WhatsAppSharedConfig & {
|
||||
/** Optional per-account WhatsApp configuration (multi-account). */
|
||||
accounts?: Record<string, WhatsAppAccountConfig>;
|
||||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
/** Per-action tool gating (default: true for all). */
|
||||
actions?: WhatsAppActionConfig;
|
||||
};
|
||||
|
||||
@@ -244,6 +244,7 @@ export const TelegramAccountSchema = TelegramAccountSchemaBase.superRefine((valu
|
||||
|
||||
export const TelegramConfigSchema = TelegramAccountSchemaBase.extend({
|
||||
accounts: z.record(z.string(), TelegramAccountSchema.optional()).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
normalizeTelegramStreamingConfig(value);
|
||||
requireOpenAllowFrom({
|
||||
@@ -581,6 +582,7 @@ export const DiscordAccountSchema = z
|
||||
|
||||
export const DiscordConfigSchema = DiscordAccountSchema.extend({
|
||||
accounts: z.record(z.string(), DiscordAccountSchema.optional()).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
const dmPolicy = value.dmPolicy ?? value.dm?.policy ?? "pairing";
|
||||
const allowFrom = value.allowFrom ?? value.dm?.allowFrom;
|
||||
@@ -843,6 +845,7 @@ export const SlackConfigSchema = SlackAccountSchema.safeExtend({
|
||||
webhookPath: z.string().optional().default("/slack/events"),
|
||||
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
||||
accounts: z.record(z.string(), SlackAccountSchema.optional()).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
const dmPolicy = value.dmPolicy ?? value.dm?.policy ?? "pairing";
|
||||
const allowFrom = value.allowFrom ?? value.dm?.allowFrom;
|
||||
@@ -971,6 +974,7 @@ export const SignalAccountSchema = SignalAccountSchemaBase;
|
||||
|
||||
export const SignalConfigSchema = SignalAccountSchemaBase.extend({
|
||||
accounts: z.record(z.string(), SignalAccountSchema.optional()).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
@@ -1119,6 +1123,7 @@ export const IrcAccountSchema = IrcAccountSchemaBase.superRefine((value, ctx) =>
|
||||
|
||||
export const IrcConfigSchema = IrcAccountSchemaBase.extend({
|
||||
accounts: z.record(z.string(), IrcAccountSchema.optional()).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
refineIrcAllowFromAndNickserv(value, ctx);
|
||||
if (!value.accounts) {
|
||||
@@ -1209,6 +1214,7 @@ export const IMessageAccountSchema = IMessageAccountSchemaBase;
|
||||
|
||||
export const IMessageConfigSchema = IMessageAccountSchemaBase.extend({
|
||||
accounts: z.record(z.string(), IMessageAccountSchema.optional()).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
@@ -1319,6 +1325,7 @@ export const BlueBubblesAccountSchema = BlueBubblesAccountSchemaBase;
|
||||
|
||||
export const BlueBubblesConfigSchema = BlueBubblesAccountSchemaBase.extend({
|
||||
accounts: z.record(z.string(), BlueBubblesAccountSchema.optional()).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
actions: BlueBubblesActionSchema,
|
||||
}).superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
|
||||
@@ -114,6 +114,7 @@ export const WhatsAppAccountSchema = WhatsAppSharedSchema.extend({
|
||||
|
||||
export const WhatsAppConfigSchema = WhatsAppSharedSchema.extend({
|
||||
accounts: z.record(z.string(), WhatsAppAccountSchema.optional()).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
mediaMaxMb: z.number().int().positive().optional().default(50),
|
||||
actions: z
|
||||
.object({
|
||||
|
||||
@@ -100,6 +100,39 @@ describe("LINE accounts", () => {
|
||||
});
|
||||
|
||||
describe("resolveDefaultLineAccountId", () => {
|
||||
it("prefers channels.line.defaultAccount when configured", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
line: {
|
||||
defaultAccount: "business",
|
||||
accounts: {
|
||||
business: { enabled: true },
|
||||
support: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const id = resolveDefaultLineAccountId(cfg);
|
||||
expect(id).toBe("business");
|
||||
});
|
||||
|
||||
it("normalizes channels.line.defaultAccount before lookup", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
line: {
|
||||
defaultAccount: "Business Ops",
|
||||
accounts: {
|
||||
"business-ops": { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const id = resolveDefaultLineAccountId(cfg);
|
||||
expect(id).toBe("business-ops");
|
||||
});
|
||||
|
||||
it("returns first named account when default not configured", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
@@ -115,6 +148,22 @@ describe("LINE accounts", () => {
|
||||
|
||||
expect(id).toBe("business");
|
||||
});
|
||||
|
||||
it("falls back when channels.line.defaultAccount is missing", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
line: {
|
||||
defaultAccount: "missing",
|
||||
accounts: {
|
||||
business: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const id = resolveDefaultLineAccountId(cfg);
|
||||
expect(id).toBe("business");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeAccountId", () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId as normalizeSharedAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "../routing/account-id.js";
|
||||
import { resolveAccountEntry } from "../routing/account-lookup.js";
|
||||
import type {
|
||||
@@ -124,8 +125,16 @@ export function resolveLineAccount(params: {
|
||||
accountConfig,
|
||||
});
|
||||
|
||||
const {
|
||||
accounts: _ignoredAccounts,
|
||||
defaultAccount: _ignoredDefaultAccount,
|
||||
...lineBase
|
||||
} = (lineConfig ?? {}) as LineConfig & {
|
||||
accounts?: unknown;
|
||||
defaultAccount?: unknown;
|
||||
};
|
||||
const mergedConfig: LineConfig & LineAccountConfig = {
|
||||
...lineConfig,
|
||||
...lineBase,
|
||||
...accountConfig,
|
||||
};
|
||||
|
||||
@@ -172,6 +181,15 @@ export function listLineAccountIds(cfg: OpenClawConfig): string[] {
|
||||
}
|
||||
|
||||
export function resolveDefaultLineAccountId(cfg: OpenClawConfig): string {
|
||||
const preferred = normalizeOptionalAccountId(
|
||||
(cfg.channels?.line as LineConfig | undefined)?.defaultAccount,
|
||||
);
|
||||
if (
|
||||
preferred &&
|
||||
listLineAccountIds(cfg).some((accountId) => normalizeSharedAccountId(accountId) === preferred)
|
||||
) {
|
||||
return preferred;
|
||||
}
|
||||
const ids = listLineAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
|
||||
@@ -35,6 +35,7 @@ const LineAccountConfigSchema = LineCommonConfigSchema.extend({
|
||||
|
||||
export const LineConfigSchema = LineCommonConfigSchema.extend({
|
||||
accounts: z.record(z.string(), LineAccountConfigSchema.optional()).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
groups: z.record(z.string(), LineGroupConfigSchema.optional()).optional(),
|
||||
}).strict();
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ interface LineAccountBaseConfig {
|
||||
export interface LineConfig extends LineAccountBaseConfig {
|
||||
/** Per-account overrides keyed by account id. */
|
||||
accounts?: Record<string, LineAccountConfig>;
|
||||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
}
|
||||
|
||||
export interface LineAccountConfig extends LineAccountBaseConfig {}
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
export {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "../routing/session-key.js";
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { withEnv } from "../test-utils/env.js";
|
||||
import { listTelegramAccountIds, resolveTelegramAccount } from "./accounts.js";
|
||||
import {
|
||||
listTelegramAccountIds,
|
||||
resolveDefaultTelegramAccountId,
|
||||
resolveTelegramAccount,
|
||||
} from "./accounts.js";
|
||||
|
||||
const { warnMock } = vi.hoisted(() => ({
|
||||
warnMock: vi.fn(),
|
||||
@@ -100,6 +104,47 @@ describe("resolveTelegramAccount", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDefaultTelegramAccountId", () => {
|
||||
it("prefers channels.telegram.defaultAccount when it matches a configured account", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
defaultAccount: "work",
|
||||
accounts: { default: { botToken: "tok-default" }, work: { botToken: "tok-work" } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(resolveDefaultTelegramAccountId(cfg)).toBe("work");
|
||||
});
|
||||
|
||||
it("normalizes channels.telegram.defaultAccount before lookup", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
defaultAccount: "Router D",
|
||||
accounts: { "router-d": { botToken: "tok-work" } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(resolveDefaultTelegramAccountId(cfg)).toBe("router-d");
|
||||
});
|
||||
|
||||
it("falls back when channels.telegram.defaultAccount is not configured", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
telegram: {
|
||||
defaultAccount: "missing",
|
||||
accounts: { default: { botToken: "tok-default" }, work: { botToken: "tok-work" } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(resolveDefaultTelegramAccountId(cfg)).toBe("default");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveTelegramAccount allowFrom precedence", () => {
|
||||
it("prefers accounts.default allowlists over top-level for default account", () => {
|
||||
const resolved = resolveTelegramAccount({
|
||||
|
||||
@@ -6,7 +6,11 @@ import { isTruthyEnvValue } from "../infra/env.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { resolveAccountEntry } from "../routing/account-lookup.js";
|
||||
import { listBoundAccountIds, resolveDefaultAgentBoundAccountId } from "../routing/bindings.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "../routing/session-key.js";
|
||||
import { resolveTelegramToken } from "./token.js";
|
||||
|
||||
const log = createSubsystemLogger("telegram/accounts");
|
||||
@@ -68,6 +72,13 @@ export function resolveDefaultTelegramAccountId(cfg: OpenClawConfig): string {
|
||||
if (boundDefault) {
|
||||
return boundDefault;
|
||||
}
|
||||
const preferred = normalizeOptionalAccountId(cfg.channels?.telegram?.defaultAccount);
|
||||
if (
|
||||
preferred &&
|
||||
listTelegramAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
||||
) {
|
||||
return preferred;
|
||||
}
|
||||
const ids = listTelegramAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
@@ -86,9 +97,13 @@ function resolveAccountConfig(
|
||||
function mergeTelegramAccountConfig(cfg: OpenClawConfig, accountId: string): TelegramAccountConfig {
|
||||
const {
|
||||
accounts: _ignored,
|
||||
defaultAccount: _ignoredDefaultAccount,
|
||||
groups: channelGroups,
|
||||
...base
|
||||
} = (cfg.channels?.telegram ?? {}) as TelegramAccountConfig & { accounts?: unknown };
|
||||
} = (cfg.channels?.telegram ?? {}) as TelegramAccountConfig & {
|
||||
accounts?: unknown;
|
||||
defaultAccount?: unknown;
|
||||
};
|
||||
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
||||
|
||||
// In multi-account setups, channel-level `groups` must NOT be inherited by
|
||||
|
||||
Reference in New Issue
Block a user