refactor: share account config merge helper

This commit is contained in:
Peter Steinberger
2026-03-22 18:16:24 +00:00
parent 7ae09410b7
commit ff759f991e
10 changed files with 154 additions and 32 deletions

View File

@@ -1,6 +1,7 @@
import {
createAccountActionGate,
createAccountListHelpers,
mergeAccountConfig,
} from "openclaw/plugin-sdk/account-helpers";
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
@@ -31,11 +32,10 @@ export function mergeDiscordAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): DiscordAccountConfig {
const { accounts: _ignored, ...base } = (cfg.channels?.discord ?? {}) as DiscordAccountConfig & {
accounts?: unknown;
};
const account = resolveDiscordAccountConfig(cfg, accountId) ?? {};
return { ...base, ...account };
return mergeAccountConfig<DiscordAccountConfig>({
channelConfig: cfg.channels?.discord as DiscordAccountConfig | undefined,
accountConfig: resolveDiscordAccountConfig(cfg, accountId),
});
}
export function createDiscordActionGate(params: {

View File

@@ -1,5 +1,6 @@
import {
createAccountListHelpers,
mergeAccountConfig,
normalizeAccountId,
resolveAccountEntry,
type OpenClawConfig,
@@ -26,10 +27,10 @@ function resolveAccountConfig(
}
function mergeIMessageAccountConfig(cfg: OpenClawConfig, accountId: string): IMessageAccountConfig {
const { accounts: _ignored, ...base } = (cfg.channels?.imessage ??
{}) as IMessageAccountConfig & { accounts?: unknown };
const account = resolveAccountConfig(cfg, accountId) ?? {};
return { ...base, ...account };
return mergeAccountConfig<IMessageAccountConfig>({
channelConfig: cfg.channels?.imessage as IMessageAccountConfig | undefined,
accountConfig: resolveAccountConfig(cfg, accountId),
});
}
export function resolveIMessageAccount(params: {

View File

@@ -1,5 +1,6 @@
import {
createAccountListHelpers,
mergeAccountConfig,
normalizeAccountId,
resolveAccountEntry,
type OpenClawConfig,
@@ -27,11 +28,10 @@ function resolveAccountConfig(
}
function mergeSignalAccountConfig(cfg: OpenClawConfig, accountId: string): SignalAccountConfig {
const { accounts: _ignored, ...base } = (cfg.channels?.signal ?? {}) as SignalAccountConfig & {
accounts?: unknown;
};
const account = resolveAccountConfig(cfg, accountId) ?? {};
return { ...base, ...account };
return mergeAccountConfig<SignalAccountConfig>({
channelConfig: cfg.channels?.signal as SignalAccountConfig | undefined,
accountConfig: resolveAccountConfig(cfg, accountId),
});
}
export function resolveSignalAccount(params: {

View File

@@ -1,6 +1,7 @@
import {
createAccountListHelpers,
DEFAULT_ACCOUNT_ID,
mergeAccountConfig,
normalizeAccountId,
normalizeChatType,
resolveAccountEntry,
@@ -40,11 +41,10 @@ export function mergeSlackAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): SlackAccountConfig {
const { accounts: _ignored, ...base } = (cfg.channels?.slack ?? {}) as SlackAccountConfig & {
accounts?: unknown;
};
const account = resolveAccountConfig(cfg, accountId) ?? {};
return { ...base, ...account };
return mergeAccountConfig<SlackAccountConfig>({
channelConfig: cfg.channels?.slack as SlackAccountConfig | undefined,
accountConfig: resolveAccountConfig(cfg, accountId),
});
}
export function resolveSlackAccount(params: {

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import { resolveZaloAccount } from "./accounts.js";
describe("resolveZaloAccount", () => {
it("resolves account config when account key casing differs from normalized id", () => {
const resolved = resolveZaloAccount({
cfg: {
channels: {
zalo: {
webhookUrl: "https://top.example.com",
accounts: {
Work: {
name: "Work",
webhookUrl: "https://work.example.com",
},
},
},
},
},
accountId: "work",
});
expect(resolved.accountId).toBe("work");
expect(resolved.name).toBe("Work");
expect(resolved.config.webhookUrl).toBe("https://work.example.com");
});
it("falls back to top-level config for named accounts without overrides", () => {
const resolved = resolveZaloAccount({
cfg: {
channels: {
zalo: {
enabled: true,
webhookUrl: "https://top.example.com",
accounts: {
work: {},
},
},
},
},
accountId: "work",
});
expect(resolved.accountId).toBe("work");
expect(resolved.enabled).toBe(true);
expect(resolved.config.webhookUrl).toBe("https://top.example.com");
});
});

View File

@@ -1,5 +1,6 @@
import { createAccountListHelpers } from "openclaw/plugin-sdk/account-helpers";
import { createAccountListHelpers, mergeAccountConfig } from "openclaw/plugin-sdk/account-helpers";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
import type { OpenClawConfig } from "./runtime-api.js";
import { resolveZaloToken } from "./token.js";
import type { ResolvedZaloAccount, ZaloAccountConfig, ZaloConfig } from "./types.js";
@@ -14,18 +15,20 @@ function resolveAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): ZaloAccountConfig | undefined {
const accounts = (cfg.channels?.zalo as ZaloConfig | undefined)?.accounts;
if (!accounts || typeof accounts !== "object") {
return undefined;
}
return accounts[accountId] as ZaloAccountConfig | undefined;
return resolveAccountEntry(
(cfg.channels?.zalo as ZaloConfig | undefined)?.accounts as
| Record<string, ZaloAccountConfig>
| undefined,
accountId,
);
}
function mergeZaloAccountConfig(cfg: OpenClawConfig, accountId: string): ZaloAccountConfig {
const raw = (cfg.channels?.zalo ?? {}) as ZaloConfig;
const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
const account = resolveAccountConfig(cfg, accountId) ?? {};
return { ...base, ...account };
return mergeAccountConfig<ZaloAccountConfig>({
channelConfig: cfg.channels?.zalo as ZaloAccountConfig | undefined,
accountConfig: resolveAccountConfig(cfg, accountId),
omitKeys: ["defaultAccount"],
});
}
export function resolveZaloAccount(params: {

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import { createAccountListHelpers } from "./account-helpers.js";
import { createAccountListHelpers, mergeAccountConfig } from "./account-helpers.js";
const { listConfiguredAccountIds, listAccountIds, resolveDefaultAccountId } =
createAccountListHelpers("testchannel");
@@ -109,3 +109,50 @@ describe("createAccountListHelpers", () => {
});
});
});
describe("mergeAccountConfig", () => {
it("drops accounts from the base config before merging", () => {
const merged = mergeAccountConfig<{
enabled?: boolean;
name?: string;
accounts?: Record<string, { name?: string }>;
}>({
channelConfig: {
enabled: true,
accounts: {
work: { name: "Work" },
},
},
accountConfig: {
name: "Work",
},
});
expect(merged).toEqual({
enabled: true,
name: "Work",
});
});
it("drops caller-specified keys from the base config before merging", () => {
const merged = mergeAccountConfig<{
enabled?: boolean;
defaultAccount?: string;
name?: string;
}>({
channelConfig: {
enabled: true,
defaultAccount: "work",
},
accountConfig: {
name: "Work",
},
omitKeys: ["defaultAccount"],
});
expect(merged).toEqual({
enabled: true,
name: "Work",
});
});
});

View File

@@ -60,3 +60,20 @@ export function createAccountListHelpers(
return { listConfiguredAccountIds, listAccountIds, resolveDefaultAccountId };
}
export function mergeAccountConfig<TConfig extends Record<string, unknown>>(params: {
channelConfig: TConfig | undefined;
accountConfig: Partial<TConfig> | undefined;
omitKeys?: string[];
}): TConfig {
const omitKeys = new Set(["accounts", ...(params.omitKeys ?? [])]);
const base = Object.fromEntries(
Object.entries((params.channelConfig ?? {}) as Record<string, unknown>).filter(
([key]) => !omitKeys.has(key),
),
) as TConfig;
return {
...base,
...params.accountConfig,
};
}

View File

@@ -1,2 +1,5 @@
export { createAccountListHelpers } from "../channels/plugins/account-helpers.js";
export {
createAccountListHelpers,
mergeAccountConfig,
} from "../channels/plugins/account-helpers.js";
export { createAccountActionGate } from "../channels/plugins/account-action-gate.js";

View File

@@ -1,7 +1,10 @@
export type { OpenClawConfig } from "../config/config.js";
export { createAccountActionGate } from "../channels/plugins/account-action-gate.js";
export { createAccountListHelpers } from "../channels/plugins/account-helpers.js";
export {
createAccountListHelpers,
mergeAccountConfig,
} from "../channels/plugins/account-helpers.js";
export { normalizeChatType } from "../channels/chat-type.js";
export { resolveAccountEntry } from "../routing/account-lookup.js";
export {