fix(channel): support channel names that include numbers; legacy test names containing numbers may still appear (#796)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
Haze
2026-04-08 18:38:59 +08:00
committed by GitHub
Unverified
parent 19b3ea974b
commit c1e165d48d
6 changed files with 219 additions and 29 deletions

View File

@@ -317,6 +317,20 @@ interface ChannelAccountsView {
accounts: ChannelAccountView[];
}
function shouldIncludeRuntimeAccountId(
accountId: string,
configuredAccountIds: Set<string>,
runtimeAccount: { configured?: boolean },
): boolean {
if (configuredAccountIds.has(accountId)) {
return true;
}
// Defensive filtering: channels.status can occasionally expose transient
// runtime rows for stale sessions. Only include runtime-only accounts when
// gateway marks them as configured.
return runtimeAccount.configured === true;
}
interface ChannelTargetOptionView {
value: string;
label: string;
@@ -375,6 +389,7 @@ async function buildChannelAccountsView(ctx: HostApiContext): Promise<ChannelAcc
for (const rawChannelType of channelTypes) {
const uiChannelType = toUiChannelType(rawChannelType);
const channelAccountsFromConfig = configuredAccounts[rawChannelType]?.accountIds ?? [];
const configuredAccountIdSet = new Set(channelAccountsFromConfig);
const hasLocalConfig = configuredChannels.includes(rawChannelType) || Boolean(configuredAccounts[rawChannelType]);
const channelSection = openClawConfig.channels?.[rawChannelType];
const channelSummary =
@@ -396,9 +411,17 @@ async function buildChannelAccountsView(ctx: HostApiContext): Promise<ChannelAcc
if (!hasLocalConfig && !hasRuntimeConfigured) {
continue;
}
const runtimeAccountIds = runtimeAccounts
.map((account) => account.accountId)
.filter((accountId): accountId is string => typeof accountId === 'string' && accountId.trim().length > 0);
const runtimeAccountIds = runtimeAccounts.reduce<string[]>((acc, account) => {
const accountId = typeof account.accountId === 'string' ? account.accountId.trim() : '';
if (!accountId) {
return acc;
}
if (!shouldIncludeRuntimeAccountId(accountId, configuredAccountIdSet, account)) {
return acc;
}
acc.push(accountId);
return acc;
}, []);
const accountIds = Array.from(new Set([...channelAccountsFromConfig, ...runtimeAccountIds, defaultAccountId]));
const accounts: ChannelAccountView[] = accountIds.map((accountId) => {

View File

@@ -139,7 +139,7 @@ function slugifyAgentId(name: string): string {
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
if (!normalized) return 'agent';
if (!normalized || /^\d+$/.test(normalized)) return 'agent';
if (normalized === MAIN_AGENT_ID) return 'agent';
return normalized;
}

View File

@@ -200,11 +200,33 @@ function removePluginRegistration(currentConfig: OpenClawConfig, pluginId: strin
return modified;
}
function getChannelAccountsMap(
channelSection: ChannelConfigData | undefined,
): Record<string, ChannelConfigData> | undefined {
if (!channelSection || typeof channelSection !== 'object') return undefined;
const accounts = channelSection.accounts;
if (!accounts || typeof accounts !== 'object' || Array.isArray(accounts)) {
return undefined;
}
return accounts as Record<string, ChannelConfigData>;
}
function ensureChannelAccountsMap(
channelSection: ChannelConfigData,
): Record<string, ChannelConfigData> {
const accounts = getChannelAccountsMap(channelSection);
if (accounts) {
return accounts;
}
channelSection.accounts = {};
return channelSection.accounts as Record<string, ChannelConfigData>;
}
function channelHasConfiguredAccounts(channelSection: ChannelConfigData | undefined): boolean {
if (!channelSection || typeof channelSection !== 'object') return false;
const accounts = channelSection.accounts as Record<string, ChannelConfigData> | undefined;
if (accounts && typeof accounts === 'object') {
return Object.keys(accounts).length > 0;
const accounts = getChannelAccountsMap(channelSection);
if (accounts) {
return Object.keys(accounts).some((accountId) => accountId.trim().length > 0);
}
return Object.keys(channelSection).some((key) => !CHANNEL_TOP_LEVEL_KEYS_TO_KEEP.has(key));
}
@@ -578,7 +600,7 @@ function resolveAccountConfig(
accountId: string,
): ChannelConfigData {
if (!channelSection) return {};
const accounts = channelSection.accounts as Record<string, ChannelConfigData> | undefined;
const accounts = getChannelAccountsMap(channelSection);
return accounts?.[accountId] ?? {};
}
@@ -597,10 +619,8 @@ function migrateLegacyChannelConfigToAccounts(
): void {
const legacyPayload = getLegacyChannelPayload(channelSection);
const legacyKeys = Object.keys(legacyPayload);
const hasAccounts =
Boolean(channelSection.accounts) &&
typeof channelSection.accounts === 'object' &&
Object.keys(channelSection.accounts as Record<string, ChannelConfigData>).length > 0;
const existingAccounts = getChannelAccountsMap(channelSection);
const hasAccounts = Boolean(existingAccounts) && Object.keys(existingAccounts).length > 0;
if (legacyKeys.length === 0) {
if (hasAccounts && typeof channelSection.defaultAccount !== 'string') {
@@ -609,10 +629,7 @@ function migrateLegacyChannelConfigToAccounts(
return;
}
if (!channelSection.accounts || typeof channelSection.accounts !== 'object') {
channelSection.accounts = {};
}
const accounts = channelSection.accounts as Record<string, ChannelConfigData>;
const accounts = ensureChannelAccountsMap(channelSection);
const existingDefaultAccount = accounts[defaultAccountId] ?? {};
accounts[defaultAccountId] = {
@@ -657,7 +674,7 @@ function assertNoDuplicateCredential(
});
}
const accounts = channelSection.accounts as Record<string, ChannelConfigData> | undefined;
const accounts = getChannelAccountsMap(channelSection);
if (!accounts) return;
for (const [existingAccountId, accountCfg] of Object.entries(accounts)) {
@@ -737,10 +754,7 @@ export async function saveChannelConfig(
}
// Write credentials into accounts.<accountId>
if (!channelSection.accounts || typeof channelSection.accounts !== 'object') {
channelSection.accounts = {};
}
const accounts = channelSection.accounts as Record<string, ChannelConfigData>;
const accounts = ensureChannelAccountsMap(channelSection);
channelSection.defaultAccount =
typeof channelSection.defaultAccount === 'string' && channelSection.defaultAccount.trim()
? channelSection.defaultAccount
@@ -790,7 +804,7 @@ export async function getChannelConfig(channelType: string, accountId?: string):
if (!channelSection) return undefined;
const resolvedAccountId = accountId || DEFAULT_ACCOUNT_ID;
const accounts = channelSection.accounts as Record<string, ChannelConfigData> | undefined;
const accounts = getChannelAccountsMap(channelSection);
if (accounts?.[resolvedAccountId]) {
return accounts[resolvedAccountId];
}
@@ -868,7 +882,7 @@ export async function deleteChannelAccountConfig(channelType: string, accountId:
}
migrateLegacyChannelConfigToAccounts(channelSection, DEFAULT_ACCOUNT_ID);
const accounts = channelSection.accounts as Record<string, ChannelConfigData> | undefined;
const accounts = getChannelAccountsMap(channelSection);
if (!accounts?.[accountId]) return;
delete accounts[accountId];
@@ -959,8 +973,8 @@ export async function deleteChannelConfig(channelType: string): Promise<void> {
}
function channelHasAnyAccount(channelSection: ChannelConfigData): boolean {
const accounts = channelSection.accounts as Record<string, ChannelConfigData> | undefined;
if (accounts && typeof accounts === 'object') {
const accounts = getChannelAccountsMap(channelSection);
if (accounts) {
return Object.values(accounts).some((acc) => acc.enabled !== false);
}
return false;
@@ -1024,8 +1038,9 @@ export function listConfiguredChannelAccountsFromConfig(config: OpenClawConfig):
for (const [channelType, section] of Object.entries(config.channels)) {
if (!section || section.enabled === false) continue;
const accountIds = section.accounts && typeof section.accounts === 'object'
? Object.keys(section.accounts).filter(Boolean)
const accounts = getChannelAccountsMap(section);
const accountIds = accounts
? Object.keys(accounts).filter((accountId) => accountId.trim().length > 0)
: [];
let defaultAccountId = typeof section.defaultAccount === 'string' && section.defaultAccount.trim()
@@ -1082,7 +1097,7 @@ export async function setChannelDefaultAccount(channelType: string, accountId: s
}
migrateLegacyChannelConfigToAccounts(channelSection, DEFAULT_ACCOUNT_ID);
const accounts = channelSection.accounts as Record<string, ChannelConfigData> | undefined;
const accounts = getChannelAccountsMap(channelSection);
if (!accounts || !accounts[trimmedAccountId]) {
throw new Error(`Account "${trimmedAccountId}" is not configured for channel "${resolvedChannelType}"`);
}
@@ -1110,7 +1125,7 @@ export async function deleteAgentChannelAccounts(agentId: string, ownedChannelAc
for (const channelType of Object.keys(currentConfig.channels)) {
const section = currentConfig.channels[channelType];
migrateLegacyChannelConfigToAccounts(section, DEFAULT_ACCOUNT_ID);
const accounts = section.accounts as Record<string, ChannelConfigData> | undefined;
const accounts = getChannelAccountsMap(section);
if (!accounts?.[accountId]) continue;
if (ownedChannelAccounts && !ownedChannelAccounts.has(`${channelType}:${accountId}`)) {
continue;