From d3d527ca34754b8850ed9d1e253e1c6a9b0459e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Sat, 23 May 2026 03:48:33 +0800 Subject: [PATCH] feat(hermes): add dingtalk channel config --- scripts/dev-api.js | 39 +++++- src-tauri/src/commands/hermes.rs | 183 ++++++++++++++++++++++++++- src/engines/hermes/pages/channels.js | 11 ++ src/locales/modules/engine.js | 6 +- tests/hermes-channel-config.test.js | 61 +++++++++ 5 files changed, 289 insertions(+), 11 deletions(-) diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 20f2db3..6be57fd 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -2694,7 +2694,7 @@ export function buildMessagingPlatformFormValues(platform, saved = {}, options = return form } -const HERMES_CHANNEL_PLATFORMS = ['telegram', 'discord', 'slack', 'feishu'] +const HERMES_CHANNEL_PLATFORMS = ['telegram', 'discord', 'slack', 'feishu', 'dingtalk'] function normalizeHermesPlatform(platform) { const p = String(platform || '').trim().toLowerCase() @@ -2788,12 +2788,26 @@ export function buildHermesChannelConfigValues(config = {}, envValues = {}) { for (const key of ['typing_indicator', 'resolve_sender_names']) { putHermesBool(form, extra, key) } + } else if (platform === 'dingtalk') { + putHermesString(form, extra, 'client_id') + putHermesString(form, extra, 'client_secret') + form.clientId = hermesEnvValue(envValues, 'DINGTALK_CLIENT_ID') || form.clientId || '' + form.clientSecret = hermesEnvValue(envValues, 'DINGTALK_CLIENT_SECRET') || form.clientSecret || '' } putHermesString(form, extra, 'dm_policy') putHermesString(form, extra, 'group_policy') putHermesBool(form, extra, 'require_mention') - putHermesCsv(form, extra, 'allow_from') - putHermesCsv(form, extra, 'group_allow_from') + if (platform === 'dingtalk') { + putHermesCsv(form, extra, 'allowed_users') + if (form.allowedUsers && !form.allowFrom) form.allowFrom = form.allowedUsers + delete form.allowedUsers + putHermesCsv(form, extra, 'allowed_chats') + if (form.allowedChats && !form.groupAllowFrom) form.groupAllowFrom = form.allowedChats + delete form.allowedChats + } else { + putHermesCsv(form, extra, 'allow_from') + putHermesCsv(form, extra, 'group_allow_from') + } values[platform] = form } return values @@ -2866,6 +2880,11 @@ export function mergeHermesChannelConfig(config = {}, platform, form = {}) { setHermesExtra(entry, 'reaction_notifications', normalized.reactionNotifications) setHermesExtra(entry, 'typing_indicator', !!normalized.typingIndicator) setHermesExtra(entry, 'resolve_sender_names', !!normalized.resolveSenderNames) + } else if (normalizedPlatform === 'dingtalk') { + deleteHermesExtraKey(entry, 'client_id') + deleteHermesExtraKey(entry, 'client_secret') + deleteHermesExtraKey(entry, 'allow_from') + deleteHermesExtraKey(entry, 'group_allow_from') } if (Object.hasOwn(normalized, 'dmPolicy')) setHermesExtra(entry, 'dm_policy', normalized.dmPolicy) if (Object.hasOwn(normalized, 'groupPolicy')) { @@ -2873,8 +2892,12 @@ export function mergeHermesChannelConfig(config = {}, platform, form = {}) { if (normalizedPlatform === 'feishu') setHermesExtra(entry, 'default_group_policy', normalized.groupPolicy) } if (Object.hasOwn(normalized, 'requireMention')) setHermesExtra(entry, 'require_mention', !!normalized.requireMention) - if (Array.isArray(normalized.allowFrom)) setHermesExtra(entry, 'allow_from', normalized.allowFrom) - if (Array.isArray(normalized.groupAllowFrom)) setHermesExtra(entry, 'group_allow_from', normalized.groupAllowFrom) + if (Array.isArray(normalized.allowFrom)) { + setHermesExtra(entry, normalizedPlatform === 'dingtalk' ? 'allowed_users' : 'allow_from', normalized.allowFrom) + } + if (Array.isArray(normalized.groupAllowFrom)) { + setHermesExtra(entry, normalizedPlatform === 'dingtalk' ? 'allowed_chats' : 'group_allow_from', normalized.groupAllowFrom) + } next.platforms[normalizedPlatform] = entry return next } @@ -2966,6 +2989,12 @@ export function buildHermesChannelEnvUpdates(platform, form = {}) { updates.FEISHU_GROUP_POLICY = String(form.groupPolicy || 'allowlist').trim() updates.FEISHU_REQUIRE_MENTION = Object.hasOwn(form, 'requireMention') ? boolEnvValue(form.requireMention) : 'true' updates.FEISHU_REACTIONS = String(form.reactionNotifications || '').trim() === 'off' ? 'false' : 'true' + } else if (platform === 'dingtalk') { + updates.DINGTALK_CLIENT_ID = String(form.clientId || '').trim() + updates.DINGTALK_CLIENT_SECRET = String(form.clientSecret || '').trim() + updates.DINGTALK_ALLOWED_USERS = csvEnvValue(form.allowFrom) + updates.DINGTALK_ALLOWED_CHATS = csvEnvValue(form.groupAllowFrom) + if (Object.hasOwn(form, 'requireMention')) updates.DINGTALK_REQUIRE_MENTION = boolEnvValue(form.requireMention) } return updates } diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index b841d14..f59d2f3 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -2152,7 +2152,7 @@ fn merge_env_file(existing: &str, managed_keys: &[&str], new_pairs: &[(String, S // 并同步 Hermes 运行时仍会读取的 .env 变量。 // --------------------------------------------------------------------------- -const HERMES_CHANNEL_PLATFORMS: [&str; 4] = ["telegram", "discord", "slack", "feishu"]; +const HERMES_CHANNEL_PLATFORMS: [&str; 5] = ["telegram", "discord", "slack", "feishu", "dingtalk"]; fn normalize_hermes_channel_platform(platform: &str) -> Option<&'static str> { let platform = platform.trim().to_ascii_lowercase(); @@ -2372,14 +2372,30 @@ fn build_hermes_channel_config_values( "resolveSenderNames", ); } + "dingtalk" => { + insert_json_string_if_present(&mut form, &extra, "client_id", "clientId"); + insert_json_string_if_present(&mut form, &extra, "client_secret", "clientSecret"); + put_json_string_from_env(&mut form, env_values, "DINGTALK_CLIENT_ID", "clientId"); + put_json_string_from_env( + &mut form, + env_values, + "DINGTALK_CLIENT_SECRET", + "clientSecret", + ); + } _ => {} } insert_json_string_if_present(&mut form, &extra, "dm_policy", "dmPolicy"); insert_json_string_if_present(&mut form, &extra, "group_policy", "groupPolicy"); insert_json_bool_if_present(&mut form, &extra, "require_mention", "requireMention"); - insert_json_csv_if_present(&mut form, &extra, "allow_from", "allowFrom"); - insert_json_csv_if_present(&mut form, &extra, "group_allow_from", "groupAllowFrom"); + if platform == "dingtalk" { + insert_json_csv_if_present(&mut form, &extra, "allowed_users", "allowFrom"); + insert_json_csv_if_present(&mut form, &extra, "allowed_chats", "groupAllowFrom"); + } else { + insert_json_csv_if_present(&mut form, &extra, "allow_from", "allowFrom"); + insert_json_csv_if_present(&mut form, &extra, "group_allow_from", "groupAllowFrom"); + } values.insert(platform.to_string(), Value::Object(form)); } @@ -2601,6 +2617,12 @@ fn merge_hermes_channel_config( form_bool(form, "resolveSenderNames").unwrap_or(true), ); } + "dingtalk" => { + delete_extra_key(entry, "client_id"); + delete_extra_key(entry, "client_secret"); + delete_extra_key(entry, "allow_from"); + delete_extra_key(entry, "group_allow_from"); + } _ => {} } @@ -2622,10 +2644,20 @@ fn merge_hermes_channel_config( set_extra_bool(entry, "require_mention", value); } if let Some(values) = form_string_array(form, "allowFrom") { - set_extra_string_array(entry, "allow_from", values); + let key = if platform == "dingtalk" { + "allowed_users" + } else { + "allow_from" + }; + set_extra_string_array(entry, key, values); } if let Some(values) = form_string_array(form, "groupAllowFrom") { - set_extra_string_array(entry, "group_allow_from", values); + let key = if platform == "dingtalk" { + "allowed_chats" + } else { + "group_allow_from" + }; + set_extra_string_array(entry, key, values); } Ok(()) @@ -2764,6 +2796,24 @@ fn build_hermes_channel_env_updates(platform: &str, form: &Value) -> Vec<(String .to_string(), ); } + "dingtalk" => { + push( + "DINGTALK_CLIENT_ID", + form_string(form, "clientId").unwrap_or_default(), + ); + push( + "DINGTALK_CLIENT_SECRET", + form_string(form, "clientSecret").unwrap_or_default(), + ); + push("DINGTALK_ALLOWED_USERS", csv_env_value(form, "allowFrom")); + push( + "DINGTALK_ALLOWED_CHATS", + csv_env_value(form, "groupAllowFrom"), + ); + if let Some(value) = form_bool(form, "requireMention") { + push("DINGTALK_REQUIRE_MENTION", bool_env_value(value)); + } + } _ => {} } @@ -2805,6 +2855,13 @@ fn write_hermes_channel_env(platform: &str, form: &Value) -> Result<(), String> "FEISHU_REQUIRE_MENTION", "FEISHU_REACTIONS", ], + "dingtalk" => vec![ + "DINGTALK_CLIENT_ID", + "DINGTALK_CLIENT_SECRET", + "DINGTALK_ALLOWED_USERS", + "DINGTALK_ALLOWED_CHATS", + "DINGTALK_REQUIRE_MENTION", + ], _ => Vec::new(), }; let pairs = build_hermes_channel_env_updates(platform, form); @@ -7781,6 +7838,13 @@ platforms: app_secret: yaml-secret domain: lark connection_mode: webhook + dingtalk: + enabled: true + extra: + client_id: yaml-client-id + client_secret: yaml-client-secret + allowed_users: ["staff-1"] + allowed_chats: ["cid-1"] "#, ) .unwrap(); @@ -7793,6 +7857,14 @@ platforms: "FEISHU_CONNECTION_MODE".to_string(), "websocket".to_string(), ); + env.insert( + "DINGTALK_CLIENT_ID".to_string(), + "env-client-id".to_string(), + ); + env.insert( + "DINGTALK_CLIENT_SECRET".to_string(), + "env-client-secret".to_string(), + ); let values = build_hermes_channel_config_values(&config, &env); @@ -7802,6 +7874,10 @@ platforms: assert_eq!(values["feishu"]["appSecret"], "env-secret"); assert_eq!(values["feishu"]["domain"], "feishu"); assert_eq!(values["feishu"]["connectionMode"], "websocket"); + assert_eq!(values["dingtalk"]["clientId"], "env-client-id"); + assert_eq!(values["dingtalk"]["clientSecret"], "env-client-secret"); + assert_eq!(values["dingtalk"]["allowFrom"], "staff-1"); + assert_eq!(values["dingtalk"]["groupAllowFrom"], "cid-1"); } #[test] @@ -7872,6 +7948,103 @@ platforms: ))); } + #[test] + fn merge_dingtalk_channel_uses_runtime_fields() { + let mut config: serde_yaml::Value = serde_yaml::from_str( + r#" +platforms: + dingtalk: + enabled: true + extra: + client_id: old-client-id + client_secret: old-client-secret + group_allow_from: ["legacy-chat"] + unknown_option: keep-me +"#, + ) + .unwrap(); + + merge_hermes_channel_config( + &mut config, + "dingtalk", + &json!({ + "enabled": true, + "clientId": "ding-app-key", + "clientSecret": "ding-secret", + "allowFrom": "staff-1, staff-2", + "groupAllowFrom": "cid-1\ncid-2", + "requireMention": true, + }), + ) + .unwrap(); + + assert_eq!(config["platforms"]["dingtalk"]["enabled"], true); + assert_eq!( + config["platforms"]["dingtalk"]["extra"]["client_id"], + serde_yaml::Value::Null + ); + assert_eq!( + config["platforms"]["dingtalk"]["extra"]["client_secret"], + serde_yaml::Value::Null + ); + assert_eq!( + config["platforms"]["dingtalk"]["extra"]["group_allow_from"], + serde_yaml::Value::Null + ); + assert_eq!( + config["platforms"]["dingtalk"]["extra"]["allowed_users"] + .as_sequence() + .unwrap() + .iter() + .filter_map(|item| item.as_str()) + .collect::>(), + vec!["staff-1", "staff-2"] + ); + assert_eq!( + config["platforms"]["dingtalk"]["extra"]["allowed_chats"] + .as_sequence() + .unwrap() + .iter() + .filter_map(|item| item.as_str()) + .collect::>(), + vec!["cid-1", "cid-2"] + ); + assert_eq!( + config["platforms"]["dingtalk"]["extra"]["require_mention"].as_bool(), + Some(true) + ); + assert_eq!( + config["platforms"]["dingtalk"]["extra"]["unknown_option"].as_str(), + Some("keep-me") + ); + + let env = build_hermes_channel_env_updates( + "dingtalk", + &json!({ + "clientId": "ding-app-key", + "clientSecret": "ding-secret", + "allowFrom": "staff-1, staff-2", + "groupAllowFrom": "cid-1\ncid-2", + "requireMention": true, + }), + ); + + assert!(env.contains(&("DINGTALK_CLIENT_ID".to_string(), "ding-app-key".to_string()))); + assert!(env.contains(&( + "DINGTALK_CLIENT_SECRET".to_string(), + "ding-secret".to_string() + ))); + assert!(env.contains(&( + "DINGTALK_ALLOWED_USERS".to_string(), + "staff-1,staff-2".to_string() + ))); + assert!(env.contains(&( + "DINGTALK_ALLOWED_CHATS".to_string(), + "cid-1,cid-2".to_string() + ))); + assert!(env.contains(&("DINGTALK_REQUIRE_MENTION".to_string(), "true".to_string()))); + } + #[test] fn merge_channel_config_removes_yaml_secrets() { let mut config: serde_yaml::Value = serde_yaml::from_str( diff --git a/src/engines/hermes/pages/channels.js b/src/engines/hermes/pages/channels.js index 973e660..67f7fdf 100644 --- a/src/engines/hermes/pages/channels.js +++ b/src/engines/hermes/pages/channels.js @@ -60,6 +60,17 @@ const CHANNELS = [ { key: 'resolveSenderNames', labelKey: 'engine.hermesChannelResolveSenderNames' }, ], }, + { + id: 'dingtalk', + icon: 'message-circle', + titleKey: 'engine.hermesChannelDingTalk', + descKey: 'engine.hermesChannelDingTalkDesc', + secretFields: ['clientSecret'], + fields: [ + { key: 'clientId', labelKey: 'engine.hermesChannelDingTalkClientId', type: 'text', placeholder: 'dingxxxxxx' }, + { key: 'clientSecret', labelKey: 'engine.hermesChannelDingTalkClientSecret', type: 'password', placeholder: 'client secret' }, + ], + }, ] const COMMON_FIELDS = [ diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js index 91d7f3e..c3fbfcc 100644 --- a/src/locales/modules/engine.js +++ b/src/locales/modules/engine.js @@ -922,10 +922,12 @@ export default { hermesChannelDiscord: _('Discord', 'Discord', 'Discord'), hermesChannelSlack: _('Slack', 'Slack', 'Slack'), hermesChannelFeishu: _('飞书 / Lark', 'Feishu / Lark', '飛書 / Lark'), + hermesChannelDingTalk: _('钉钉', 'DingTalk', '釘釘'), hermesChannelTelegramDesc: _('通过 Telegram Bot 与 Hermes 对话,适合个人私聊和小群组。', 'Talk to Hermes through a Telegram bot for direct chats and small groups.', '透過 Telegram Bot 與 Hermes 對話,適合個人私聊和小群組。'), hermesChannelDiscordDesc: _('连接 Discord Bot,支持服务器频道和线程里的 Agent 会话。', 'Connect a Discord bot for server channels and threaded agent sessions.', '連接 Discord Bot,支援伺服器頻道和討論串裡的 Agent 會話。'), hermesChannelSlackDesc: _('连接 Slack Bot,可用于团队频道、私信和工作流通知。', 'Connect a Slack bot for team channels, direct messages, and workflow notifications.', '連接 Slack Bot,可用於團隊頻道、私訊和工作流通知。'), hermesChannelFeishuDesc: _('连接飞书或 Lark 应用,支持长连接和 Webhook 两种模式。', 'Connect a Feishu or Lark app with WebSocket or webhook mode.', '連接飛書或 Lark 應用,支援長連線和 Webhook 兩種模式。'), + hermesChannelDingTalkDesc: _('连接钉钉机器人应用,支持群聊白名单、用户白名单和 @Bot 唤醒策略。', 'Connect a DingTalk robot app with group allowlists, user allowlists, and @mention wake rules.', '連接釘釘機器人應用,支援群聊白名單、使用者白名單和 @Bot 喚醒策略。'), hermesChannelEnabled: _('已启用', 'Enabled', '已啟用'), hermesChannelDisabled: _('未启用', 'Disabled', '未啟用'), hermesChannelSave: _('保存渠道', 'Save Channel', '儲存頻道'), @@ -942,6 +944,8 @@ export default { hermesChannelWebhookPath: _('Webhook 路径', 'Webhook Path', 'Webhook 路徑'), hermesChannelFeishuAppId: _('App ID', 'App ID', 'App ID'), hermesChannelFeishuAppSecret: _('App Secret', 'App Secret', 'App Secret'), + hermesChannelDingTalkClientId: _('Client ID / App Key', 'Client ID / App Key', 'Client ID / App Key'), + hermesChannelDingTalkClientSecret: _('Client Secret', 'Client Secret', 'Client Secret'), hermesChannelFeishuDomain: _('区域', 'Region', '區域'), hermesChannelFeishuDomainCn: _('中国大陆(feishu)', 'Mainland China (feishu)', '中國大陸(feishu)'), hermesChannelFeishuDomainIntl: _('国际版(lark)', 'International (lark)', '國際版(lark)'), @@ -962,7 +966,7 @@ export default { hermesChannelAllowFromPlaceholder: _('每行或逗号分隔一个用户 ID,开放策略可留空。', 'One user ID per line or comma-separated. Leave empty for open policy.', '每行或逗號分隔一個使用者 ID,開放策略可留空。'), hermesChannelGroupAllowFromPlaceholder: _('每行或逗号分隔一个群组 / 频道 ID。', 'One group or channel ID per line or comma-separated.', '每行或逗號分隔一個群組 / 頻道 ID。'), hermesChannelRequireMention: _('群组消息需要 @Bot 才响应', 'Require @mention in groups', '群組訊息需要 @Bot 才回應'), - hermesChannelRestartHint: _('保存会将访问策略等偏好写入 config.yaml,并将 Bot Token、App Secret 及 Hermes 运行时兼容环境变量同步到 .env。Hermes Gateway 读取启动时配置,修改后请重启 Gateway。', 'Saving writes access preferences to config.yaml and syncs Bot Token, App Secret, and Hermes runtime compatibility variables to .env. Hermes Gateway reads them on startup, so restart the gateway after changes.', '儲存會將存取策略等偏好寫入 config.yaml,並將 Bot Token、App Secret 及 Hermes 執行時相容環境變數同步到 .env。Hermes Gateway 於啟動時讀取設定,修改後請重啟 Gateway。'), + hermesChannelRestartHint: _('保存会将访问策略等偏好写入 config.yaml,并将 Bot Token、App Secret、Client Secret 及 Hermes 运行时兼容环境变量同步到 .env。Hermes Gateway 读取启动时配置,修改后请重启 Gateway。', 'Saving writes access preferences to config.yaml and syncs Bot Token, App Secret, Client Secret, and Hermes runtime compatibility variables to .env. Hermes Gateway reads them on startup, so restart the gateway after changes.', '儲存會將存取策略等偏好寫入 config.yaml,並將 Bot Token、App Secret、Client Secret 及 Hermes 執行時相容環境變數同步到 .env。Hermes Gateway 於啟動時讀取設定,修改後請重啟 Gateway。'), extensionsEyebrow: _('HERMES AGENT · 扩展', 'HERMES AGENT · EXTENSIONS', 'HERMES AGENT · 擴展'), extensionsTitle: _('文档 / 插件 / 主题', 'Docs / Plugins / Themes', '文件 / 插件 / 主題'), extensionsDesc: _('集中管理 Dashboard 扩展清单、视觉主题和使用洞察。', 'Manage dashboard extension manifests, visual themes and usage intelligence.', '集中管理 Dashboard 擴展清單、視覺主題和使用洞察。'), diff --git a/tests/hermes-channel-config.test.js b/tests/hermes-channel-config.test.js index d779703..f91de21 100644 --- a/tests/hermes-channel-config.test.js +++ b/tests/hermes-channel-config.test.js @@ -50,6 +50,15 @@ test('Hermes 渠道读取会按运行时优先级合并 .env 凭证', () => { connection_mode: 'webhook', }, }, + dingtalk: { + enabled: true, + extra: { + client_id: 'yaml-client-id', + client_secret: 'yaml-client-secret', + allowed_users: ['staff-1'], + allowed_chats: ['cid-1'], + }, + }, }, }, { TELEGRAM_BOT_TOKEN: 'env-token', @@ -57,6 +66,8 @@ test('Hermes 渠道读取会按运行时优先级合并 .env 凭证', () => { FEISHU_APP_SECRET: 'env-secret', FEISHU_DOMAIN: 'feishu', FEISHU_CONNECTION_MODE: 'websocket', + DINGTALK_CLIENT_ID: 'env-client-id', + DINGTALK_CLIENT_SECRET: 'env-client-secret', }) assert.equal(values.telegram.botToken, 'env-token') @@ -65,6 +76,10 @@ test('Hermes 渠道读取会按运行时优先级合并 .env 凭证', () => { assert.equal(values.feishu.appSecret, 'env-secret') assert.equal(values.feishu.domain, 'feishu') assert.equal(values.feishu.connectionMode, 'websocket') + assert.equal(values.dingtalk.clientId, 'env-client-id') + assert.equal(values.dingtalk.clientSecret, 'env-client-secret') + assert.equal(values.dingtalk.allowFrom, 'staff-1') + assert.equal(values.dingtalk.groupAllowFrom, 'cid-1') }) test('Hermes 渠道保存会写入 Hermes 最新 platforms 配置并保留无关配置', () => { @@ -152,6 +167,20 @@ test('Hermes 渠道保存会生成运行时仍会读取的环境变量', () => { assert.equal(feishuEnv.FEISHU_WEBHOOK_PATH, '/feishu/webhook') assert.equal(feishuEnv.FEISHU_GROUP_POLICY, 'allowlist') assert.equal(feishuEnv.FEISHU_REACTIONS, 'false') + + const dingTalkEnv = buildHermesChannelEnvUpdates('dingtalk', { + clientId: 'ding-app-key', + clientSecret: 'ding-secret', + allowFrom: 'staff-1, staff-2', + groupAllowFrom: 'cid-1\ncid-2', + requireMention: true, + }) + + assert.equal(dingTalkEnv.DINGTALK_CLIENT_ID, 'ding-app-key') + assert.equal(dingTalkEnv.DINGTALK_CLIENT_SECRET, 'ding-secret') + assert.equal(dingTalkEnv.DINGTALK_ALLOWED_USERS, 'staff-1,staff-2') + assert.equal(dingTalkEnv.DINGTALK_ALLOWED_CHATS, 'cid-1,cid-2') + assert.equal(dingTalkEnv.DINGTALK_REQUIRE_MENTION, 'true') }) test('Hermes 渠道保存会从 YAML 清理旧凭证,避免覆盖 .env 运行时值', () => { @@ -182,3 +211,35 @@ test('Hermes 渠道保存会从 YAML 清理旧凭证,避免覆盖 .env 运行 assert.equal(next.platforms.slack.extra.webhook_path, '/slack/events') assert.equal(next.platforms.slack.extra.unknown_option, 'keep-me') }) + +test('Hermes 钉钉保存会使用运行时实际读取的字段', () => { + const next = mergeHermesChannelConfig({ + platforms: { + dingtalk: { + enabled: true, + extra: { + client_id: 'old-client-id', + client_secret: 'old-client-secret', + group_allow_from: ['legacy-chat'], + unknown_option: 'keep-me', + }, + }, + }, + }, 'dingtalk', { + enabled: true, + clientId: 'ding-app-key', + clientSecret: 'ding-secret', + allowFrom: 'staff-1, staff-2', + groupAllowFrom: 'cid-1\ncid-2', + requireMention: true, + }) + + assert.equal(next.platforms.dingtalk.enabled, true) + assert.equal(next.platforms.dingtalk.extra.client_id, undefined) + assert.equal(next.platforms.dingtalk.extra.client_secret, undefined) + assert.equal(next.platforms.dingtalk.extra.group_allow_from, undefined) + assert.deepEqual(next.platforms.dingtalk.extra.allowed_users, ['staff-1', 'staff-2']) + assert.deepEqual(next.platforms.dingtalk.extra.allowed_chats, ['cid-1', 'cid-2']) + assert.equal(next.platforms.dingtalk.extra.require_mention, true) + assert.equal(next.platforms.dingtalk.extra.unknown_option, 'keep-me') +})