From b6a353d6221877a4e2c19df55efc89668f808e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Sat, 23 May 2026 03:12:40 +0800 Subject: [PATCH] fix(channels): stabilize default account selection --- scripts/dev-api.js | 19 ++++++++ src-tauri/src/commands/messaging.rs | 50 ++++++++++++++++++++ tests/channel-config-normalization.test.js | 55 ++++++++++++++++++++++ 3 files changed, 124 insertions(+) diff --git a/scripts/dev-api.js b/scripts/dev-api.js index f767b45..696f180 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -2564,8 +2564,20 @@ const MESSAGING_CREDENTIAL_FIELDS = [ 'password', 'signingSecret', 'token', + 'tokenFile', ] +function hasConfiguredMessagingValue(value) { + if (typeof value === 'string') return value.trim().length > 0 + if (normalizeSecretRef(value)) return true + return value !== undefined && value !== null +} + +function channelRootHasMessagingCredential(root) { + if (!root || typeof root !== 'object' || Array.isArray(root)) return false + return MESSAGING_CREDENTIAL_FIELDS.some(key => hasConfiguredMessagingValue(root[key])) +} + function preserveMessagingCredentialRefs(entry, form, current) { delete entry.__secretRefs for (const key of MESSAGING_CREDENTIAL_FIELDS) { @@ -3083,12 +3095,19 @@ function mergeMessagingAccountEntry(cfg, storageKey, accountId, entry) { const root = existingRoot && typeof existingRoot === 'object' && !Array.isArray(existingRoot) ? existingRoot : { enabled: true } + const accountsBefore = root.accounts && typeof root.accounts === 'object' && !Array.isArray(root.accounts) + ? Object.keys(root.accounts).filter(Boolean) + : [] + const shouldSetDefaultAccount = !String(root.defaultAccount || '').trim() + && !channelRootHasMessagingCredential(root) + && accountsBefore.length === 0 root.enabled = true if (!root.accounts || typeof root.accounts !== 'object' || Array.isArray(root.accounts)) root.accounts = {} const existingAccount = root.accounts[accountId] root.accounts[accountId] = existingAccount && typeof existingAccount === 'object' && !Array.isArray(existingAccount) ? { ...existingAccount, ...entry } : entry + if (shouldSetDefaultAccount) root.defaultAccount = accountId cfg.channels[storageKey] = root } diff --git a/src-tauri/src/commands/messaging.rs b/src-tauri/src/commands/messaging.rs index b6986d3..f364bd5 100644 --- a/src-tauri/src/commands/messaging.rs +++ b/src-tauri/src/commands/messaging.rs @@ -147,6 +147,7 @@ fn preserve_messaging_credential_refs( "password", "signingSecret", "token", + "tokenFile", ] { if !form_obj.contains_key(key) { continue; @@ -162,6 +163,36 @@ fn preserve_messaging_credential_refs( } } +fn has_configured_messaging_value(value: Option<&Value>) -> bool { + match value { + Some(Value::String(raw)) => !raw.trim().is_empty(), + Some(value) if secret_ref_parts(value).is_some() => true, + Some(Value::Null) | None => false, + Some(_) => true, + } +} + +fn channel_root_has_messaging_credential(root: &Map) -> bool { + [ + "accessToken", + "appId", + "appPassword", + "appSecret", + "appToken", + "botToken", + "clientId", + "clientSecret", + "gatewayPassword", + "gatewayToken", + "password", + "signingSecret", + "token", + "tokenFile", + ] + .iter() + .any(|key| has_configured_messaging_value(root.get(*key))) +} + fn insert_bool_as_string(form: &mut Map, source: &Value, key: &str) { if let Some(v) = source.get(key).and_then(|v| v.as_bool()) { form.insert( @@ -454,6 +485,19 @@ fn merge_account_channel_entry( let channel_obj = channel .as_object_mut() .ok_or(format!("{} 节点格式错误", key))?; + let accounts_before = channel_obj + .get("accounts") + .and_then(|value| value.as_object()) + .map(|accounts| accounts.keys().filter(|id| !id.is_empty()).count()) + .unwrap_or(0); + let should_set_default_account = channel_obj + .get("defaultAccount") + .and_then(|value| value.as_str()) + .map(str::trim) + .filter(|value| !value.is_empty()) + .is_none() + && !channel_root_has_messaging_credential(channel_obj) + && accounts_before == 0; channel_obj.insert("enabled".into(), Value::Bool(true)); let accounts = channel_obj.entry("accounts").or_insert_with(|| json!({})); let accounts_obj = accounts.as_object_mut().ok_or("accounts 格式错误")?; @@ -467,6 +511,12 @@ fn merge_account_channel_entry( new_entry }; accounts_obj.insert(account_id.to_string(), Value::Object(merged)); + if should_set_default_account { + channel_obj.insert( + "defaultAccount".into(), + Value::String(account_id.to_string()), + ); + } Ok(()) } diff --git a/tests/channel-config-normalization.test.js b/tests/channel-config-normalization.test.js index 54fdbf6..95bbbec 100644 --- a/tests/channel-config-normalization.test.js +++ b/tests/channel-config-normalization.test.js @@ -275,3 +275,58 @@ test('OpenClaw 渠道保存带账号标识时会写入 accounts 而不是覆盖 assert.equal(cfg.channels.slack.accounts['team-a'].botToken, 'team-slack') assert.equal(cfg.channels.slack.accounts['team-a'].appToken, 'team-app') }) + +test('OpenClaw 渠道保存第一个命名账号时会固定 defaultAccount', () => { + const cfg = { channels: {} } + + mergeOpenClawMessagingPlatformConfig(cfg, { + platform: 'telegram', + accountId: 'alerts', + form: { botToken: 'alerts-token' }, + }) + mergeOpenClawMessagingPlatformConfig(cfg, { + platform: 'telegram', + accountId: 'ops', + form: { botToken: 'ops-token' }, + }) + + assert.equal(cfg.channels.telegram.defaultAccount, 'alerts') + assert.equal(cfg.channels.telegram.accounts.alerts.botToken, 'alerts-token') + assert.equal(cfg.channels.telegram.accounts.ops.botToken, 'ops-token') +}) + +test('OpenClaw 渠道保存命名账号时不会覆盖已有默认账号或根凭证默认账号', () => { + const explicitDefault = { + channels: { + discord: { + defaultAccount: 'ops', + accounts: { ops: { token: 'ops-token' } }, + }, + }, + } + mergeOpenClawMessagingPlatformConfig(explicitDefault, { + platform: 'discord', + accountId: 'alerts', + form: { token: 'alerts-token' }, + }) + + const rootDefault = { + channels: { + slack: { + mode: 'socket', + botToken: 'root-bot', + appToken: 'root-app', + }, + }, + } + mergeOpenClawMessagingPlatformConfig(rootDefault, { + platform: 'slack', + accountId: 'team-a', + form: { mode: 'socket', botToken: 'team-bot', appToken: 'team-app' }, + }) + + assert.equal(explicitDefault.channels.discord.defaultAccount, 'ops') + assert.equal(explicitDefault.channels.discord.accounts.alerts.token, 'alerts-token') + assert.equal(rootDefault.channels.slack.defaultAccount, undefined) + assert.equal(rootDefault.channels.slack.accounts['team-a'].botToken, 'team-bot') +})