import test from 'node:test' import assert from 'node:assert/strict' import { buildOpenClawChannelDiagnosis, buildMessagingPlatformFormValues, listPlatformAccounts, mergeOpenClawMessagingPlatformConfig, resolveMessagingCredentialValueForSave, normalizeMessagingPlatformForm, } from '../scripts/dev-api.js' test('渠道保存会为 Telegram 补齐新版 OpenClaw 必填访问策略', () => { const form = normalizeMessagingPlatformForm('telegram', { botToken: '123:token', }) assert.equal(form.botToken, '123:token') assert.equal(form.dmPolicy, 'pairing') assert.equal(form.groupPolicy, 'allowlist') }) test('渠道保存会把旧 UI 策略值转换为 OpenClaw 支持的枚举', () => { const form = normalizeMessagingPlatformForm('slack', { mode: 'socket', botToken: 'xoxb-token', appToken: 'xapp-token', dmPolicy: 'allow', groupPolicy: 'mentioned', }) assert.equal(form.dmPolicy, 'open') assert.deepEqual(form.allowFrom, ['*']) assert.equal(form.groupPolicy, 'open') assert.equal(form.requireMention, true) assert.equal(form.webhookPath, '/slack/events') assert.equal(form.userTokenReadOnly, false) }) test('渠道保存不会向不支持顶层 requireMention 的平台写入非法字段', () => { const form = normalizeMessagingPlatformForm('signal', { account: '+15551234567', dmPolicy: 'deny', groupPolicy: 'mentioned', }) assert.equal(form.dmPolicy, 'disabled') assert.equal(form.groupPolicy, 'open') assert.equal(Object.hasOwn(form, 'requireMention'), false) }) test('WhatsApp 渠道保存会写入扫码运行和访问策略字段并启用插件', () => { const cfg = { channels: {} } mergeOpenClawMessagingPlatformConfig(cfg, { platform: 'whatsapp', accountId: 'phone-a', form: { enabled: 'true', configWrites: 'true', sendReadReceipts: 'false', selfChatMode: 'true', dmPolicy: 'allowlist', allowFrom: '+15551234567, +15557654321', defaultTo: '+15550001111', groupPolicy: 'allowlist', groupAllowFrom: '120363@g.us, 120364@g.us', contextVisibility: 'allowlist_quote', historyLimit: '80', dmHistoryLimit: '20', mediaMaxMb: '50', debounceMs: '800', textChunkLimit: '1800', chunkMode: 'newline', blockStreaming: 'true', reactionLevel: 'ack', replyToMode: 'first', messagePrefix: '[WA]', responsePrefix: '[AI]', ackEmoji: '✅', ackDirect: 'true', ackGroup: 'mentions', }, }) const root = cfg.channels.whatsapp const account = root.accounts['phone-a'] assert.equal(root.defaultAccount, 'phone-a') assert.equal(account.enabled, true) assert.equal(account.configWrites, true) assert.equal(account.sendReadReceipts, false) assert.equal(account.selfChatMode, true) assert.equal(account.dmPolicy, 'allowlist') assert.deepEqual(account.allowFrom, ['+15551234567', '+15557654321']) assert.equal(account.defaultTo, '+15550001111') assert.equal(account.groupPolicy, 'allowlist') assert.deepEqual(account.groupAllowFrom, ['120363@g.us', '120364@g.us']) assert.equal(account.contextVisibility, 'allowlist_quote') assert.equal(account.historyLimit, 80) assert.equal(account.dmHistoryLimit, 20) assert.equal(account.mediaMaxMb, 50) assert.equal(account.debounceMs, 800) assert.equal(account.textChunkLimit, 1800) assert.equal(account.chunkMode, 'newline') assert.equal(account.blockStreaming, true) assert.equal(account.reactionLevel, 'ack') assert.equal(account.replyToMode, 'first') assert.equal(account.messagePrefix, '[WA]') assert.equal(account.responsePrefix, '[AI]') assert.deepEqual(account.ackReaction, { emoji: '✅', direct: true, group: 'mentions' }) assert.equal(cfg.plugins.entries.whatsapp.enabled, true) }) test('WhatsApp 读取会回显扫码运行字段且诊断不要求 Bot Token', () => { const values = buildMessagingPlatformFormValues('whatsapp', { enabled: true, configWrites: true, sendReadReceipts: false, selfChatMode: true, dmPolicy: 'open', allowFrom: ['*'], groupPolicy: 'allowlist', groupAllowFrom: ['120363@g.us'], historyLimit: 50, debounceMs: 800, mediaMaxMb: 50, blockStreaming: true, ackReaction: { emoji: '✅', direct: true, group: 'mentions' }, }) const diagnosis = buildOpenClawChannelDiagnosis({ platform: 'whatsapp', configExists: true, channelEnabled: true, form: values, }) assert.equal(values.enabled, 'true') assert.equal(values.configWrites, 'true') assert.equal(values.sendReadReceipts, 'false') assert.equal(values.selfChatMode, 'true') assert.equal(values.allowFrom, '*') assert.equal(values.groupAllowFrom, '120363@g.us') assert.equal(values.historyLimit, '50') assert.equal(values.debounceMs, '800') assert.equal(values.mediaMaxMb, '50') assert.equal(values.blockStreaming, 'true') assert.equal(values.ackEmoji, '✅') assert.equal(values.ackDirect, 'true') assert.equal(values.ackGroup, 'mentions') assert.equal(diagnosis.checks.find(item => item.id === 'credentials')?.ok, true) assert.match(diagnosis.checks.find(item => item.id === 'credentials')?.title || '', /扫码|会话/) }) test('ClickClack 渠道保存会写入自托管运行字段并启用插件', () => { const cfg = { channels: {} } mergeOpenClawMessagingPlatformConfig(cfg, { platform: 'clickclack', accountId: 'work', form: { name: 'Ops Workspace', enabled: 'true', baseUrl: 'https://clickclack.example.com', token: 'clickclack-token', workspace: 'ops', botUserId: 'bot-1', agentId: 'agent-1', replyMode: 'model', model: 'claude-sonnet-4-5', systemPrompt: 'You are an ops assistant.', timeoutSeconds: '120', toolsAllow: 'shell, browser.search', senderIsOwner: 'true', defaultTo: 'channel:ops', allowFrom: 'channel:ops, dm:alice', reconnectMs: '2500', }, }) const root = cfg.channels.clickclack const account = root.accounts.work assert.equal(root.defaultAccount, 'work') assert.equal(account.enabled, true) assert.equal(account.name, 'Ops Workspace') assert.equal(account.baseUrl, 'https://clickclack.example.com') assert.equal(account.token, 'clickclack-token') assert.equal(account.workspace, 'ops') assert.equal(account.botUserId, 'bot-1') assert.equal(account.agentId, 'agent-1') assert.equal(account.replyMode, 'model') assert.equal(account.model, 'claude-sonnet-4-5') assert.equal(account.systemPrompt, 'You are an ops assistant.') assert.equal(account.timeoutSeconds, 120) assert.deepEqual(account.toolsAllow, ['shell', 'browser.search']) assert.equal(account.senderIsOwner, true) assert.equal(account.defaultTo, 'channel:ops') assert.deepEqual(account.allowFrom, ['channel:ops', 'dm:alice']) assert.equal(account.reconnectMs, 2500) assert.equal(cfg.plugins.entries.clickclack.enabled, true) }) test('ClickClack 读取会回显运行字段且诊断要求 Base URL、Token 和 Workspace', () => { const values = buildMessagingPlatformFormValues('clickclack', { name: 'Ops Workspace', enabled: true, baseUrl: 'https://clickclack.example.com', token: 'clickclack-token', workspace: 'ops', botUserId: 'bot-1', agentId: 'agent-1', replyMode: 'agent', model: 'claude-sonnet-4-5', systemPrompt: 'You are an ops assistant.', timeoutSeconds: 90, toolsAllow: ['shell', 'browser.search'], senderIsOwner: false, defaultTo: 'channel:general', allowFrom: ['*'], reconnectMs: 1500, }) const missingWorkspace = buildOpenClawChannelDiagnosis({ platform: 'clickclack', configExists: true, channelEnabled: true, form: { baseUrl: 'https://clickclack.example.com', token: 'clickclack-token', }, }) const ready = buildOpenClawChannelDiagnosis({ platform: 'clickclack', configExists: true, channelEnabled: true, form: values, }) assert.equal(values.enabled, 'true') assert.equal(values.baseUrl, 'https://clickclack.example.com') assert.equal(values.token, 'clickclack-token') assert.equal(values.workspace, 'ops') assert.equal(values.replyMode, 'agent') assert.equal(values.timeoutSeconds, '90') assert.equal(values.toolsAllow, 'shell, browser.search') assert.equal(values.senderIsOwner, 'false') assert.equal(values.allowFrom, '*') assert.equal(values.reconnectMs, '1500') assert.equal(missingWorkspace.checks.find(item => item.id === 'credentials')?.ok, false) assert.match(missingWorkspace.checks.find(item => item.id === 'credentials')?.detail || '', /Workspace/) assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true) }) test('Nextcloud Talk 渠道保存会写入自托管 Talk 字段并启用插件', () => { const cfg = { channels: {} } mergeOpenClawMessagingPlatformConfig(cfg, { platform: 'nextcloud-talk', accountId: 'work', form: { enabled: 'true', name: 'Work Cloud', baseUrl: 'https://cloud.example.com', botSecret: 'bot-secret', apiUser: 'openclaw-bot', apiPassword: 'app-password', webhookPort: '8788', webhookHost: '0.0.0.0', webhookPath: '/nextcloud-talk-webhook', webhookPublicUrl: 'https://panel.example.com/nextcloud-talk-webhook', dmPolicy: 'allowlist', allowFrom: 'alice, bob', groupPolicy: 'mentioned', groupAllowFrom: 'room-token-1, room-token-2', historyLimit: '80', dmHistoryLimit: '20', mediaMaxMb: '50', textChunkLimit: '4000', chunkMode: 'newline', blockStreaming: 'true', responsePrefix: '[Talk]', dangerouslyAllowPrivateNetwork: 'true', }, }) const root = cfg.channels['nextcloud-talk'] const account = root.accounts.work assert.equal(root.defaultAccount, 'work') assert.equal(account.enabled, true) assert.equal(account.name, 'Work Cloud') assert.equal(account.baseUrl, 'https://cloud.example.com') assert.equal(account.botSecret, 'bot-secret') assert.equal(account.apiUser, 'openclaw-bot') assert.equal(account.apiPassword, 'app-password') assert.equal(account.webhookPort, 8788) assert.equal(account.webhookHost, '0.0.0.0') assert.equal(account.webhookPath, '/nextcloud-talk-webhook') assert.equal(account.webhookPublicUrl, 'https://panel.example.com/nextcloud-talk-webhook') assert.equal(account.dmPolicy, 'allowlist') assert.deepEqual(account.allowFrom, ['alice', 'bob']) assert.equal(account.groupPolicy, 'open') assert.equal(account.requireMention, true) assert.deepEqual(account.groupAllowFrom, ['room-token-1', 'room-token-2']) assert.equal(account.historyLimit, 80) assert.equal(account.dmHistoryLimit, 20) assert.equal(account.mediaMaxMb, 50) assert.equal(account.textChunkLimit, 4000) assert.equal(account.chunkMode, 'newline') assert.equal(account.blockStreaming, true) assert.equal(account.responsePrefix, '[Talk]') assert.deepEqual(account.network, { dangerouslyAllowPrivateNetwork: true }) assert.equal(cfg.plugins.entries['nextcloud-talk'].enabled, true) }) test('Nextcloud Talk 读取和诊断支持 Bot Secret 或 Secret File 二选一', () => { const values = buildMessagingPlatformFormValues('nextcloud-talk', { enabled: true, baseUrl: 'https://cloud.example.com', botSecretFile: '/run/secrets/nextcloud-talk-secret', apiUser: 'openclaw-bot', allowFrom: ['alice'], groupPolicy: 'open', requireMention: true, groupAllowFrom: ['room-token-1'], webhookPort: 8788, historyLimit: 80, blockStreaming: true, network: { dangerouslyAllowPrivateNetwork: true }, }) const missingSecret = buildOpenClawChannelDiagnosis({ platform: 'nextcloud-talk', configExists: true, channelEnabled: true, form: { baseUrl: 'https://cloud.example.com' }, }) const ready = buildOpenClawChannelDiagnosis({ platform: 'nextcloud-talk', configExists: true, channelEnabled: true, form: values, }) assert.equal(values.enabled, 'true') assert.equal(values.baseUrl, 'https://cloud.example.com') assert.equal(values.botSecretFile, '/run/secrets/nextcloud-talk-secret') assert.equal(values.groupPolicy, 'mentioned') assert.equal(values.groupAllowFrom, 'room-token-1') assert.equal(values.webhookPort, '8788') assert.equal(values.historyLimit, '80') assert.equal(values.blockStreaming, 'true') assert.equal(values.dangerouslyAllowPrivateNetwork, 'true') assert.equal(missingSecret.checks.find(item => item.id === 'credentials')?.ok, false) assert.match(missingSecret.checks.find(item => item.id === 'credentials')?.detail || '', /Bot Secret.*Secret File/) assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true) }) test('Twitch 渠道保存会写入聊天账号字段并启用插件', () => { const cfg = { channels: {} } mergeOpenClawMessagingPlatformConfig(cfg, { platform: 'twitch', accountId: 'stream', form: { enabled: 'true', username: 'openclawbot', accessToken: 'oauth:access-token', clientId: 'client-id', channel: '#openclaw', allowFrom: '123456789, 987654321', allowedRoles: 'moderator, vip', requireMention: 'true', responsePrefix: '[Twitch]', clientSecret: 'client-secret', refreshToken: 'refresh-token', expiresIn: '3600', obtainmentTimestamp: '1779490000', }, }) const root = cfg.channels.twitch const account = root.accounts.stream assert.equal(root.defaultAccount, 'stream') assert.equal(account.enabled, true) assert.equal(account.username, 'openclawbot') assert.equal(account.accessToken, 'oauth:access-token') assert.equal(account.clientId, 'client-id') assert.equal(account.channel, '#openclaw') assert.deepEqual(account.allowFrom, ['123456789', '987654321']) assert.deepEqual(account.allowedRoles, ['moderator', 'vip']) assert.equal(account.requireMention, true) assert.equal(account.responsePrefix, '[Twitch]') assert.equal(account.clientSecret, 'client-secret') assert.equal(account.refreshToken, 'refresh-token') assert.equal(account.expiresIn, 3600) assert.equal(account.obtainmentTimestamp, 1779490000) assert.equal(cfg.plugins.entries.twitch.enabled, true) }) test('Twitch 读取和诊断会回显访问控制与刷新 Token 字段', () => { const values = buildMessagingPlatformFormValues('twitch', { enabled: true, username: 'openclawbot', accessToken: 'oauth:access-token', clientId: 'client-id', channel: '#openclaw', allowFrom: ['123456789'], allowedRoles: ['moderator', 'subscriber'], requireMention: true, responsePrefix: '[Twitch]', clientSecret: 'client-secret', refreshToken: 'refresh-token', expiresIn: 3600, obtainmentTimestamp: 1779490000, }) const missingToken = buildOpenClawChannelDiagnosis({ platform: 'twitch', configExists: true, channelEnabled: true, form: { username: 'openclawbot', clientId: 'client-id', channel: '#openclaw' }, }) const ready = buildOpenClawChannelDiagnosis({ platform: 'twitch', configExists: true, channelEnabled: true, form: values, }) assert.equal(values.enabled, 'true') assert.equal(values.username, 'openclawbot') assert.equal(values.accessToken, 'oauth:access-token') assert.equal(values.clientId, 'client-id') assert.equal(values.channel, '#openclaw') assert.equal(values.allowFrom, '123456789') assert.equal(values.allowedRoles, 'moderator, subscriber') assert.equal(values.requireMention, 'true') assert.equal(values.refreshToken, 'refresh-token') assert.equal(values.expiresIn, '3600') assert.equal(values.obtainmentTimestamp, '1779490000') assert.equal(missingToken.checks.find(item => item.id === 'credentials')?.ok, false) assert.match(missingToken.checks.find(item => item.id === 'credentials')?.detail || '', /Access Token/) assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true) }) test('Signal 渠道保存会保留多账号和上游运行字段', () => { const cfg = { channels: {} } mergeOpenClawMessagingPlatformConfig(cfg, { platform: 'signal', accountId: 'phone-a', form: { account: '+15551234567', cliPath: 'signal-cli', httpUrl: 'http://127.0.0.1:8080', dmPolicy: 'allowlist', allowFrom: '+15550000001', groupPolicy: 'allowlist', groupAllowFrom: 'group-1, group-2', mediaMaxMb: '25', historyLimit: '80', dmHistoryLimit: '20', textChunkLimit: '1800', blockStreaming: 'true', responsePrefix: '[Signal]', }, }) const root = cfg.channels.signal const account = root.accounts['phone-a'] assert.equal(root.defaultAccount, 'phone-a') assert.equal(account.account, '+15551234567') assert.equal(account.cliPath, 'signal-cli') assert.equal(account.httpUrl, 'http://127.0.0.1:8080') assert.equal(account.dmPolicy, 'allowlist') assert.deepEqual(account.allowFrom, ['+15550000001']) assert.equal(account.groupPolicy, 'allowlist') assert.deepEqual(account.groupAllowFrom, ['group-1', 'group-2']) assert.equal(account.mediaMaxMb, 25) assert.equal(account.historyLimit, 80) assert.equal(account.dmHistoryLimit, 20) assert.equal(account.textChunkLimit, 1800) assert.equal(account.blockStreaming, true) assert.equal(account.responsePrefix, '[Signal]') }) test('Signal 渠道读取会回显群组和运行字段', () => { const values = buildMessagingPlatformFormValues('signal', { account: '+15551234567', groupAllowFrom: ['group-1', 'group-2'], mediaMaxMb: 25, historyLimit: 80, dmHistoryLimit: 20, textChunkLimit: 1800, blockStreaming: true, responsePrefix: '[Signal]', }) assert.equal(values.account, '+15551234567') assert.equal(values.groupAllowFrom, 'group-1, group-2') assert.equal(values.mediaMaxMb, '25') assert.equal(values.historyLimit, '80') assert.equal(values.dmHistoryLimit, '20') assert.equal(values.textChunkLimit, '1800') assert.equal(values.blockStreaming, 'true') assert.equal(values.responsePrefix, '[Signal]') }) test('渠道保存会为飞书补齐新版内核要求的默认字段', () => { const form = normalizeMessagingPlatformForm('feishu', { appId: 'cli_a', appSecret: 'secret', domain: '', }) assert.equal(form.domain, 'feishu') assert.equal(form.connectionMode, 'websocket') assert.equal(form.webhookPath, '/feishu/events') assert.equal(form.dmPolicy, 'pairing') assert.equal(form.groupPolicy, 'allowlist') assert.equal(form.reactionNotifications, 'off') assert.equal(form.typingIndicator, true) assert.equal(form.resolveSenderNames, true) }) test('渠道读取会把新版访问策略字段回显为表单可编辑值', () => { const values = buildMessagingPlatformFormValues('telegram', { botToken: '123:token', dmPolicy: 'allowlist', groupPolicy: 'disabled', allowFrom: ['u-1', 'u-2'], }) assert.equal(values.botToken, '123:token') assert.equal(values.dmPolicy, 'allowlist') assert.equal(values.groupPolicy, 'disabled') assert.equal(values.allowFrom, 'u-1, u-2') assert.equal(values.allowedUsers, 'u-1, u-2') }) test('渠道读取会合并飞书账号凭证和根节点共享策略字段', () => { const values = buildMessagingPlatformFormValues( 'feishu', { appId: 'cli_a', appSecret: 'secret', }, { channelRoot: { domain: 'lark', connectionMode: 'websocket', webhookPath: '/feishu/events', dmPolicy: 'pairing', groupPolicy: 'allowlist', reactionNotifications: 'off', typingIndicator: true, resolveSenderNames: false, }, }, ) assert.equal(values.appId, 'cli_a') assert.equal(values.appSecret, 'secret') assert.equal(values.domain, 'lark') assert.equal(values.connectionMode, 'websocket') assert.equal(values.webhookPath, '/feishu/events') assert.equal(values.dmPolicy, 'pairing') assert.equal(values.groupPolicy, 'allowlist') assert.equal(values.reactionNotifications, 'off') assert.equal(values.typingIndicator, 'true') assert.equal(values.resolveSenderNames, 'false') }) test('渠道读取飞书多账号时不会用根节点旧凭证覆盖账号凭证', () => { const values = buildMessagingPlatformFormValues( 'feishu', { appId: 'account_app', appSecret: 'account_secret', dmPolicy: 'pairing', }, { channelRoot: { appId: 'root_app', appSecret: 'root_secret', domain: 'lark', groupPolicy: 'allowlist', }, }, ) assert.equal(values.appId, 'account_app') assert.equal(values.appSecret, 'account_secret') assert.equal(values.domain, 'lark') assert.equal(values.dmPolicy, 'pairing') assert.equal(values.groupPolicy, 'allowlist') }) test('渠道读取会把 open + requireMention 反向回显为仅提及时策略', () => { const values = buildMessagingPlatformFormValues('slack', { mode: 'socket', botToken: 'xoxb-token', appToken: 'xapp-token', groupPolicy: 'open', requireMention: true, }) assert.equal(values.groupPolicy, 'mentioned') assert.equal(values.requireMention, 'true') }) test('Discord 渠道读取会回显 applicationId', () => { const values = buildMessagingPlatformFormValues('discord', { token: 'discord-token', applicationId: '123456789012345678', }) assert.equal(values.token, 'discord-token') assert.equal(values.applicationId, '123456789012345678') }) test('渠道保存会在用户改回所有群组时显式清除仅提及开关', () => { const form = normalizeMessagingPlatformForm('slack', { mode: 'socket', botToken: 'xoxb-token', appToken: 'xapp-token', groupPolicy: 'open', }) assert.equal(form.groupPolicy, 'open') assert.equal(form.requireMention, false) }) test('渠道读取会把 SecretRef 密钥显示为安全占位并携带原始对象', () => { const secretRef = { source: 'env', provider: 'default', id: 'TELEGRAM_BOT_TOKEN' } const values = buildMessagingPlatformFormValues('telegram', { botToken: secretRef, dmPolicy: 'pairing', groupPolicy: 'allowlist', }) assert.equal(values.botToken, 'SecretRef(env:default:TELEGRAM_BOT_TOKEN)') assert.deepEqual(values.__secretRefs, { botToken: secretRef }) }) test('渠道保存时用户未改动 SecretRef 占位会保留原始密钥引用', () => { const secretRef = { source: 'env', provider: 'default', id: 'SLACK_BOT_TOKEN' } const value = resolveMessagingCredentialValueForSave({ form: { botToken: 'SecretRef(env:default:SLACK_BOT_TOKEN)' }, current: { botToken: secretRef }, key: 'botToken', }) assert.deepEqual(value, secretRef) }) test('渠道保存时用户输入新密钥会替换旧 SecretRef', () => { const secretRef = { source: 'env', provider: 'default', id: 'DISCORD_BOT_TOKEN' } const value = resolveMessagingCredentialValueForSave({ form: { token: 'new-discord-token' }, current: { token: secretRef }, key: 'token', }) assert.equal(value, 'new-discord-token') }) test('渠道账号列表会把 SecretRef 标识显示为安全占位', () => { const accounts = listPlatformAccounts({ accounts: { prod: { appId: { source: 'env', provider: 'default', id: 'FEISHU_APP_ID' }, }, backup: { clientId: { source: 'env', provider: 'default', id: 'DINGTALK_CLIENT_ID' }, }, }, }) assert.deepEqual(accounts, [ { accountId: 'backup', appId: 'SecretRef(env:default:DINGTALK_CLIENT_ID)' }, { accountId: 'prod', appId: 'SecretRef(env:default:FEISHU_APP_ID)' }, ]) }) test('渠道保存时 clientId 未改动 SecretRef 占位会保留原始引用', () => { const secretRef = { source: 'env', provider: 'default', id: 'DINGTALK_CLIENT_ID' } const value = resolveMessagingCredentialValueForSave({ form: { clientId: 'SecretRef(env:default:DINGTALK_CLIENT_ID)' }, current: { clientId: secretRef }, key: 'clientId', }) 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') }) test('通用渠道诊断会指出 Telegram 缺少 Bot Token', () => { const result = buildOpenClawChannelDiagnosis({ platform: 'telegram', configExists: true, channelEnabled: true, form: { dmPolicy: 'pairing', groupPolicy: 'allowlist', }, }) assert.equal(result.ok, false) assert.equal(result.overallReady, false) assert.equal(result.checks.find(item => item.id === 'credentials')?.ok, false) assert.match(result.checks.find(item => item.id === 'credentials')?.detail || '', /Bot Token/) }) test('通用渠道诊断在缺少渠道配置时不会误报渠道已禁用', () => { const result = buildOpenClawChannelDiagnosis({ platform: 'telegram', configExists: false, channelEnabled: true, form: {}, }) assert.equal(result.ok, false) assert.equal(result.checks.find(item => item.id === 'config_exists')?.ok, false) assert.equal(result.checks.find(item => item.id === 'channel_enabled')?.ok, true) assert.match(result.checks.find(item => item.id === 'channel_enabled')?.detail || '', /未被显式禁用/) }) test('通用渠道诊断会按 Slack 模式检查动态必填凭证', () => { const socketResult = buildOpenClawChannelDiagnosis({ platform: 'slack', configExists: true, channelEnabled: true, form: { mode: 'socket', botToken: 'xoxb-token', }, }) const httpResult = buildOpenClawChannelDiagnosis({ platform: 'slack', configExists: true, channelEnabled: true, form: { mode: 'http', botToken: 'xoxb-token', signingSecret: 'secret', }, }) assert.equal(socketResult.ok, false) assert.match(socketResult.checks.find(item => item.id === 'credentials')?.detail || '', /App Token/) assert.equal(httpResult.checks.find(item => item.id === 'credentials')?.ok, true) }) test('通用渠道诊断会识别钉钉 Client ID 和 Client Secret', () => { const result = buildOpenClawChannelDiagnosis({ platform: 'dingtalk', configExists: true, channelEnabled: true, form: { clientId: 'ding-app-key', clientSecret: 'ding-secret', }, verifyResult: { valid: true, details: ['已通过 accessToken 接口校验'], }, }) assert.equal(result.ok, true) assert.equal(result.overallReady, true) assert.equal(result.checks.find(item => item.id === 'credentials')?.ok, true) assert.equal(result.checks.find(item => item.id === 'online_verify')?.ok, true) }) test('Zalo 渠道保存会补齐策略并保留 Bot Token 或 Token File', () => { const tokenForm = normalizeMessagingPlatformForm('zalo', { botToken: 'zalo-token', groupAllowFrom: 'group-1, group-2', mediaMaxMb: '25', }) const tokenFileForm = normalizeMessagingPlatformForm('zalo', { tokenFile: '/run/secrets/zalo-token', }) assert.equal(tokenForm.botToken, 'zalo-token') assert.equal(tokenForm.dmPolicy, 'pairing') assert.equal(tokenForm.groupPolicy, 'allowlist') assert.deepEqual(tokenForm.groupAllowFrom, ['group-1', 'group-2']) assert.equal(tokenForm.mediaMaxMb, 25) assert.equal(tokenFileForm.tokenFile, '/run/secrets/zalo-token') }) test('OpenClaw 渠道保存会写入 Zalo 多账号配置', () => { const cfg = { channels: {} } mergeOpenClawMessagingPlatformConfig(cfg, { platform: 'zalo', accountId: 'vn', form: { botToken: 'zalo-token', groupAllowFrom: 'thread-1', mediaMaxMb: '30', }, }) assert.equal(cfg.channels.zalo.defaultAccount, 'vn') assert.equal(cfg.channels.zalo.accounts.vn.botToken, 'zalo-token') assert.deepEqual(cfg.channels.zalo.accounts.vn.groupAllowFrom, ['thread-1']) assert.equal(cfg.channels.zalo.accounts.vn.mediaMaxMb, 30) }) test('Zalo 诊断接受 Bot Token 或 Token File 二选一', () => { const tokenResult = buildOpenClawChannelDiagnosis({ platform: 'zalo', configExists: true, channelEnabled: true, form: { botToken: 'zalo-token' }, }) const fileResult = buildOpenClawChannelDiagnosis({ platform: 'zalo', configExists: true, channelEnabled: true, form: { tokenFile: '/run/secrets/zalo-token' }, }) const missingResult = buildOpenClawChannelDiagnosis({ platform: 'zalo', configExists: true, channelEnabled: true, form: {}, }) assert.equal(tokenResult.checks.find(item => item.id === 'credentials')?.ok, true) assert.equal(fileResult.checks.find(item => item.id === 'credentials')?.ok, true) assert.equal(missingResult.checks.find(item => item.id === 'credentials')?.ok, false) assert.match(missingResult.checks.find(item => item.id === 'credentials')?.detail || '', /Bot Token.*Token File/) }) test('Zalo Personal 保存和诊断按二维码会话型渠道处理', () => { const form = normalizeMessagingPlatformForm('zalouser', { profile: 'work', dangerouslyAllowNameMatching: 'true', allowFrom: '12345, Alice', groupAllowFrom: 'group-1', historyLimit: '12', }) const cfg = { channels: {} } mergeOpenClawMessagingPlatformConfig(cfg, { platform: 'zalouser', accountId: 'work', form, }) const result = buildOpenClawChannelDiagnosis({ platform: 'zalouser', configExists: true, channelEnabled: true, form: buildMessagingPlatformFormValues('zalouser', cfg.channels.zalouser.accounts.work), }) assert.equal(cfg.channels.zalouser.defaultAccount, 'work') assert.equal(cfg.channels.zalouser.accounts.work.profile, 'work') assert.equal(cfg.channels.zalouser.accounts.work.dangerouslyAllowNameMatching, true) assert.deepEqual(cfg.channels.zalouser.accounts.work.allowFrom, ['12345', 'Alice']) assert.deepEqual(cfg.channels.zalouser.accounts.work.groupAllowFrom, ['group-1']) assert.equal(cfg.channels.zalouser.accounts.work.historyLimit, 12) assert.equal(result.checks.find(item => item.id === 'credentials')?.ok, true) assert.equal(result.checks.find(item => item.id === 'credentials')?.title, '登录/会话配置') }) test('LINE 渠道保存会写入双凭证组合并支持多账号', () => { const cfg = { channels: {} } mergeOpenClawMessagingPlatformConfig(cfg, { platform: 'line', accountId: 'jp', form: { tokenFile: '/run/secrets/line-token', secretFile: '/run/secrets/line-secret', allowFrom: 'U123, U456', groupAllowFrom: 'C123', groupPolicy: 'open', mediaMaxMb: '25', webhookPath: '/line/webhook', }, }) const account = cfg.channels.line.accounts.jp assert.equal(cfg.channels.line.defaultAccount, 'jp') assert.equal(account.tokenFile, '/run/secrets/line-token') assert.equal(account.secretFile, '/run/secrets/line-secret') assert.deepEqual(account.allowFrom, ['U123', 'U456']) assert.deepEqual(account.groupAllowFrom, ['C123']) assert.equal(account.groupPolicy, 'open') assert.equal(account.mediaMaxMb, 25) assert.equal(account.webhookPath, '/line/webhook') }) test('LINE 诊断要求 token 与 secret 两组凭证各满足一项', () => { const ready = buildOpenClawChannelDiagnosis({ platform: 'line', configExists: true, channelEnabled: true, form: { channelAccessToken: 'line-token', secretFile: '/run/secrets/line-secret', }, }) const missingSecret = buildOpenClawChannelDiagnosis({ platform: 'line', configExists: true, channelEnabled: true, form: { channelAccessToken: 'line-token', }, }) assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true) assert.equal(missingSecret.checks.find(item => item.id === 'credentials')?.ok, false) assert.match(missingSecret.checks.find(item => item.id === 'credentials')?.detail || '', /Channel Secret.*Secret File/) }) test('Mattermost 渠道保存会写入嵌套命令和网络配置', () => { const cfg = { channels: {} } mergeOpenClawMessagingPlatformConfig(cfg, { platform: 'mattermost', accountId: 'ops', form: { botToken: 'mattermost-token', baseUrl: 'https://mattermost.example.com/', groupPolicy: 'mentioned', allowFrom: '@alice, bob', groupAllowFrom: 'town-square', callbackPath: '/api/channels/mattermost/ops', callbackUrl: 'https://panel.example.com/api/channels/mattermost/ops', dangerouslyAllowNameMatching: 'true', dangerouslyAllowPrivateNetwork: 'true', replyToMode: 'all', }, }) const account = cfg.channels.mattermost.accounts.ops assert.equal(cfg.channels.mattermost.defaultAccount, 'ops') assert.equal(account.botToken, 'mattermost-token') assert.equal(account.baseUrl, 'https://mattermost.example.com/') assert.equal(account.groupPolicy, 'open') assert.equal(account.requireMention, true) assert.deepEqual(account.allowFrom, ['@alice', 'bob']) assert.deepEqual(account.groupAllowFrom, ['town-square']) assert.equal(account.commands.callbackPath, '/api/channels/mattermost/ops') assert.equal(account.commands.callbackUrl, 'https://panel.example.com/api/channels/mattermost/ops') assert.equal(account.network.dangerouslyAllowPrivateNetwork, true) assert.equal(account.dangerouslyAllowNameMatching, true) assert.equal(account.replyToMode, 'all') }) test('Mattermost 诊断要求 Bot Token 和 Base URL', () => { const missingBaseUrl = buildOpenClawChannelDiagnosis({ platform: 'mattermost', configExists: true, channelEnabled: true, form: { botToken: 'mattermost-token' }, }) const ready = buildOpenClawChannelDiagnosis({ platform: 'mattermost', configExists: true, channelEnabled: true, form: { botToken: 'mattermost-token', baseUrl: 'https://mattermost.example.com', }, }) assert.equal(missingBaseUrl.checks.find(item => item.id === 'credentials')?.ok, false) assert.match(missingBaseUrl.checks.find(item => item.id === 'credentials')?.detail || '', /Base URL/) assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true) }) test('Synology Chat 渠道保存会写入上游运行时字段并支持多账号', () => { const cfg = { channels: {} } mergeOpenClawMessagingPlatformConfig(cfg, { platform: 'synology-chat', accountId: 'nas', form: { token: 'synology-token', incomingUrl: 'https://nas.example.com/webapi/entry.cgi', nasHost: 'https://nas.example.com', webhookPath: '/webhook/synology', dmPolicy: 'allowlist', allowedUserIds: 'alice, bob', rateLimitPerMinute: '45', botName: 'OpenClaw Ops', dangerouslyAllowNameMatching: 'true', dangerouslyAllowInheritedWebhookPath: 'true', allowInsecureSsl: 'true', }, }) const account = cfg.channels['synology-chat'].accounts.nas assert.equal(cfg.channels['synology-chat'].defaultAccount, 'nas') assert.equal(account.token, 'synology-token') assert.equal(account.incomingUrl, 'https://nas.example.com/webapi/entry.cgi') assert.equal(account.nasHost, 'https://nas.example.com') assert.equal(account.webhookPath, '/webhook/synology') assert.equal(account.dmPolicy, 'allowlist') assert.deepEqual(account.allowedUserIds, ['alice', 'bob']) assert.equal(account.rateLimitPerMinute, 45) assert.equal(account.botName, 'OpenClaw Ops') assert.equal(account.dangerouslyAllowNameMatching, true) assert.equal(account.dangerouslyAllowInheritedWebhookPath, true) assert.equal(account.allowInsecureSsl, true) assert.equal(cfg.plugins.entries['synology-chat'].enabled, true) }) test('Synology Chat 诊断要求 Token 和 Incoming URL', () => { const missingUrl = buildOpenClawChannelDiagnosis({ platform: 'synology-chat', configExists: true, channelEnabled: true, form: { token: 'synology-token' }, }) const ready = buildOpenClawChannelDiagnosis({ platform: 'synology-chat', configExists: true, channelEnabled: true, form: { token: 'synology-token', incomingUrl: 'https://nas.example.com/webapi/entry.cgi', }, }) assert.equal(missingUrl.checks.find(item => item.id === 'credentials')?.ok, false) assert.match(missingUrl.checks.find(item => item.id === 'credentials')?.detail || '', /Incoming URL/) assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true) }) test('Google Chat 渠道保存会写入 service account 与嵌套 DM 策略', () => { const cfg = { channels: {} } mergeOpenClawMessagingPlatformConfig(cfg, { platform: 'googlechat', accountId: 'workspace', form: { serviceAccountFile: '/run/secrets/googlechat.json', audienceType: 'app-url', audience: 'https://panel.example.com/googlechat', webhookPath: '/googlechat', webhookUrl: 'https://panel.example.com/googlechat', dmPolicy: 'open', allowFrom: 'users/123', groupPolicy: 'mentioned', groupAllowFrom: 'spaces/AAA', dangerouslyAllowNameMatching: 'true', requireMention: 'true', mediaMaxMb: '20', responsePrefix: '[AI]', }, }) const account = cfg.channels.googlechat.accounts.workspace assert.equal(cfg.channels.googlechat.defaultAccount, 'workspace') assert.equal(account.serviceAccountFile, '/run/secrets/googlechat.json') assert.equal(account.audienceType, 'app-url') assert.equal(account.audience, 'https://panel.example.com/googlechat') assert.equal(account.webhookPath, '/googlechat') assert.equal(account.webhookUrl, 'https://panel.example.com/googlechat') assert.deepEqual(account.dm, { policy: 'open', allowFrom: ['users/123', '*'] }) assert.equal(account.groupPolicy, 'open') assert.equal(account.requireMention, true) assert.deepEqual(account.groupAllowFrom, ['spaces/AAA']) assert.equal(account.dangerouslyAllowNameMatching, true) assert.equal(account.mediaMaxMb, 20) assert.equal(account.responsePrefix, '[AI]') assert.equal(cfg.plugins.entries.googlechat.enabled, true) }) test('Google Chat 读取会把嵌套 DM 策略回显为表单字段', () => { const values = buildMessagingPlatformFormValues('googlechat', { serviceAccountFile: '/run/secrets/googlechat.json', audienceType: 'project-number', audience: '1234567890', dm: { policy: 'allowlist', allowFrom: ['users/123', 'name@example.com'] }, groupPolicy: 'allowlist', groupAllowFrom: ['spaces/AAA'], requireMention: true, dangerouslyAllowNameMatching: true, mediaMaxMb: 20, }) assert.equal(values.serviceAccountFile, '/run/secrets/googlechat.json') assert.equal(values.audienceType, 'project-number') assert.equal(values.audience, '1234567890') assert.equal(values.dmPolicy, 'allowlist') assert.equal(values.allowFrom, 'users/123, name@example.com') assert.equal(values.groupPolicy, 'allowlist') assert.equal(values.groupAllowFrom, 'spaces/AAA') assert.equal(values.requireMention, 'true') assert.equal(values.dangerouslyAllowNameMatching, 'true') assert.equal(values.mediaMaxMb, '20') }) test('Google Chat 诊断要求 service account 文件或内联 JSON 其中一项', () => { const missingCredential = buildOpenClawChannelDiagnosis({ platform: 'googlechat', configExists: true, channelEnabled: true, form: { audienceType: 'app-url', audience: 'https://panel.example.com/googlechat' }, }) const ready = buildOpenClawChannelDiagnosis({ platform: 'googlechat', configExists: true, channelEnabled: true, form: { serviceAccountFile: '/run/secrets/googlechat.json' }, }) assert.equal(missingCredential.checks.find(item => item.id === 'credentials')?.ok, false) assert.match(missingCredential.checks.find(item => item.id === 'credentials')?.detail || '', /Service Account/) assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true) }) test('Microsoft Teams 渠道保存会写入新版认证和运行字段', () => { const cfg = { channels: {} } mergeOpenClawMessagingPlatformConfig(cfg, { platform: 'msteams', form: { appId: 'teams-app-id', appPassword: 'teams-secret', tenantId: 'tenant-1', authType: 'federated', certificatePath: '/run/secrets/teams.pem', certificateThumbprint: 'thumbprint-1', useManagedIdentity: 'true', managedIdentityClientId: 'identity-client-id', webhookPort: '3978', webhookPath: '/api/teams/messages', dmPolicy: 'allowlist', allowFrom: 'user-a, user-b', groupPolicy: 'mentioned', groupAllowFrom: 'team-1, team-2', textChunkLimit: '1800', historyLimit: '80', dmHistoryLimit: '20', mediaMaxMb: '100', blockStreaming: 'true', typingIndicator: 'true', replyStyle: 'thread', sharePointSiteId: 'contoso.sharepoint.com,guid1,guid2', responsePrefix: '[Teams]', welcomeCard: 'true', promptStarters: 'help, status', delegatedAuthEnabled: 'true', delegatedAuthScopes: 'User.Read, offline_access', ssoEnabled: 'true', ssoConnectionName: 'teams-oauth', }, }) const entry = cfg.channels.msteams assert.equal(entry.appId, 'teams-app-id') assert.equal(entry.appPassword, 'teams-secret') assert.equal(entry.tenantId, 'tenant-1') assert.equal(entry.authType, 'federated') assert.equal(entry.certificatePath, '/run/secrets/teams.pem') assert.equal(entry.certificateThumbprint, 'thumbprint-1') assert.equal(entry.useManagedIdentity, true) assert.equal(entry.managedIdentityClientId, 'identity-client-id') assert.deepEqual(entry.webhook, { port: 3978, path: '/api/teams/messages' }) assert.equal(entry.dmPolicy, 'allowlist') assert.deepEqual(entry.allowFrom, ['user-a', 'user-b']) assert.equal(entry.groupPolicy, 'open') assert.equal(entry.requireMention, true) assert.deepEqual(entry.groupAllowFrom, ['team-1', 'team-2']) assert.equal(entry.textChunkLimit, 1800) assert.equal(entry.historyLimit, 80) assert.equal(entry.dmHistoryLimit, 20) assert.equal(entry.mediaMaxMb, 100) assert.equal(entry.blockStreaming, true) assert.equal(entry.typingIndicator, true) assert.equal(entry.replyStyle, 'thread') assert.equal(entry.sharePointSiteId, 'contoso.sharepoint.com,guid1,guid2') assert.equal(entry.responsePrefix, '[Teams]') assert.equal(entry.welcomeCard, true) assert.deepEqual(entry.promptStarters, ['help', 'status']) assert.deepEqual(entry.delegatedAuth, { enabled: true, scopes: ['User.Read', 'offline_access'] }) assert.deepEqual(entry.sso, { enabled: true, connectionName: 'teams-oauth' }) assert.equal(cfg.plugins.entries.msteams.enabled, true) }) test('Microsoft Teams 渠道读取会回显嵌套 webhook 和运行字段', () => { const values = buildMessagingPlatformFormValues('msteams', { appId: 'teams-app-id', appPassword: 'teams-secret', authType: 'federated', webhook: { port: 3978, path: '/api/teams/messages' }, dmPolicy: 'allowlist', allowFrom: ['user-a', 'user-b'], groupPolicy: 'open', requireMention: true, groupAllowFrom: ['team-1', 'team-2'], textChunkLimit: 1800, mediaMaxMb: 100, blockStreaming: true, typingIndicator: true, welcomeCard: true, promptStarters: ['help', 'status'], delegatedAuth: { enabled: true, scopes: ['User.Read', 'offline_access'] }, sso: { enabled: true, connectionName: 'teams-oauth' }, }) assert.equal(values.appId, 'teams-app-id') assert.equal(values.appPassword, 'teams-secret') assert.equal(values.authType, 'federated') assert.equal(values.webhookPort, '3978') assert.equal(values.webhookPath, '/api/teams/messages') assert.equal(values.groupPolicy, 'mentioned') assert.equal(values.groupAllowFrom, 'team-1, team-2') assert.equal(values.textChunkLimit, '1800') assert.equal(values.mediaMaxMb, '100') assert.equal(values.blockStreaming, 'true') assert.equal(values.typingIndicator, 'true') assert.equal(values.welcomeCard, 'true') assert.equal(values.promptStarters, 'help, status') assert.equal(values.delegatedAuthEnabled, 'true') assert.equal(values.delegatedAuthScopes, 'User.Read, offline_access') assert.equal(values.ssoEnabled, 'true') assert.equal(values.ssoConnectionName, 'teams-oauth') }) test('Microsoft Teams 诊断会按认证模式检查动态必填凭证', () => { const missingSecret = buildOpenClawChannelDiagnosis({ platform: 'msteams', configExists: true, channelEnabled: true, form: { appId: 'teams-app-id', authType: 'secret', }, }) const federatedCert = buildOpenClawChannelDiagnosis({ platform: 'msteams', configExists: true, channelEnabled: true, form: { appId: 'teams-app-id', authType: 'federated', certificatePath: '/run/secrets/teams.pem', }, }) const managedIdentity = buildOpenClawChannelDiagnosis({ platform: 'msteams', configExists: true, channelEnabled: true, form: { appId: 'teams-app-id', useManagedIdentity: 'true', }, }) assert.equal(missingSecret.checks.find(item => item.id === 'credentials')?.ok, false) assert.match(missingSecret.checks.find(item => item.id === 'credentials')?.detail || '', /App Password/) assert.equal(federatedCert.checks.find(item => item.id === 'credentials')?.ok, true) assert.equal(managedIdentity.checks.find(item => item.id === 'credentials')?.ok, true) }) test('iMessage 渠道保存会写入桥接运行字段并启用插件', () => { const cfg = { channels: {} } mergeOpenClawMessagingPlatformConfig(cfg, { platform: 'imessage', form: { cliPath: '/usr/local/bin/imsg', dbPath: '~/Library/Messages/chat.db', remoteHost: 'mac-mini.local', service: 'auto', region: 'US', dmPolicy: 'allowlist', allowFrom: '+15551234567, +15557654321', defaultTo: '+15550001111', groupPolicy: 'allowlist', groupAllowFrom: 'chat-guid-1, chat-guid-2', historyLimit: '80', dmHistoryLimit: '20', mediaMaxMb: '25', probeTimeoutMs: '5000', textChunkLimit: '1800', includeAttachments: 'true', attachmentRoots: '/Users/me/Downloads, /tmp/imessage', remoteAttachmentRoots: '/mnt/messages', sendReadReceipts: 'false', coalesceSameSenderDms: 'true', reactionNotifications: 'own', responsePrefix: '[iMessage]', }, }) const entry = cfg.channels.imessage assert.equal(entry.cliPath, '/usr/local/bin/imsg') assert.equal(entry.dbPath, '~/Library/Messages/chat.db') assert.equal(entry.remoteHost, 'mac-mini.local') assert.equal(entry.service, 'auto') assert.equal(entry.region, 'US') assert.equal(entry.dmPolicy, 'allowlist') assert.deepEqual(entry.allowFrom, ['+15551234567', '+15557654321']) assert.equal(entry.defaultTo, '+15550001111') assert.equal(entry.groupPolicy, 'allowlist') assert.deepEqual(entry.groupAllowFrom, ['chat-guid-1', 'chat-guid-2']) assert.equal(entry.historyLimit, 80) assert.equal(entry.dmHistoryLimit, 20) assert.equal(entry.mediaMaxMb, 25) assert.equal(entry.probeTimeoutMs, 5000) assert.equal(entry.textChunkLimit, 1800) assert.equal(entry.includeAttachments, true) assert.deepEqual(entry.attachmentRoots, ['/Users/me/Downloads', '/tmp/imessage']) assert.deepEqual(entry.remoteAttachmentRoots, ['/mnt/messages']) assert.equal(entry.sendReadReceipts, false) assert.equal(entry.coalesceSameSenderDms, true) assert.equal(entry.reactionNotifications, 'own') assert.equal(entry.responsePrefix, '[iMessage]') assert.equal(cfg.plugins.entries.imessage.enabled, true) }) test('iMessage 读取会回显桥接字段且诊断不要求 Bot Token', () => { const values = buildMessagingPlatformFormValues('imessage', { cliPath: '/usr/local/bin/imsg', dbPath: '~/Library/Messages/chat.db', remoteHost: 'mac-mini.local', service: 'sms', dmPolicy: 'open', allowFrom: ['*'], groupPolicy: 'allowlist', groupAllowFrom: ['chat-guid-1'], includeAttachments: true, attachmentRoots: ['/Users/me/Downloads'], sendReadReceipts: false, historyLimit: 50, responsePrefix: '[iMessage]', }) const diagnosis = buildOpenClawChannelDiagnosis({ platform: 'imessage', configExists: true, channelEnabled: true, form: values, }) assert.equal(values.cliPath, '/usr/local/bin/imsg') assert.equal(values.service, 'sms') assert.equal(values.dmPolicy, 'open') assert.equal(values.allowFrom, '*') assert.equal(values.groupAllowFrom, 'chat-guid-1') assert.equal(values.includeAttachments, 'true') assert.equal(values.attachmentRoots, '/Users/me/Downloads') assert.equal(values.sendReadReceipts, 'false') assert.equal(values.historyLimit, '50') assert.equal(values.responsePrefix, '[iMessage]') assert.equal(diagnosis.checks.find(item => item.id === 'credentials')?.ok, true) assert.match(diagnosis.checks.find(item => item.id === 'credentials')?.title || '', /桥接/) }) test('Discord 渠道保存会保留运行时需要的 applicationId', () => { const cfg = { channels: {} } mergeOpenClawMessagingPlatformConfig(cfg, { platform: 'discord', form: { token: 'discord-token', applicationId: '123456789012345678', }, }) assert.equal(cfg.channels.discord.token, 'discord-token') assert.equal(cfg.channels.discord.applicationId, '123456789012345678') }) 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') })