diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 4020083..47be666 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -6851,11 +6851,15 @@ function secretAwareAccountDisplayValue(value) { return formatSecretRefPlaceholder(value) } -function resolvePlatformConfigEntry(channelRoot, platform, accountId) { +export function resolvePlatformConfigEntry(channelRoot, platform, accountId) { if (!channelRoot || typeof channelRoot !== 'object') return null const accountKey = typeof accountId === 'string' ? accountId.trim() : '' if (platformStorageKey(platform) === 'tlon' && accountKey === QQBOT_DEFAULT_ACCOUNT_ID) return channelRoot - if (accountKey) return channelRoot.accounts?.[accountKey] || channelRoot + if (accountKey) { + const entry = channelRoot.accounts?.[accountKey] + if (entry && typeof entry === 'object') return entry + return null + } if (platformStorageKey(platform) === 'qqbot' && !channelHasQqbotCredentials(channelRoot)) { return channelRoot.accounts?.[QQBOT_DEFAULT_ACCOUNT_ID] || channelRoot } diff --git a/src-tauri/src/commands/messaging.rs b/src-tauri/src/commands/messaging.rs index beefccc..6fffa1d 100644 --- a/src-tauri/src/commands/messaging.rs +++ b/src-tauri/src/commands/messaging.rs @@ -1426,10 +1426,9 @@ fn resolve_platform_config_entry( if let Some(value) = root.get("accounts").and_then(|a| a.get(acct)) { return Some(value.clone()); } - if platform_storage_key(platform) == "qqbot" && !qqbot_channel_has_credentials(root) { - return None; - } - return Some(root.clone()); + // Missing account must not fall back to root: preserve_messaging_credential_refs + // would copy the default account's SecretRefs into a new accounts. entry. + None } if platform_storage_key(platform) == "qqbot" && !qqbot_channel_has_credentials(root) { @@ -7593,6 +7592,27 @@ mod tests { ); } + #[test] + fn resolve_platform_config_entry_missing_account_does_not_fallback_to_root() { + let root = json!({ + "enabled": true, + "botToken": { + "source": "env", + "provider": "default", + "id": "TELEGRAM_BOT_TOKEN" + }, + "accounts": { + "default": { "botToken": "default-token" } + } + }); + let resolved = + resolve_platform_config_entry(Some(&root), "telegram", Some("work")); + assert!( + resolved.is_none(), + "unknown account id must not resolve to root credentials" + ); + } + #[test] fn channel_save_preserves_unchanged_secret_ref_placeholder() { let current = json!({ diff --git a/src/locales/modules/channels.js b/src/locales/modules/channels.js index 3f3c4b5..77d45bc 100644 --- a/src/locales/modules/channels.js +++ b/src/locales/modules/channels.js @@ -544,6 +544,7 @@ export default { accountIdentifier: _('账号标识', 'Account Identifier', '账號標識'), accountIdPlaceholder: _('留空为默认账号;修改会创建新账号', 'Leave empty for default account; changing creates a new account', '留空為預設账號;修改會建立新账號'), accountIdHint: _('每个账号对应一个独立机器人。不同账号可绑定不同 Agent。', 'Each account corresponds to an independent bot. Different accounts can bind to different Agents.', '每個账號对應一個獨立機器人。不同账號可綁定不同 Agent。'), + accountIdLockedHint: _('编辑已有账号时不可修改标识,避免误写入根配置。', 'Account identifier cannot be changed when editing an existing account; this prevents writing to the root config by mistake.', '編輯已有账號時不可修改標識,避免誤寫入根設定。'), bindAgent: _('绑定 Agent', 'Bind Agent', '綁定 Agent', 'Agent をバインド', 'Agent 바인딩', 'Liên kết Agent', 'Vincular Agent', 'Vincular Agent', 'Привязать агента', 'Lier un Agent', 'Agent verknüpfen'), bindAgentHint: _('该账号收到的消息路由到哪个 Agent(可在「Agent 对接」页添加更多绑定)。', 'Which Agent receives messages for this account (add more bindings in "Agent Binding" tab).', '該账號收到的訊息路由到哪個 Agent(可在「Agent 对接」頁新增更多綁定)。'), show: _('显示', 'Show', '顯示'), diff --git a/src/pages/channels.js b/src/pages/channels.js index d41292b..0976f00 100644 --- a/src/pages/channels.js +++ b/src/pages/channels.js @@ -2524,17 +2524,22 @@ async function openConfigDialog(pid, page, state, accountId) { } // 尝试加载已有配置(accountId 用于多账号读取) + // 「添加账号」传入 '',且 readPlatformConfig 会把 '' 当成 null;此处直接跳过读取,避免预填默认账号凭证。 + const dialogAccountId = accountId != null ? String(accountId).trim() : '' + const isNewAccountDialog = supportsMessagingMultiAccount(pid) && accountId === '' let existing = {} let isEdit = false - try { - const res = await api.readPlatformConfig(pid, accountId) - if (res?.values) { - existing = res.values - } - if (res?.exists) { - isEdit = true - } - } catch {} + if (!isNewAccountDialog) { + try { + const res = await api.readPlatformConfig(pid, accountId) + if (res?.values) { + existing = res.values + } + if (res?.exists) { + isEdit = true + } + } catch {} + } // 加载 Agent 列表(不预选,因为一个 channel+accountId 可以被多个 agent 绑定) let agents = [] @@ -2547,11 +2552,12 @@ async function openConfigDialog(pid, page, state, accountId) { const supportsMultiAccount = supportsMessagingMultiAccount(pid) // 账号标识(多账号);编辑时 accountId 非空会在 input value 中显示 + const accountIdReadonly = isEdit && dialogAccountId const accountIdHtml = supportsMultiAccount ? `
- -
${t('channels.accountIdHint')}
+ +
${accountIdReadonly ? t('channels.accountIdLockedHint') : t('channels.accountIdHint')}
` : '' diff --git a/tests/channel-config-normalization.test.js b/tests/channel-config-normalization.test.js index 8e4dea0..8e13c9b 100644 --- a/tests/channel-config-normalization.test.js +++ b/tests/channel-config-normalization.test.js @@ -7,6 +7,7 @@ import { listPlatformAccounts, mergeOpenClawMessagingPlatformConfig, resolveMessagingCredentialValueForSave, + resolvePlatformConfigEntry, normalizeMessagingPlatformForm, } from '../scripts/dev-api.js' @@ -1028,6 +1029,15 @@ test('渠道读取会把 SecretRef 密钥显示为安全占位并携带原始对 assert.deepEqual(values.__secretRefs, { botToken: secretRef }) }) +test('未知账号标识不会回落到渠道根配置', () => { + const root = { + enabled: true, + botToken: { source: 'env', provider: 'default', id: 'TELEGRAM_BOT_TOKEN' }, + accounts: { default: { botToken: 'default-token' } }, + } + assert.equal(resolvePlatformConfigEntry(root, 'telegram', 'work'), null) +}) + test('渠道保存时用户未改动 SecretRef 占位会保留原始密钥引用', () => { const secretRef = { source: 'env', provider: 'default', id: 'SLACK_BOT_TOKEN' } const value = resolveMessagingCredentialValueForSave({