mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix(channels): add optional defaultAccount routing
This commit is contained in:
@@ -118,7 +118,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Feishu/System preview prompt leakage: stop enqueuing inbound Feishu message previews as system events so user preview text is not injected into later turns as trusted `System:` context. Landed from contributor PR #31209 by @stakeswky. Thanks @stakeswky.
|
||||
- Feishu/Multi-account + reply reliability: add `channels.feishu.defaultAccount` outbound routing support with schema validation, keep quoted-message extraction text-first (post/interactive/file placeholders instead of raw JSON), route Feishu video sends as `msg_type: "file"`, and avoid websocket event blocking by using non-blocking event handling in monitor dispatch. Landed from contributor PRs #29610, #30432, #30331, and #29501. Thanks @hclsys, @bmendonca3, @patrick-yingxi-pan, and @zwffff.
|
||||
- Feishu/Typing replay suppression: skip typing indicators for stale replayed inbound messages after compaction using message-age checks with second/millisecond timestamp normalization, preventing old-message reaction floods while preserving typing for fresh messages. Landed from contributor PR #30709 by @arkyu2077. Thanks @arkyu2077.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Changes
|
||||
@@ -138,6 +137,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Feishu/Multi-account + reply reliability: add `channels.feishu.defaultAccount` outbound routing support with schema validation, prevent inbound preview text from leaking into prompt system events, keep quoted-message extraction text-first (post/interactive/file placeholders instead of raw JSON), route Feishu video sends as `msg_type: "file"`, and avoid websocket event blocking by using non-blocking event handling in monitor dispatch. Landed from contributor PRs #31209, #29610, #30432, #30331, and #29501. Thanks @stakeswky, @hclsys, @bmendonca3, @patrick-yingxi-pan, and @zwffff.
|
||||
- Feishu/Target routing + replies + dedupe: normalize provider-prefixed targets (`feishu:`/`lark:`), prefer configured `channels.feishu.defaultAccount` for tool execution, honor Feishu outbound `renderMode` in adapter text/caption sends, fall back to normal send when reply targets are withdrawn/deleted, and add synchronous in-memory dedupe guard for concurrent duplicate inbound events. Landed from contributor PRs #30428, #30438, #29958, #30444, and #29463. Thanks @bmendonca3 and @Yaxuan42.
|
||||
- Channels/Multi-account default routing: add optional `channels.<channel>.defaultAccount` default-selection support across message channels so omitted `accountId` routes to an explicit configured account instead of relying on implicit first-entry ordering (fallback behavior unchanged when unset).
|
||||
- Google Chat/Thread replies: set `messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD` on threaded sends so replies attach to existing threads instead of silently failing thread placement. Landed from contributor PR #30965 by @novan. Thanks @novan.
|
||||
- Mattermost/Private channel policy routing: map Mattermost private channel type `P` to group chat type so `groupPolicy`/`groupAllowFrom` gates apply correctly instead of being treated as open public channels. Landed from contributor PR #30891 by @BlueBirdBack. Thanks @BlueBirdBack.
|
||||
- Models/Custom provider keys: trim custom provider map keys during normalization so image-capable models remain discoverable when provider keys are configured with leading/trailing whitespace. Landed from contributor PR #31202 by @stakeswky. Thanks @stakeswky.
|
||||
|
||||
@@ -15,6 +15,8 @@ host configuration.
|
||||
|
||||
- **Channel**: `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `webchat`.
|
||||
- **AccountId**: per‑channel account instance (when supported).
|
||||
- Optional channel default account: `channels.<channel>.defaultAccount` chooses
|
||||
which account is used when an outbound path does not specify `accountId`.
|
||||
- **AgentId**: an isolated workspace + session store (“brain”).
|
||||
- **SessionKey**: the bucket key used to store context and control concurrency.
|
||||
|
||||
|
||||
@@ -197,6 +197,16 @@ Channels that support **multiple accounts** (e.g. WhatsApp) use `accountId` to i
|
||||
each login. Each `accountId` can be routed to a different agent, so one server can host
|
||||
multiple phone numbers without mixing sessions.
|
||||
|
||||
If you want a channel-wide default account when `accountId` is omitted, set
|
||||
`channels.<channel>.defaultAccount` (optional). When unset, OpenClaw falls back
|
||||
to `default` if present, otherwise the first configured account id (sorted).
|
||||
|
||||
Common channels supporting this pattern include:
|
||||
|
||||
- `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`
|
||||
- `irc`, `line`, `googlechat`, `mattermost`, `matrix`, `nextcloud-talk`
|
||||
- `bluebubbles`, `zalo`, `zalouser`, `nostr`, `feishu`
|
||||
|
||||
## Concepts
|
||||
|
||||
- `agentId`: one “brain” (workspace, per-agent auth, per-agent session store).
|
||||
|
||||
@@ -143,6 +143,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
||||
```
|
||||
|
||||
- Outbound commands default to account `default` if present; otherwise the first configured account id (sorted).
|
||||
- Optional `channels.whatsapp.defaultAccount` overrides that fallback default account selection when it matches a configured account id.
|
||||
- Legacy single-account Baileys auth dir is migrated by `openclaw doctor` into `whatsapp/default`.
|
||||
- Per-account overrides: `channels.whatsapp.accounts.<id>.sendReadReceipts`, `channels.whatsapp.accounts.<id>.dmPolicy`, `channels.whatsapp.accounts.<id>.allowFrom`.
|
||||
|
||||
@@ -203,6 +204,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
||||
```
|
||||
|
||||
- Bot token: `channels.telegram.botToken` or `channels.telegram.tokenFile`, with `TELEGRAM_BOT_TOKEN` as fallback for the default account.
|
||||
- Optional `channels.telegram.defaultAccount` overrides default account selection when it matches a configured account id.
|
||||
- `configWrites: false` blocks Telegram-initiated config writes (supergroup ID migrations, `/config set|unset`).
|
||||
- Telegram stream previews use `sendMessage` + `editMessageText` (works in direct and group chats).
|
||||
- Retry policy: see [Retry policy](/concepts/retry).
|
||||
@@ -299,6 +301,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
||||
```
|
||||
|
||||
- Token: `channels.discord.token`, with `DISCORD_BOT_TOKEN` as fallback for the default account.
|
||||
- Optional `channels.discord.defaultAccount` overrides default account selection when it matches a configured account id.
|
||||
- Use `user:<id>` (DM) or `channel:<id>` (guild channel) for delivery targets; bare numeric IDs are rejected.
|
||||
- Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged name (no `#`). Prefer guild IDs.
|
||||
- Bot-authored messages are ignored by default. `allowBots: true` enables them (own messages still filtered).
|
||||
@@ -410,6 +413,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
||||
- **Socket mode** requires both `botToken` and `appToken` (`SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` for default account env fallback).
|
||||
- **HTTP mode** requires `botToken` plus `signingSecret` (at root or per-account).
|
||||
- `configWrites: false` blocks Slack-initiated config writes.
|
||||
- Optional `channels.slack.defaultAccount` overrides default account selection when it matches a configured account id.
|
||||
- `channels.slack.streaming` is the canonical stream mode key. Legacy `streamMode` and boolean `streaming` values are auto-migrated.
|
||||
- Use `user:<id>` (DM) or `channel:<id>` for delivery targets.
|
||||
|
||||
@@ -450,6 +454,7 @@ Chat modes: `oncall` (respond on @-mention, default), `onmessage` (every message
|
||||
|
||||
- `channels.mattermost.configWrites`: allow or deny Mattermost-initiated config writes.
|
||||
- `channels.mattermost.requireMention`: require `@mention` before replying in channels.
|
||||
- Optional `channels.mattermost.defaultAccount` overrides default account selection when it matches a configured account id.
|
||||
|
||||
### Signal
|
||||
|
||||
@@ -474,6 +479,7 @@ Chat modes: `oncall` (respond on @-mention, default), `onmessage` (every message
|
||||
|
||||
- `channels.signal.account`: pin channel startup to a specific Signal account identity.
|
||||
- `channels.signal.configWrites`: allow or deny Signal-initiated config writes.
|
||||
- Optional `channels.signal.defaultAccount` overrides default account selection when it matches a configured account id.
|
||||
|
||||
### BlueBubbles
|
||||
|
||||
@@ -493,6 +499,7 @@ BlueBubbles is the recommended iMessage path (plugin-backed, configured under `c
|
||||
```
|
||||
|
||||
- Core key paths covered here: `channels.bluebubbles`, `channels.bluebubbles.dmPolicy`.
|
||||
- Optional `channels.bluebubbles.defaultAccount` overrides default account selection when it matches a configured account id.
|
||||
- Full BlueBubbles channel configuration is documented in [BlueBubbles](/channels/bluebubbles).
|
||||
|
||||
### iMessage
|
||||
@@ -521,6 +528,8 @@ OpenClaw spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required.
|
||||
}
|
||||
```
|
||||
|
||||
- Optional `channels.imessage.defaultAccount` overrides default account selection when it matches a configured account id.
|
||||
|
||||
- Requires Full Disk Access to the Messages DB.
|
||||
- Prefer `chat_id:<id>` targets. Use `imsg chats --limit 20` to list chats.
|
||||
- `cliPath` can point to an SSH wrapper; set `remoteHost` (`host` or `user@host`) for SCP attachment fetching.
|
||||
@@ -581,6 +590,7 @@ IRC is extension-backed and configured under `channels.irc`.
|
||||
```
|
||||
|
||||
- Core key paths covered here: `channels.irc`, `channels.irc.dmPolicy`, `channels.irc.configWrites`, `channels.irc.nickserv.*`.
|
||||
- Optional `channels.irc.defaultAccount` overrides default account selection when it matches a configured account id.
|
||||
- Full IRC channel configuration (host/port/TLS/channels/allowlists/mention gating) is documented in [IRC](/channels/irc).
|
||||
|
||||
### Multi-account (all channels)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "openclaw/plugin-sdk/account-id";
|
||||
import { normalizeBlueBubblesServerUrl, type BlueBubblesAccountConfig } from "./types.js";
|
||||
|
||||
export type ResolvedBlueBubblesAccount = {
|
||||
@@ -28,6 +32,13 @@ export function listBlueBubblesAccountIds(cfg: OpenClawConfig): string[] {
|
||||
}
|
||||
|
||||
export function resolveDefaultBlueBubblesAccountId(cfg: OpenClawConfig): string {
|
||||
const preferred = normalizeOptionalAccountId(cfg.channels?.bluebubbles?.defaultAccount);
|
||||
if (
|
||||
preferred &&
|
||||
listBlueBubblesAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
||||
) {
|
||||
return preferred;
|
||||
}
|
||||
const ids = listBlueBubblesAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
@@ -52,8 +63,9 @@ function mergeBlueBubblesAccountConfig(
|
||||
): BlueBubblesAccountConfig {
|
||||
const base = (cfg.channels?.bluebubbles ?? {}) as BlueBubblesAccountConfig & {
|
||||
accounts?: unknown;
|
||||
defaultAccount?: unknown;
|
||||
};
|
||||
const { accounts: _ignored, ...rest } = base;
|
||||
const { accounts: _ignored, defaultAccount: _ignoredDefaultAccount, ...rest } = base;
|
||||
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
||||
const chunkMode = account.chunkMode ?? rest.chunkMode ?? "length";
|
||||
return { ...rest, ...account, chunkMode };
|
||||
|
||||
@@ -61,5 +61,6 @@ const bluebubblesAccountSchema = z
|
||||
|
||||
export const BlueBubblesConfigSchema = bluebubblesAccountSchema.extend({
|
||||
accounts: z.object({}).catchall(bluebubblesAccountSchema).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
actions: bluebubblesActionSchema,
|
||||
});
|
||||
|
||||
@@ -75,6 +75,8 @@ export type BlueBubblesActionConfig = {
|
||||
export type BlueBubblesConfig = {
|
||||
/** Optional per-account BlueBubbles configuration (multi-account). */
|
||||
accounts?: Record<string, BlueBubblesAccountConfig>;
|
||||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
/** Per-action tool gating (default: true for all). */
|
||||
actions?: BlueBubblesActionConfig;
|
||||
} & BlueBubblesAccountConfig;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "openclaw/plugin-sdk/account-id";
|
||||
import type { GoogleChatAccountConfig } from "./types.config.js";
|
||||
|
||||
export type GoogleChatCredentialSource = "file" | "inline" | "env" | "none";
|
||||
@@ -35,8 +39,12 @@ export function listGoogleChatAccountIds(cfg: OpenClawConfig): string[] {
|
||||
|
||||
export function resolveDefaultGoogleChatAccountId(cfg: OpenClawConfig): string {
|
||||
const channel = cfg.channels?.["googlechat"];
|
||||
if (channel?.defaultAccount?.trim()) {
|
||||
return channel.defaultAccount.trim();
|
||||
const preferred = normalizeOptionalAccountId(channel?.defaultAccount);
|
||||
if (
|
||||
preferred &&
|
||||
listGoogleChatAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
||||
) {
|
||||
return preferred;
|
||||
}
|
||||
const ids = listGoogleChatAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "openclaw/plugin-sdk/account-id";
|
||||
import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js";
|
||||
|
||||
const TRUTHY_ENV = new Set(["true", "1", "yes", "on"]);
|
||||
@@ -78,8 +82,13 @@ function resolveAccountConfig(cfg: CoreConfig, accountId: string): IrcAccountCon
|
||||
}
|
||||
|
||||
function mergeIrcAccountConfig(cfg: CoreConfig, accountId: string): IrcAccountConfig {
|
||||
const { accounts: _ignored, ...base } = (cfg.channels?.irc ?? {}) as IrcAccountConfig & {
|
||||
const {
|
||||
accounts: _ignored,
|
||||
defaultAccount: _ignoredDefaultAccount,
|
||||
...base
|
||||
} = (cfg.channels?.irc ?? {}) as IrcAccountConfig & {
|
||||
accounts?: unknown;
|
||||
defaultAccount?: unknown;
|
||||
};
|
||||
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
||||
const merged: IrcAccountConfig = { ...base, ...account };
|
||||
@@ -155,6 +164,13 @@ export function listIrcAccountIds(cfg: CoreConfig): string[] {
|
||||
}
|
||||
|
||||
export function resolveDefaultIrcAccountId(cfg: CoreConfig): string {
|
||||
const preferred = normalizeOptionalAccountId(cfg.channels?.irc?.defaultAccount);
|
||||
if (
|
||||
preferred &&
|
||||
listIrcAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
||||
) {
|
||||
return preferred;
|
||||
}
|
||||
const ids = listIrcAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
|
||||
@@ -80,6 +80,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) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
|
||||
@@ -68,6 +68,7 @@ export type IrcAccountConfig = {
|
||||
|
||||
export type IrcConfig = IrcAccountConfig & {
|
||||
accounts?: Record<string, IrcAccountConfig>;
|
||||
defaultAccount?: string;
|
||||
};
|
||||
|
||||
export type CoreConfig = OpenClawConfig & {
|
||||
|
||||
@@ -37,6 +37,8 @@ const matrixRoomSchema = z
|
||||
export const MatrixConfigSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
accounts: z.record(z.string(), z.unknown()).optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
homeserver: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { CoreConfig } from "../types.js";
|
||||
import { resolveMatrixAccount } from "./accounts.js";
|
||||
import { resolveDefaultMatrixAccountId, resolveMatrixAccount } from "./accounts.js";
|
||||
|
||||
vi.mock("./credentials.js", () => ({
|
||||
loadMatrixCredentials: () => null,
|
||||
@@ -80,3 +80,52 @@ describe("resolveMatrixAccount", () => {
|
||||
expect(account.configured).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDefaultMatrixAccountId", () => {
|
||||
it("prefers channels.matrix.defaultAccount when it matches a configured account", () => {
|
||||
const cfg: CoreConfig = {
|
||||
channels: {
|
||||
matrix: {
|
||||
defaultAccount: "alerts",
|
||||
accounts: {
|
||||
default: { homeserver: "https://matrix.example.org", accessToken: "tok-default" },
|
||||
alerts: { homeserver: "https://matrix.example.org", accessToken: "tok-alerts" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(resolveDefaultMatrixAccountId(cfg)).toBe("alerts");
|
||||
});
|
||||
|
||||
it("normalizes channels.matrix.defaultAccount before lookup", () => {
|
||||
const cfg: CoreConfig = {
|
||||
channels: {
|
||||
matrix: {
|
||||
defaultAccount: "Team Alerts",
|
||||
accounts: {
|
||||
"team-alerts": { homeserver: "https://matrix.example.org", accessToken: "tok-alerts" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(resolveDefaultMatrixAccountId(cfg)).toBe("team-alerts");
|
||||
});
|
||||
|
||||
it("falls back when channels.matrix.defaultAccount is not configured", () => {
|
||||
const cfg: CoreConfig = {
|
||||
channels: {
|
||||
matrix: {
|
||||
defaultAccount: "missing",
|
||||
accounts: {
|
||||
default: { homeserver: "https://matrix.example.org", accessToken: "tok-default" },
|
||||
alerts: { homeserver: "https://matrix.example.org", accessToken: "tok-alerts" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(resolveDefaultMatrixAccountId(cfg)).toBe("default");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "openclaw/plugin-sdk/account-id";
|
||||
import type { CoreConfig, MatrixConfig } from "../types.js";
|
||||
import { resolveMatrixConfigForAccount } from "./client.js";
|
||||
import { credentialsMatchConfig, loadMatrixCredentials } from "./credentials.js";
|
||||
@@ -16,6 +20,7 @@ function mergeAccountConfig(base: MatrixConfig, account: MatrixConfig): MatrixCo
|
||||
}
|
||||
// Don't propagate the accounts map into the merged per-account config
|
||||
delete (merged as Record<string, unknown>).accounts;
|
||||
delete (merged as Record<string, unknown>).defaultAccount;
|
||||
return merged;
|
||||
}
|
||||
|
||||
@@ -54,6 +59,13 @@ export function listMatrixAccountIds(cfg: CoreConfig): string[] {
|
||||
}
|
||||
|
||||
export function resolveDefaultMatrixAccountId(cfg: CoreConfig): string {
|
||||
const preferred = normalizeOptionalAccountId(cfg.channels?.matrix?.defaultAccount);
|
||||
if (
|
||||
preferred &&
|
||||
listMatrixAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
||||
) {
|
||||
return preferred;
|
||||
}
|
||||
const ids = listMatrixAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
|
||||
@@ -49,6 +49,8 @@ export type MatrixConfig = {
|
||||
enabled?: boolean;
|
||||
/** Multi-account configuration keyed by account ID. */
|
||||
accounts?: Record<string, MatrixAccountConfig>;
|
||||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
/** Matrix homeserver URL (https://matrix.example.org). */
|
||||
homeserver?: string;
|
||||
/** Matrix user id (@user:server). */
|
||||
|
||||
@@ -50,6 +50,7 @@ const MattermostAccountSchema = MattermostAccountSchemaBase.superRefine((value,
|
||||
|
||||
export const MattermostConfigSchema = MattermostAccountSchemaBase.extend({
|
||||
accounts: z.record(z.string(), MattermostAccountSchema.optional()).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "openclaw/plugin-sdk/account-id";
|
||||
import type { MattermostAccountConfig, MattermostChatMode } from "../types.js";
|
||||
import { normalizeMattermostBaseUrl } from "./client.js";
|
||||
|
||||
@@ -40,6 +44,13 @@ export function listMattermostAccountIds(cfg: OpenClawConfig): string[] {
|
||||
}
|
||||
|
||||
export function resolveDefaultMattermostAccountId(cfg: OpenClawConfig): string {
|
||||
const preferred = normalizeOptionalAccountId(cfg.channels?.mattermost?.defaultAccount);
|
||||
if (
|
||||
preferred &&
|
||||
listMattermostAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
||||
) {
|
||||
return preferred;
|
||||
}
|
||||
const ids = listMattermostAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
@@ -62,8 +73,14 @@ function mergeMattermostAccountConfig(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
): MattermostAccountConfig {
|
||||
const { accounts: _ignored, ...base } = (cfg.channels?.mattermost ??
|
||||
{}) as MattermostAccountConfig & { accounts?: unknown };
|
||||
const {
|
||||
accounts: _ignored,
|
||||
defaultAccount: _ignoredDefaultAccount,
|
||||
...base
|
||||
} = (cfg.channels?.mattermost ?? {}) as MattermostAccountConfig & {
|
||||
accounts?: unknown;
|
||||
defaultAccount?: unknown;
|
||||
};
|
||||
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
||||
return { ...base, ...account };
|
||||
}
|
||||
|
||||
@@ -59,4 +59,6 @@ export type MattermostAccountConfig = {
|
||||
export type MattermostConfig = {
|
||||
/** Optional per-account Mattermost configuration (multi-account). */
|
||||
accounts?: Record<string, MattermostAccountConfig>;
|
||||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
} & MattermostAccountConfig;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "openclaw/plugin-sdk/account-id";
|
||||
import type { CoreConfig, NextcloudTalkAccountConfig } from "./types.js";
|
||||
|
||||
function isTruthyEnvValue(value?: string): boolean {
|
||||
@@ -48,6 +52,15 @@ export function listNextcloudTalkAccountIds(cfg: CoreConfig): string[] {
|
||||
}
|
||||
|
||||
export function resolveDefaultNextcloudTalkAccountId(cfg: CoreConfig): string {
|
||||
const preferred = normalizeOptionalAccountId(cfg.channels?.["nextcloud-talk"]?.defaultAccount);
|
||||
if (
|
||||
preferred &&
|
||||
listNextcloudTalkAccountIds(cfg).some(
|
||||
(accountId) => normalizeAccountId(accountId) === preferred,
|
||||
)
|
||||
) {
|
||||
return preferred;
|
||||
}
|
||||
const ids = listNextcloudTalkAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
@@ -76,8 +89,14 @@ function mergeNextcloudTalkAccountConfig(
|
||||
cfg: CoreConfig,
|
||||
accountId: string,
|
||||
): NextcloudTalkAccountConfig {
|
||||
const { accounts: _ignored, ...base } = (cfg.channels?.["nextcloud-talk"] ??
|
||||
{}) as NextcloudTalkAccountConfig & { accounts?: unknown };
|
||||
const {
|
||||
accounts: _ignored,
|
||||
defaultAccount: _ignoredDefaultAccount,
|
||||
...base
|
||||
} = (cfg.channels?.["nextcloud-talk"] ?? {}) as NextcloudTalkAccountConfig & {
|
||||
accounts?: unknown;
|
||||
defaultAccount?: unknown;
|
||||
};
|
||||
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
||||
return { ...base, ...account };
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ export const NextcloudTalkAccountSchema = NextcloudTalkAccountSchemaBase.superRe
|
||||
|
||||
export const NextcloudTalkConfigSchema = NextcloudTalkAccountSchemaBase.extend({
|
||||
accounts: z.record(z.string(), NextcloudTalkAccountSchema.optional()).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
|
||||
@@ -79,6 +79,8 @@ export type NextcloudTalkAccountConfig = {
|
||||
export type NextcloudTalkConfig = {
|
||||
/** Optional per-account Nextcloud Talk configuration (multi-account). */
|
||||
accounts?: Record<string, NextcloudTalkAccountConfig>;
|
||||
/** Optional default account id when multiple accounts are configured. */
|
||||
defaultAccount?: string;
|
||||
} & NextcloudTalkAccountConfig;
|
||||
|
||||
export type CoreConfig = {
|
||||
|
||||
@@ -60,6 +60,9 @@ export const NostrConfigSchema = z.object({
|
||||
/** Account name (optional display name) */
|
||||
name: z.string().optional(),
|
||||
|
||||
/** Optional default account id for routing/account selection. */
|
||||
defaultAccount: z.string().optional(),
|
||||
|
||||
/** Whether this channel is enabled */
|
||||
enabled: z.boolean().optional(),
|
||||
|
||||
|
||||
@@ -22,6 +22,15 @@ describe("listNostrAccountIds", () => {
|
||||
};
|
||||
expect(listNostrAccountIds(cfg)).toEqual(["default"]);
|
||||
});
|
||||
|
||||
it("returns configured defaultAccount when privateKey is configured", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
nostr: { privateKey: TEST_PRIVATE_KEY, defaultAccount: "work" },
|
||||
},
|
||||
};
|
||||
expect(listNostrAccountIds(cfg)).toEqual(["work"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDefaultNostrAccountId", () => {
|
||||
@@ -38,6 +47,15 @@ describe("resolveDefaultNostrAccountId", () => {
|
||||
const cfg = { channels: {} };
|
||||
expect(resolveDefaultNostrAccountId(cfg)).toBe("default");
|
||||
});
|
||||
|
||||
it("prefers configured defaultAccount when present", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
nostr: { privateKey: TEST_PRIVATE_KEY, defaultAccount: "work" },
|
||||
},
|
||||
};
|
||||
expect(resolveDefaultNostrAccountId(cfg)).toBe("work");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveNostrAccount", () => {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "openclaw/plugin-sdk/account-id";
|
||||
import type { NostrProfile } from "./config-schema.js";
|
||||
import { getPublicKeyFromPrivate } from "./nostr-bus.js";
|
||||
import { DEFAULT_RELAYS } from "./nostr-bus.js";
|
||||
@@ -6,6 +11,7 @@ import { DEFAULT_RELAYS } from "./nostr-bus.js";
|
||||
export interface NostrAccountConfig {
|
||||
enabled?: boolean;
|
||||
name?: string;
|
||||
defaultAccount?: string;
|
||||
privateKey?: string;
|
||||
relays?: string[];
|
||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||
@@ -25,7 +31,12 @@ export interface ResolvedNostrAccount {
|
||||
config: NostrAccountConfig;
|
||||
}
|
||||
|
||||
const DEFAULT_ACCOUNT_ID = "default";
|
||||
function resolveConfiguredDefaultNostrAccountId(cfg: OpenClawConfig): string | undefined {
|
||||
const nostrCfg = (cfg.channels as Record<string, unknown> | undefined)?.nostr as
|
||||
| NostrAccountConfig
|
||||
| undefined;
|
||||
return normalizeOptionalAccountId(nostrCfg?.defaultAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all configured Nostr account IDs
|
||||
@@ -37,7 +48,7 @@ export function listNostrAccountIds(cfg: OpenClawConfig): string[] {
|
||||
|
||||
// If privateKey is configured at top level, we have a default account
|
||||
if (nostrCfg?.privateKey) {
|
||||
return [DEFAULT_ACCOUNT_ID];
|
||||
return [resolveConfiguredDefaultNostrAccountId(cfg) ?? DEFAULT_ACCOUNT_ID];
|
||||
}
|
||||
|
||||
return [];
|
||||
@@ -47,6 +58,10 @@ export function listNostrAccountIds(cfg: OpenClawConfig): string[] {
|
||||
* Get the default account ID
|
||||
*/
|
||||
export function resolveDefaultNostrAccountId(cfg: OpenClawConfig): string {
|
||||
const preferred = resolveConfiguredDefaultNostrAccountId(cfg);
|
||||
if (preferred) {
|
||||
return preferred;
|
||||
}
|
||||
const ids = listNostrAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
@@ -61,7 +76,7 @@ export function resolveNostrAccount(opts: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
}): ResolvedNostrAccount {
|
||||
const accountId = opts.accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
const accountId = normalizeAccountId(opts.accountId ?? resolveDefaultNostrAccountId(opts.cfg));
|
||||
const nostrCfg = (opts.cfg.channels as Record<string, unknown> | undefined)?.nostr as
|
||||
| NostrAccountConfig
|
||||
| undefined;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "openclaw/plugin-sdk/account-id";
|
||||
import { resolveZaloToken } from "./token.js";
|
||||
import type { ResolvedZaloAccount, ZaloAccountConfig, ZaloConfig } from "./types.js";
|
||||
|
||||
@@ -23,8 +27,12 @@ export function listZaloAccountIds(cfg: OpenClawConfig): string[] {
|
||||
|
||||
export function resolveDefaultZaloAccountId(cfg: OpenClawConfig): string {
|
||||
const zaloConfig = cfg.channels?.zalo as ZaloConfig | undefined;
|
||||
if (zaloConfig?.defaultAccount?.trim()) {
|
||||
return zaloConfig.defaultAccount.trim();
|
||||
const preferred = normalizeOptionalAccountId(zaloConfig?.defaultAccount);
|
||||
if (
|
||||
preferred &&
|
||||
listZaloAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
||||
) {
|
||||
return preferred;
|
||||
}
|
||||
const ids = listZaloAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "openclaw/plugin-sdk/account-id";
|
||||
import type { ResolvedZalouserAccount, ZalouserAccountConfig, ZalouserConfig } from "./types.js";
|
||||
import { runZca, parseJsonOutput } from "./zca.js";
|
||||
|
||||
@@ -21,8 +25,12 @@ export function listZalouserAccountIds(cfg: OpenClawConfig): string[] {
|
||||
|
||||
export function resolveDefaultZalouserAccountId(cfg: OpenClawConfig): string {
|
||||
const zalouserConfig = cfg.channels?.zalouser as ZalouserConfig | undefined;
|
||||
if (zalouserConfig?.defaultAccount?.trim()) {
|
||||
return zalouserConfig.defaultAccount.trim();
|
||||
const preferred = normalizeOptionalAccountId(zalouserConfig?.defaultAccount);
|
||||
if (
|
||||
preferred &&
|
||||
listZalouserAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
||||
) {
|
||||
return preferred;
|
||||
}
|
||||
const ids = listZalouserAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
|
||||
@@ -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