mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-31 05:10:14 +08:00
fix(channels): save multi-account platform configs
This commit is contained in:
@@ -3069,6 +3069,86 @@ function bindingIdentityMatches(binding, agentId, targetMatch) {
|
||||
)
|
||||
}
|
||||
|
||||
function mergeMessagingRootEntry(cfg, storageKey, entry) {
|
||||
if (!cfg.channels || typeof cfg.channels !== 'object' || Array.isArray(cfg.channels)) cfg.channels = {}
|
||||
const existing = cfg.channels[storageKey]
|
||||
cfg.channels[storageKey] = existing && typeof existing === 'object' && !Array.isArray(existing)
|
||||
? { ...existing, ...entry }
|
||||
: entry
|
||||
}
|
||||
|
||||
function mergeMessagingAccountEntry(cfg, storageKey, accountId, entry) {
|
||||
if (!cfg.channels || typeof cfg.channels !== 'object' || Array.isArray(cfg.channels)) cfg.channels = {}
|
||||
const existingRoot = cfg.channels[storageKey]
|
||||
const root = existingRoot && typeof existingRoot === 'object' && !Array.isArray(existingRoot)
|
||||
? existingRoot
|
||||
: { enabled: true }
|
||||
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
|
||||
cfg.channels[storageKey] = root
|
||||
}
|
||||
|
||||
function applyMessagingPlatformEntry(cfg, storageKey, accountId, entry) {
|
||||
const normalizedAccountId = typeof accountId === 'string' ? accountId.trim() : ''
|
||||
if (normalizedAccountId) {
|
||||
mergeMessagingAccountEntry(cfg, storageKey, normalizedAccountId, entry)
|
||||
} else {
|
||||
mergeMessagingRootEntry(cfg, storageKey, entry)
|
||||
}
|
||||
}
|
||||
|
||||
function buildOpenClawMessagingPlatformEntry(platform, form, currentSaved = {}) {
|
||||
const entry = { enabled: true }
|
||||
const storageKey = platformStorageKey(platform)
|
||||
if (storageKey === 'telegram') {
|
||||
entry.botToken = form.botToken
|
||||
entry.dmPolicy = form.dmPolicy
|
||||
entry.groupPolicy = form.groupPolicy
|
||||
if (Array.isArray(form.allowFrom) && form.allowFrom.length) entry.allowFrom = form.allowFrom
|
||||
} else if (storageKey === 'discord') {
|
||||
entry.token = form.token
|
||||
entry.dmPolicy = form.dmPolicy
|
||||
entry.groupPolicy = form.groupPolicy
|
||||
if (Array.isArray(form.allowFrom) && form.allowFrom.length) entry.allowFrom = form.allowFrom
|
||||
if (form.guildId) {
|
||||
const ck = form.channelId || '*'
|
||||
entry.guilds = { [form.guildId]: { users: ['*'], requireMention: true, channels: { [ck]: { allow: true, requireMention: true } } } }
|
||||
}
|
||||
} else if (storageKey === 'feishu') {
|
||||
entry.appId = form.appId
|
||||
entry.appSecret = form.appSecret
|
||||
entry.connectionMode = 'websocket'
|
||||
entry.domain = form.domain
|
||||
entry.webhookPath = form.webhookPath
|
||||
entry.dmPolicy = form.dmPolicy
|
||||
entry.groupPolicy = form.groupPolicy
|
||||
if (Array.isArray(form.allowFrom) && form.allowFrom.length) entry.allowFrom = form.allowFrom
|
||||
if (Object.hasOwn(form, 'requireMention')) entry.requireMention = !!form.requireMention
|
||||
entry.reactionNotifications = form.reactionNotifications
|
||||
entry.typingIndicator = form.typingIndicator
|
||||
entry.resolveSenderNames = form.resolveSenderNames
|
||||
} else {
|
||||
Object.assign(entry, form)
|
||||
}
|
||||
preserveMessagingCredentialRefs(entry, form, currentSaved)
|
||||
return entry
|
||||
}
|
||||
|
||||
export function mergeOpenClawMessagingPlatformConfig(cfg, { platform, form, accountId } = {}) {
|
||||
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) throw new Error('openclaw.json 顶层必须是对象')
|
||||
const storageKey = platformStorageKey(platform)
|
||||
const normalizedForm = normalizeMessagingPlatformForm(platform, form || {})
|
||||
const normalizedAccountId = typeof accountId === 'string' ? accountId.trim() : ''
|
||||
const currentSaved = resolvePlatformConfigEntry(cfg.channels?.[storageKey], platform, normalizedAccountId) || {}
|
||||
const entry = buildOpenClawMessagingPlatformEntry(platform, normalizedForm, currentSaved)
|
||||
applyMessagingPlatformEntry(cfg, storageKey, normalizedAccountId, entry)
|
||||
return { entry, accountId: normalizedAccountId, storageKey }
|
||||
}
|
||||
|
||||
function triggerGatewayReloadNonBlocking(reason) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
@@ -4467,22 +4547,10 @@ const handlers = {
|
||||
const normalizedAccountId = typeof accountId === 'string' ? accountId.trim() : ''
|
||||
const currentSaved = resolvePlatformConfigEntry(cfg.channels?.[storageKey], platform, normalizedAccountId) || {}
|
||||
const setRootChannelEntry = (entry) => {
|
||||
const current = cfg.channels?.[storageKey]
|
||||
// 合并模式:保留用户通过 CLI 或手动编辑的自定义字段(streaming, retry, dmPolicy 等)
|
||||
if (current && typeof current === 'object') {
|
||||
cfg.channels[storageKey] = { ...current, ...entry }
|
||||
} else {
|
||||
cfg.channels[storageKey] = entry
|
||||
}
|
||||
mergeMessagingRootEntry(cfg, storageKey, entry)
|
||||
}
|
||||
const setAccountChannelEntry = (entry) => {
|
||||
const current = cfg.channels?.[storageKey] && typeof cfg.channels[storageKey] === 'object'
|
||||
? cfg.channels[storageKey]
|
||||
: { enabled: true }
|
||||
current.enabled = true
|
||||
if (!current.accounts || typeof current.accounts !== 'object') current.accounts = {}
|
||||
current.accounts[normalizedAccountId] = entry
|
||||
cfg.channels[storageKey] = current
|
||||
mergeMessagingAccountEntry(cfg, storageKey, normalizedAccountId, entry)
|
||||
}
|
||||
const entry = { enabled: true }
|
||||
if (platform === 'qqbot') {
|
||||
@@ -4547,16 +4615,12 @@ const handlers = {
|
||||
} else {
|
||||
Object.assign(entry, form)
|
||||
preserveMessagingCredentialRefs(entry, form, currentSaved)
|
||||
setRootChannelEntry(entry)
|
||||
}
|
||||
|
||||
if (platform !== 'qqbot' && platform !== 'feishu' && platform !== 'dingtalk' && platform !== 'dingtalk-connector') {
|
||||
preserveMessagingCredentialRefs(entry, form, currentSaved)
|
||||
// 合并模式:保留用户通过 CLI 或手动编辑的自定义字段
|
||||
const existing = cfg.channels[storageKey]
|
||||
cfg.channels[storageKey] = (existing && typeof existing === 'object')
|
||||
? { ...existing, ...entry }
|
||||
: entry
|
||||
applyMessagingPlatformEntry(cfg, storageKey, normalizedAccountId, entry)
|
||||
// Discord: 仅在首次创建时设置默认值,不覆盖用户已有的设置
|
||||
if (platform === 'discord') {
|
||||
const d = cfg.channels[storageKey]
|
||||
|
||||
@@ -441,6 +441,49 @@ fn merge_channel_entry(
|
||||
channels_map.insert(key.to_string(), Value::Object(merged));
|
||||
}
|
||||
|
||||
/// 合并账号级渠道配置:保留渠道根节点和账号已有自定义字段,只覆盖本次表单字段。
|
||||
fn merge_account_channel_entry(
|
||||
channels_map: &mut Map<String, Value>,
|
||||
key: &str,
|
||||
account_id: &str,
|
||||
new_entry: Map<String, Value>,
|
||||
) -> Result<(), String> {
|
||||
let channel = channels_map
|
||||
.entry(key.to_string())
|
||||
.or_insert_with(|| json!({ "enabled": true }));
|
||||
let channel_obj = channel
|
||||
.as_object_mut()
|
||||
.ok_or(format!("{} 节点格式错误", key))?;
|
||||
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 格式错误")?;
|
||||
let merged = if let Some(Value::Object(existing)) = accounts_obj.get(account_id) {
|
||||
let mut m = existing.clone();
|
||||
for (k, v) in new_entry {
|
||||
m.insert(k, v);
|
||||
}
|
||||
m
|
||||
} else {
|
||||
new_entry
|
||||
};
|
||||
accounts_obj.insert(account_id.to_string(), Value::Object(merged));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn merge_channel_entry_for_account(
|
||||
channels_map: &mut Map<String, Value>,
|
||||
key: &str,
|
||||
account_id: Option<&str>,
|
||||
new_entry: Map<String, Value>,
|
||||
) -> Result<(), String> {
|
||||
if let Some(acct) = account_id.map(str::trim).filter(|s| !s.is_empty()) {
|
||||
merge_account_channel_entry(channels_map, key, acct, new_entry)
|
||||
} else {
|
||||
merge_channel_entry(channels_map, key, new_entry);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_binding_match_value(value: &Value) -> Option<Value> {
|
||||
match value {
|
||||
Value::Null => None,
|
||||
@@ -1034,7 +1077,7 @@ pub async fn save_messaging_platform(
|
||||
|
||||
// 合并到现有配置,保留用户通过 CLI 设置的 streaming / retry / dmPolicy 等
|
||||
preserve_messaging_credential_refs(&mut entry, form_obj, ¤t_saved);
|
||||
merge_channel_entry(channels_map, "discord", entry);
|
||||
merge_channel_entry_for_account(channels_map, "discord", account_id.as_deref(), entry)?;
|
||||
// 仅在首次创建时设置默认值,不覆盖用户已有的设置
|
||||
if let Some(Value::Object(d)) = channels_map.get_mut("discord") {
|
||||
d.entry("groupPolicy")
|
||||
@@ -1064,7 +1107,12 @@ pub async fn save_messaging_platform(
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
|
||||
preserve_messaging_credential_refs(&mut entry, form_obj, ¤t_saved);
|
||||
merge_channel_entry(channels_map, "telegram", entry);
|
||||
merge_channel_entry_for_account(
|
||||
channels_map,
|
||||
"telegram",
|
||||
account_id.as_deref(),
|
||||
entry,
|
||||
)?;
|
||||
}
|
||||
"qqbot" => {
|
||||
let app_id = form_obj
|
||||
@@ -1180,23 +1228,12 @@ pub async fn save_messaging_platform(
|
||||
put_bool_value_if_present(&mut entry, "requireMention", form_obj.get("requireMention"));
|
||||
preserve_messaging_credential_refs(&mut entry, form_obj, ¤t_saved);
|
||||
|
||||
// 多账号模式:写入 channels.<storage_key>.accounts.<account_id>
|
||||
if let Some(ref acct) = account_id {
|
||||
if !acct.is_empty() {
|
||||
let feishu = channels_map
|
||||
.entry(storage_key.as_str())
|
||||
.or_insert_with(|| json!({ "enabled": true }));
|
||||
let feishu_obj = feishu.as_object_mut().ok_or("飞书节点格式错误")?;
|
||||
feishu_obj.entry("enabled").or_insert(Value::Bool(true));
|
||||
let accounts = feishu_obj.entry("accounts").or_insert_with(|| json!({}));
|
||||
let accounts_obj = accounts.as_object_mut().ok_or("accounts 格式错误")?;
|
||||
accounts_obj.insert(acct.clone(), Value::Object(entry));
|
||||
} else {
|
||||
merge_channel_entry(channels_map, &storage_key, entry);
|
||||
}
|
||||
} else {
|
||||
merge_channel_entry(channels_map, &storage_key, entry);
|
||||
}
|
||||
merge_channel_entry_for_account(
|
||||
channels_map,
|
||||
&storage_key,
|
||||
account_id.as_deref(),
|
||||
entry,
|
||||
)?;
|
||||
ensure_plugin_allowed(&mut cfg, "openclaw-lark")?;
|
||||
// 禁用旧版 feishu 插件,防止新旧插件同时运行冲突
|
||||
disable_legacy_plugin(&mut cfg, "feishu");
|
||||
@@ -1248,7 +1285,12 @@ pub async fn save_messaging_platform(
|
||||
}
|
||||
|
||||
preserve_messaging_credential_refs(&mut entry, form_obj, ¤t_saved);
|
||||
merge_channel_entry(channels_map, &storage_key, entry);
|
||||
merge_channel_entry_for_account(
|
||||
channels_map,
|
||||
&storage_key,
|
||||
account_id.as_deref(),
|
||||
entry,
|
||||
)?;
|
||||
ensure_plugin_allowed(&mut cfg, "dingtalk-connector")?;
|
||||
ensure_chat_completions_enabled(&mut cfg)?;
|
||||
let _ = cleanup_legacy_plugin_backup_dir("dingtalk-connector");
|
||||
@@ -1304,7 +1346,12 @@ pub async fn save_messaging_platform(
|
||||
);
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
preserve_messaging_credential_refs(&mut entry, form_obj, ¤t_saved);
|
||||
merge_channel_entry(channels_map, &storage_key, entry);
|
||||
merge_channel_entry_for_account(
|
||||
channels_map,
|
||||
&storage_key,
|
||||
account_id.as_deref(),
|
||||
entry,
|
||||
)?;
|
||||
}
|
||||
"whatsapp" => {
|
||||
let mut entry = Map::new();
|
||||
@@ -1317,7 +1364,12 @@ pub async fn save_messaging_platform(
|
||||
);
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
put_bool_from_form(&mut entry, "enabled", &form_string(form_obj, "enabled"));
|
||||
merge_channel_entry(channels_map, &storage_key, entry);
|
||||
merge_channel_entry_for_account(
|
||||
channels_map,
|
||||
&storage_key,
|
||||
account_id.as_deref(),
|
||||
entry,
|
||||
)?;
|
||||
}
|
||||
"signal" => {
|
||||
let account = form_string(form_obj, "account");
|
||||
@@ -1340,7 +1392,12 @@ pub async fn save_messaging_platform(
|
||||
);
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
preserve_messaging_credential_refs(&mut entry, form_obj, ¤t_saved);
|
||||
merge_channel_entry(channels_map, &storage_key, entry);
|
||||
merge_channel_entry_for_account(
|
||||
channels_map,
|
||||
&storage_key,
|
||||
account_id.as_deref(),
|
||||
entry,
|
||||
)?;
|
||||
}
|
||||
"matrix" => {
|
||||
let homeserver = form_string(form_obj, "homeserver");
|
||||
@@ -1371,7 +1428,12 @@ pub async fn save_messaging_platform(
|
||||
put_bool_from_form(&mut entry, "e2ee", &form_string(form_obj, "e2ee"));
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
preserve_messaging_credential_refs(&mut entry, form_obj, ¤t_saved);
|
||||
merge_channel_entry(channels_map, &storage_key, entry);
|
||||
merge_channel_entry_for_account(
|
||||
channels_map,
|
||||
&storage_key,
|
||||
account_id.as_deref(),
|
||||
entry,
|
||||
)?;
|
||||
ensure_plugin_allowed(&mut cfg, "matrix")?;
|
||||
}
|
||||
"msteams" => {
|
||||
@@ -1405,7 +1467,12 @@ pub async fn save_messaging_platform(
|
||||
put_bool_value_if_present(&mut entry, "requireMention", form_obj.get("requireMention"));
|
||||
put_array_from_form_value(&mut entry, "allowFrom", form_obj.get("allowFrom"));
|
||||
preserve_messaging_credential_refs(&mut entry, form_obj, ¤t_saved);
|
||||
merge_channel_entry(channels_map, &storage_key, entry);
|
||||
merge_channel_entry_for_account(
|
||||
channels_map,
|
||||
&storage_key,
|
||||
account_id.as_deref(),
|
||||
entry,
|
||||
)?;
|
||||
ensure_plugin_allowed(&mut cfg, "msteams")?;
|
||||
}
|
||||
_ => {
|
||||
@@ -1416,7 +1483,12 @@ pub async fn save_messaging_platform(
|
||||
}
|
||||
entry.insert("enabled".into(), Value::Bool(true));
|
||||
preserve_messaging_credential_refs(&mut entry, form_obj, ¤t_saved);
|
||||
merge_channel_entry(channels_map, &storage_key, entry);
|
||||
merge_channel_entry_for_account(
|
||||
channels_map,
|
||||
&storage_key,
|
||||
account_id.as_deref(),
|
||||
entry,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -436,8 +436,12 @@ function applyRouteIntent(page, state) {
|
||||
|
||||
// ── 已配置平台渲染 ──
|
||||
|
||||
// ── 多账号支持的平台(历史配置中飞书/钉钉等多实例仍展示子账号行) ──
|
||||
const MULTI_INSTANCE_PLATFORMS = ['feishu', 'dingtalk', 'qqbot']
|
||||
// ── 多账号支持的平台:与 OpenClaw 的 accounts/defaultAccount 配置模型保持一致 ──
|
||||
const MULTI_INSTANCE_PLATFORMS = ['telegram', 'discord', 'slack', 'feishu', 'dingtalk', 'dingtalk-connector', 'qqbot']
|
||||
|
||||
function supportsMessagingMultiAccount(pid) {
|
||||
return MULTI_INSTANCE_PLATFORMS.includes(pid)
|
||||
}
|
||||
|
||||
function platformLabel(pid) {
|
||||
return PLATFORM_REGISTRY[pid]?.label || CHANNEL_LABELS[pid] || pid
|
||||
@@ -586,7 +590,7 @@ function renderConfigured(page, state) {
|
||||
const runtimeSummary = getChannelRuntimeSummary(state.runtimeStatus, channelKey, label)
|
||||
const accounts = Array.isArray(p.accounts) ? p.accounts : []
|
||||
const hasAccounts = accounts.length > 0
|
||||
const supportsMulti = MULTI_INSTANCE_PLATFORMS.includes(p.id)
|
||||
const supportsMulti = supportsMessagingMultiAccount(p.id)
|
||||
|
||||
if (hasAccounts) {
|
||||
const accountsHtml = accounts.map(acc => {
|
||||
@@ -672,7 +676,7 @@ function renderConfigured(page, state) {
|
||||
|
||||
const accounts = Array.isArray(configured.accounts) ? configured.accounts : []
|
||||
const hasAccounts = accounts.length > 0
|
||||
const supportsMulti = MULTI_INSTANCE_PLATFORMS.includes(pid)
|
||||
const supportsMulti = supportsMessagingMultiAccount(pid)
|
||||
|
||||
// 统计当前 channel+accountId 组合已有的 agent 绑定
|
||||
const channelKey = getChannelBindingKey(pid)
|
||||
@@ -1921,7 +1925,7 @@ async function openConfigDialog(pid, page, state, accountId) {
|
||||
|
||||
const formId = 'platform-form-' + Date.now()
|
||||
|
||||
const supportsMultiAccount = ['feishu', 'dingtalk', 'dingtalk-connector', 'qqbot'].includes(pid)
|
||||
const supportsMultiAccount = supportsMessagingMultiAccount(pid)
|
||||
|
||||
// 账号标识(多账号);编辑时 accountId 非空会在 input value 中显示
|
||||
const accountIdHtml = supportsMultiAccount ? `
|
||||
|
||||
@@ -4,6 +4,7 @@ import assert from 'node:assert/strict'
|
||||
import {
|
||||
buildMessagingPlatformFormValues,
|
||||
listPlatformAccounts,
|
||||
mergeOpenClawMessagingPlatformConfig,
|
||||
resolveMessagingCredentialValueForSave,
|
||||
normalizeMessagingPlatformForm,
|
||||
} from '../scripts/dev-api.js'
|
||||
@@ -224,3 +225,53 @@ test('渠道保存时 clientId 未改动 SecretRef 占位会保留原始引用',
|
||||
|
||||
assert.deepEqual(value, secretRef)
|
||||
})
|
||||
|
||||
test('OpenClaw 渠道保存带账号标识时会写入 accounts 而不是覆盖根配置', () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
enabled: true,
|
||||
botToken: 'root-token',
|
||||
dmPolicy: 'pairing',
|
||||
groupPolicy: 'allowlist',
|
||||
},
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: 'root-discord',
|
||||
groupPolicy: 'allowlist',
|
||||
},
|
||||
slack: {
|
||||
enabled: true,
|
||||
mode: 'socket',
|
||||
botToken: 'root-slack',
|
||||
appToken: 'root-app',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mergeOpenClawMessagingPlatformConfig(cfg, {
|
||||
platform: 'telegram',
|
||||
accountId: 'alerts',
|
||||
form: { botToken: 'alerts-token', dmPolicy: 'allowlist', groupPolicy: 'disabled' },
|
||||
})
|
||||
mergeOpenClawMessagingPlatformConfig(cfg, {
|
||||
platform: 'discord',
|
||||
accountId: 'ops',
|
||||
form: { token: 'ops-discord', guildId: 'guild-1', channelId: 'channel-1' },
|
||||
})
|
||||
mergeOpenClawMessagingPlatformConfig(cfg, {
|
||||
platform: 'slack',
|
||||
accountId: 'team-a',
|
||||
form: { mode: 'socket', botToken: 'team-slack', appToken: 'team-app' },
|
||||
})
|
||||
|
||||
assert.equal(cfg.channels.telegram.botToken, 'root-token')
|
||||
assert.equal(cfg.channels.telegram.accounts.alerts.botToken, 'alerts-token')
|
||||
assert.equal(cfg.channels.telegram.accounts.alerts.dmPolicy, 'allowlist')
|
||||
assert.equal(cfg.channels.discord.token, 'root-discord')
|
||||
assert.equal(cfg.channels.discord.accounts.ops.token, 'ops-discord')
|
||||
assert.equal(cfg.channels.discord.accounts.ops.guilds['guild-1'].channels['channel-1'].allow, true)
|
||||
assert.equal(cfg.channels.slack.botToken, 'root-slack')
|
||||
assert.equal(cfg.channels.slack.accounts['team-a'].botToken, 'team-slack')
|
||||
assert.equal(cfg.channels.slack.accounts['team-a'].appToken, 'team-app')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user