Upgrade openclaw to 4.9 (#804)
This commit is contained in:
committed by
GitHub
Unverified
parent
96c9f6fe5b
commit
467fcf7e92
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user