fix(channels): stop missing-account resolver from copying root SecretRefs

When saving a new multi-account channel entry (e.g. accounts.work),resolve_platform_config_entry fell back to the channel root if theaccount id was not in accounts yet. preserve_messaging_credential_refsthen treated the default account SecretRef placeholders as unchangedand wrote them into the new account — both bots shared credentials.

- Return None for unknown account ids (Rust + dev-api)
- Skip config preload on Add Account dialog (accountId === '')
- Lock account identifier field when editing an existing account
- Add regression tests

Co-authored-by: 晴天 <1186258278@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-05-31 11:06:42 +00:00
parent 38934fe754
commit c78ddcc7c4
5 changed files with 58 additions and 17 deletions

View File

@@ -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
}

View File

@@ -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.<id> 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!({

View File

@@ -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', '顯示'),

View File

@@ -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 ? `
<div class="form-group">
<label class="form-label">${t('channels.accountIdentifier')}</label>
<input class="form-input" name="__accountId" placeholder="${t('channels.accountIdPlaceholder')}" value="${escapeAttr(accountId != null ? accountId : '')}">
<div class="form-hint">${t('channels.accountIdHint')}</div>
<input class="form-input" name="__accountId" placeholder="${t('channels.accountIdPlaceholder')}" value="${escapeAttr(accountId != null ? accountId : '')}" ${accountIdReadonly ? 'readonly' : ''}>
<div class="form-hint">${accountIdReadonly ? t('channels.accountIdLockedHint') : t('channels.accountIdHint')}</div>
</div>
` : ''

View File

@@ -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({