refactor: simplify tlon and discord setup accounts

This commit is contained in:
Peter Steinberger
2026-03-22 19:48:21 +00:00
parent 9bb5eb6c7f
commit 6fa0027c61
5 changed files with 179 additions and 68 deletions

View File

@@ -1,10 +1,26 @@
import { describe, expect, it } from "vitest";
import {
inspectDiscordSetupAccount,
listDiscordSetupAccountIds,
resolveDiscordSetupAccountConfig,
} from "./setup-account-state.js";
describe("discord setup account state", () => {
it("lists normalized setup account ids plus the implicit default account", () => {
expect(
listDiscordSetupAccountIds({
channels: {
discord: {
accounts: {
Work: { token: "work-token" },
alerts: { token: "alerts-token" },
},
},
},
}),
).toEqual(["alerts", "default", "work"]);
});
it("resolves setup account config when account key casing differs from normalized id", () => {
const resolved = resolveDiscordSetupAccountConfig({
cfg: {

View File

@@ -1,4 +1,5 @@
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import { listCombinedAccountIds } from "openclaw/plugin-sdk/account-resolution";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import {
hasConfiguredSecretInput,
@@ -43,13 +44,13 @@ function inspectConfiguredToken(value: unknown): {
export function listDiscordSetupAccountIds(cfg: OpenClawConfig): string[] {
const accounts = cfg.channels?.discord?.accounts;
const ids =
accounts && typeof accounts === "object" && !Array.isArray(accounts)
? Object.keys(accounts)
.map((accountId) => normalizeAccountId(accountId))
.filter(Boolean)
: [];
return [...new Set([DEFAULT_ACCOUNT_ID, ...ids])];
return listCombinedAccountIds({
configuredAccountIds:
accounts && typeof accounts === "object" && !Array.isArray(accounts)
? Object.keys(accounts).map((accountId) => normalizeAccountId(accountId))
: [],
implicitAccountId: DEFAULT_ACCOUNT_ID,
});
}
export function resolveDefaultDiscordSetupAccountId(cfg: OpenClawConfig): string {

View File

@@ -43,9 +43,8 @@ const tlonSetupWizardProxy = createTlonSetupWizardBase({
const tlonConfigAdapter = createHybridChannelConfigAdapter({
sectionKey: TLON_CHANNEL_ID,
listAccountIds: (cfg: OpenClawConfig) => listTlonAccountIds(cfg),
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) =>
resolveTlonAccount(cfg, accountId ?? undefined),
listAccountIds: listTlonAccountIds,
resolveAccount: resolveTlonAccount,
defaultAccountId: () => "default",
clearBaseFields: ["ship", "code", "url", "name"],
preserveSectionOnDefaultDelete: true,

View File

@@ -0,0 +1,81 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../api.js";
import { listTlonAccountIds, resolveTlonAccount } from "./types.js";
describe("tlon account helpers", () => {
it("lists named accounts and the implicit default account", () => {
const cfg = {
channels: {
tlon: {
ship: "~zod",
accounts: {
Work: { ship: "~bus" },
alerts: { ship: "~nec" },
},
},
},
} as OpenClawConfig;
expect(listTlonAccountIds(cfg)).toEqual(["alerts", "default", "work"]);
});
it("merges named account config over channel defaults", () => {
const resolved = resolveTlonAccount(
{
channels: {
tlon: {
name: "Base",
ship: "~zod",
url: "https://urbit.example.com",
code: "base-code",
dmAllowlist: ["~nec"],
groupInviteAllowlist: ["~bus"],
defaultAuthorizedShips: ["~marzod"],
accounts: {
Work: {
name: "Work",
code: "work-code",
dmAllowlist: ["~rovnys"],
},
},
},
},
} as OpenClawConfig,
"work",
);
expect(resolved.accountId).toBe("work");
expect(resolved.name).toBe("Work");
expect(resolved.ship).toBe("~zod");
expect(resolved.url).toBe("https://urbit.example.com");
expect(resolved.code).toBe("work-code");
expect(resolved.dmAllowlist).toEqual(["~rovnys"]);
expect(resolved.groupInviteAllowlist).toEqual(["~bus"]);
expect(resolved.defaultAuthorizedShips).toEqual(["~marzod"]);
expect(resolved.configured).toBe(true);
});
it("keeps the default account on channel-level config only", () => {
const resolved = resolveTlonAccount(
{
channels: {
tlon: {
ship: "~zod",
url: "https://urbit.example.com",
code: "base-code",
accounts: {
default: {
ship: "~ignored",
code: "ignored-code",
},
},
},
},
} as OpenClawConfig,
"default",
);
expect(resolved.ship).toBe("~zod");
expect(resolved.code).toBe("base-code");
});
});

View File

@@ -1,5 +1,30 @@
import {
DEFAULT_ACCOUNT_ID,
listCombinedAccountIds,
normalizeAccountId,
resolveMergedAccountConfig,
} from "openclaw/plugin-sdk/account-resolution";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
type TlonAccountConfig = {
name?: string;
enabled?: boolean;
ship?: string;
url?: string;
code?: string;
allowPrivateNetwork?: boolean;
groupChannels?: string[];
dmAllowlist?: string[];
groupInviteAllowlist?: string[];
autoDiscoverChannels?: boolean;
showModelSignature?: boolean;
autoAcceptDmInvites?: boolean;
autoAcceptGroupInvites?: boolean;
defaultAuthorizedShips?: string[];
ownerShip?: string;
accounts?: Record<string, TlonAccountConfig>;
};
export type TlonResolvedAccount = {
accountId: string;
name: string | null;
@@ -22,33 +47,38 @@ export type TlonResolvedAccount = {
ownerShip: string | null;
};
function resolveTlonChannelConfig(cfg: OpenClawConfig): TlonAccountConfig | undefined {
return cfg.channels?.tlon as TlonAccountConfig | undefined;
}
function resolveMergedTlonAccountConfig(
cfg: OpenClawConfig,
accountId: string,
): Record<string, unknown> & TlonAccountConfig {
const channel = resolveTlonChannelConfig(cfg);
if (accountId === DEFAULT_ACCOUNT_ID) {
return (channel ?? {}) as Record<string, unknown> & TlonAccountConfig;
}
return resolveMergedAccountConfig<Record<string, unknown> & TlonAccountConfig>({
channelConfig: (channel ?? {}) as Record<string, unknown> & TlonAccountConfig,
accounts: channel?.accounts as
| Record<string, Partial<Record<string, unknown> & TlonAccountConfig>>
| undefined,
accountId,
normalizeAccountId,
});
}
export function resolveTlonAccount(
cfg: OpenClawConfig,
accountId?: string | null,
): TlonResolvedAccount {
const base = cfg.channels?.tlon as
| {
name?: string;
enabled?: boolean;
ship?: string;
url?: string;
code?: string;
allowPrivateNetwork?: boolean;
groupChannels?: string[];
dmAllowlist?: string[];
groupInviteAllowlist?: string[];
autoDiscoverChannels?: boolean;
showModelSignature?: boolean;
autoAcceptDmInvites?: boolean;
autoAcceptGroupInvites?: boolean;
ownerShip?: string;
accounts?: Record<string, Record<string, unknown>>;
}
| undefined;
const resolvedAccountId = normalizeAccountId(accountId);
const base = resolveTlonChannelConfig(cfg);
if (!base) {
return {
accountId: accountId || "default",
accountId: resolvedAccountId,
name: null,
enabled: false,
configured: false,
@@ -68,42 +98,26 @@ export function resolveTlonAccount(
};
}
const useDefault = !accountId || accountId === "default";
const account = useDefault ? base : base.accounts?.[accountId];
const ship = (account?.ship ?? base.ship ?? null) as string | null;
const url = (account?.url ?? base.url ?? null) as string | null;
const code = (account?.code ?? base.code ?? null) as string | null;
const allowPrivateNetwork = (account?.allowPrivateNetwork ?? base.allowPrivateNetwork ?? null) as
| boolean
| null;
const groupChannels = (account?.groupChannels ?? base.groupChannels ?? []) as string[];
const dmAllowlist = (account?.dmAllowlist ?? base.dmAllowlist ?? []) as string[];
const groupInviteAllowlist = (account?.groupInviteAllowlist ??
base.groupInviteAllowlist ??
[]) as string[];
const autoDiscoverChannels = (account?.autoDiscoverChannels ??
base.autoDiscoverChannels ??
null) as boolean | null;
const showModelSignature = (account?.showModelSignature ?? base.showModelSignature ?? null) as
| boolean
| null;
const autoAcceptDmInvites = (account?.autoAcceptDmInvites ?? base.autoAcceptDmInvites ?? null) as
| boolean
| null;
const autoAcceptGroupInvites = (account?.autoAcceptGroupInvites ??
base.autoAcceptGroupInvites ??
null) as boolean | null;
const ownerShip = (account?.ownerShip ?? base.ownerShip ?? null) as string | null;
const defaultAuthorizedShips = ((account as Record<string, unknown>)?.defaultAuthorizedShips ??
(base as Record<string, unknown>)?.defaultAuthorizedShips ??
[]) as string[];
const merged = resolveMergedTlonAccountConfig(cfg, resolvedAccountId);
const ship = (merged.ship ?? null) as string | null;
const url = (merged.url ?? null) as string | null;
const code = (merged.code ?? null) as string | null;
const allowPrivateNetwork = (merged.allowPrivateNetwork ?? null) as boolean | null;
const groupChannels = (merged.groupChannels ?? []) as string[];
const dmAllowlist = (merged.dmAllowlist ?? []) as string[];
const groupInviteAllowlist = (merged.groupInviteAllowlist ?? []) as string[];
const autoDiscoverChannels = (merged.autoDiscoverChannels ?? null) as boolean | null;
const showModelSignature = (merged.showModelSignature ?? null) as boolean | null;
const autoAcceptDmInvites = (merged.autoAcceptDmInvites ?? null) as boolean | null;
const autoAcceptGroupInvites = (merged.autoAcceptGroupInvites ?? null) as boolean | null;
const ownerShip = (merged.ownerShip ?? null) as string | null;
const defaultAuthorizedShips = (merged.defaultAuthorizedShips ?? []) as string[];
const configured = Boolean(ship && url && code);
return {
accountId: accountId || "default",
name: (account?.name ?? base.name ?? null) as string | null,
enabled: (account?.enabled ?? base.enabled ?? true) !== false,
accountId: resolvedAccountId,
name: (merged.name ?? null) as string | null,
enabled: merged.enabled !== false,
configured,
ship,
url,
@@ -122,12 +136,12 @@ export function resolveTlonAccount(
}
export function listTlonAccountIds(cfg: OpenClawConfig): string[] {
const base = cfg.channels?.tlon as
| { ship?: string; accounts?: Record<string, Record<string, unknown>> }
| undefined;
const base = resolveTlonChannelConfig(cfg);
if (!base) {
return [];
}
const accounts = base.accounts ?? {};
return [...(base.ship ? ["default"] : []), ...Object.keys(accounts)];
return listCombinedAccountIds({
configuredAccountIds: Object.keys(base.accounts ?? {}).map(normalizeAccountId),
implicitAccountId: base.ship ? DEFAULT_ACCOUNT_ID : undefined,
});
}