Upgrade openclaw to 4.9 (#804)

This commit is contained in:
paisley
2026-04-09 18:51:06 +08:00
committed by GitHub
Unverified
parent 96c9f6fe5b
commit 467fcf7e92
8 changed files with 3101 additions and 572 deletions

View File

@@ -26,6 +26,11 @@ const WECOM_PLUGIN_ID = 'wecom';
const WECHAT_PLUGIN_ID = OPENCLAW_WECHAT_CHANNEL_TYPE;
const FEISHU_PLUGIN_ID_CANDIDATES = ['openclaw-lark', 'feishu-openclaw-plugin'] as const;
const DEFAULT_ACCOUNT_ID = 'default';
// Channels whose plugin schema uses additionalProperties:false, meaning
// credential keys MUST NOT appear at the top level of `channels.<type>`.
// All other channels get the default account mirrored to the top level
// so their runtime/plugin can discover the credentials.
const CHANNELS_EXCLUDING_TOP_LEVEL_MIRROR = new Set(['dingtalk']);
const CHANNEL_TOP_LEVEL_KEYS_TO_KEEP = new Set(['accounts', 'defaultAccount', 'enabled']);
const WECHAT_STATE_DIR = join(OPENCLAW_DIR, WECHAT_PLUGIN_ID);
const WECHAT_ACCOUNT_INDEX_FILE = join(WECHAT_STATE_DIR, 'accounts.json');
@@ -753,36 +758,50 @@ export async function saveChannelConfig(
}
}
// Write credentials into accounts.<accountId>
const accounts = ensureChannelAccountsMap(channelSection);
channelSection.defaultAccount =
typeof channelSection.defaultAccount === 'string' && channelSection.defaultAccount.trim()
? channelSection.defaultAccount
: resolvedAccountId;
accounts[resolvedAccountId] = {
...accounts[resolvedAccountId],
...transformedConfig,
enabled: transformedConfig.enabled ?? true,
};
// Most OpenClaw channel plugins read the default account's credentials
// from the top level of `channels.<type>` (e.g. channels.feishu.appId),
// not from `accounts.default`. Mirror them there so plugins can discover
// the credentials correctly.
// This MUST run unconditionally (not just when saving the default account)
// because migrateLegacyChannelConfigToAccounts() above strips top-level
// credential keys on every invocation. Without this, saving a non-default
// account (e.g. a sub-agent's Feishu bot) leaves the top-level credentials
// missing, breaking plugins that only read from the top level.
const mirroredAccountId =
typeof channelSection.defaultAccount === 'string' && channelSection.defaultAccount.trim()
? channelSection.defaultAccount
: resolvedAccountId;
const defaultAccountData = accounts[mirroredAccountId] ?? accounts[resolvedAccountId] ?? accounts[DEFAULT_ACCOUNT_ID];
if (defaultAccountData) {
for (const [key, value] of Object.entries(defaultAccountData)) {
// ── Strict-schema channels (e.g. dingtalk) ──────────────────────
// These plugins declare additionalProperties:false and do NOT
// recognise `accounts` / `defaultAccount`. Write credentials
// flat to the channel root and strip the multi-account keys.
if (CHANNELS_EXCLUDING_TOP_LEVEL_MIRROR.has(resolvedChannelType)) {
for (const [key, value] of Object.entries(transformedConfig)) {
channelSection[key] = value;
}
channelSection.enabled = transformedConfig.enabled ?? channelSection.enabled ?? true;
// Remove keys the strict schema rejects
delete channelSection.accounts;
delete channelSection.defaultAccount;
} else {
// ── Normal channels ──────────────────────────────────────────
// Write into accounts.<accountId> (multi-account support).
const accounts = ensureChannelAccountsMap(channelSection);
channelSection.defaultAccount =
typeof channelSection.defaultAccount === 'string' && channelSection.defaultAccount.trim()
? channelSection.defaultAccount
: resolvedAccountId;
accounts[resolvedAccountId] = {
...accounts[resolvedAccountId],
...transformedConfig,
enabled: transformedConfig.enabled ?? true,
};
// Keep channel-level enabled explicit so callers/tests that
// read channels.<type>.enabled still work.
channelSection.enabled = transformedConfig.enabled ?? channelSection.enabled ?? true;
// Most OpenClaw channel plugins/built-ins also read the default
// account's credentials from the top level of `channels.<type>`
// (e.g. channels.feishu.appId). Mirror them there so the
// runtime can discover them.
const mirroredAccountId =
typeof channelSection.defaultAccount === 'string' && channelSection.defaultAccount.trim()
? channelSection.defaultAccount
: resolvedAccountId;
const defaultAccountData = accounts[mirroredAccountId] ?? accounts[resolvedAccountId] ?? accounts[DEFAULT_ACCOUNT_ID];
if (defaultAccountData) {
for (const [key, value] of Object.entries(defaultAccountData)) {
channelSection[key] = value;
}
}
}
await writeOpenClawConfig(currentConfig);
@@ -881,9 +900,29 @@ export async function deleteChannelAccountConfig(channelType: string, accountId:
return;
}
// Strict-schema channels have no `accounts` structure — delete means
// removing the entire channel section.
if (CHANNELS_EXCLUDING_TOP_LEVEL_MIRROR.has(resolvedChannelType)) {
delete currentConfig.channels![resolvedChannelType];
syncBuiltinChannelsWithPluginAllowlist(currentConfig);
await writeOpenClawConfig(currentConfig);
logger.info('Deleted strict-schema channel config', { channelType: resolvedChannelType, accountId });
return;
}
migrateLegacyChannelConfigToAccounts(channelSection, DEFAULT_ACCOUNT_ID);
const accounts = getChannelAccountsMap(channelSection);
if (!accounts?.[accountId]) return;
if (!accounts?.[accountId]) {
// Account not found; just ensure top-level mirror is consistent
const mirroredAccountId = typeof channelSection.defaultAccount === 'string' && channelSection.defaultAccount.trim() ? channelSection.defaultAccount : DEFAULT_ACCOUNT_ID;
const defaultAccountData = accounts?.[mirroredAccountId] ?? accounts?.[DEFAULT_ACCOUNT_ID];
if (defaultAccountData) {
for (const [key, value] of Object.entries(defaultAccountData)) {
channelSection[key] = value;
}
}
return;
}
delete accounts[accountId];
@@ -905,6 +944,7 @@ export async function deleteChannelAccountConfig(channelType: string, accountId:
}
// Re-mirror default account credentials to top level after migration
// stripped them (same rationale as saveChannelConfig).
// (Strict-schema channels already returned above, so this is safe.)
const mirroredAccountId =
typeof channelSection.defaultAccount === 'string' && channelSection.defaultAccount.trim()
? channelSection.defaultAccount
@@ -1104,6 +1144,7 @@ export async function setChannelDefaultAccount(channelType: string, accountId: s
channelSection.defaultAccount = trimmedAccountId;
// Strict-schema channels don't use defaultAccount — always mirror for others
const defaultAccountData = accounts[trimmedAccountId];
for (const [key, value] of Object.entries(defaultAccountData)) {
channelSection[key] = value;
@@ -1126,8 +1167,18 @@ export async function deleteAgentChannelAccounts(agentId: string, ownedChannelAc
const section = currentConfig.channels[channelType];
migrateLegacyChannelConfigToAccounts(section, DEFAULT_ACCOUNT_ID);
const accounts = getChannelAccountsMap(section);
if (!accounts?.[accountId]) continue;
if (ownedChannelAccounts && !ownedChannelAccounts.has(`${channelType}:${accountId}`)) {
if (!accounts?.[accountId] || (ownedChannelAccounts && !ownedChannelAccounts.has(`${channelType}:${accountId}`))) {
// Strict-schema channels have no accounts map; skip them.
// For normal channels, ensure top-level mirror is consistent.
if (!CHANNELS_EXCLUDING_TOP_LEVEL_MIRROR.has(channelType)) {
const mirroredAccountId = typeof section.defaultAccount === 'string' && section.defaultAccount.trim() ? section.defaultAccount : DEFAULT_ACCOUNT_ID;
const defaultAccountData = accounts?.[mirroredAccountId] ?? accounts?.[DEFAULT_ACCOUNT_ID];
if (defaultAccountData) {
for (const [key, value] of Object.entries(defaultAccountData)) {
section[key] = value;
}
}
}
continue;
}