mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
1893 lines
66 KiB
JavaScript
1893 lines
66 KiB
JavaScript
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('Nostr 渠道保存会写入上游根节点配置并启用插件', () => {
|
|
const cfg = { channels: {} }
|
|
|
|
mergeOpenClawMessagingPlatformConfig(cfg, {
|
|
platform: 'nostr',
|
|
form: {
|
|
enabled: 'true',
|
|
name: 'nostr-bot',
|
|
defaultAccount: 'default',
|
|
privateKey: 'nsec1example',
|
|
relays: 'wss://relay.damus.io, wss://nos.lol',
|
|
dmPolicy: 'allowlist',
|
|
allowFrom: 'npub1sender, 0123456789abcdef',
|
|
profileName: 'openclaw',
|
|
profileDisplayName: 'OpenClaw Bot',
|
|
profileAbout: 'Nostr DM assistant',
|
|
profilePicture: 'https://example.com/avatar.png',
|
|
profileWebsite: 'https://example.com',
|
|
profileNip05: 'openclaw@example.com',
|
|
profileLud16: 'openclaw@example.com',
|
|
},
|
|
})
|
|
|
|
const root = cfg.channels.nostr
|
|
assert.equal(root.enabled, true)
|
|
assert.equal(root.name, 'nostr-bot')
|
|
assert.equal(root.defaultAccount, 'default')
|
|
assert.equal(root.privateKey, 'nsec1example')
|
|
assert.deepEqual(root.relays, ['wss://relay.damus.io', 'wss://nos.lol'])
|
|
assert.equal(root.dmPolicy, 'allowlist')
|
|
assert.deepEqual(root.allowFrom, ['npub1sender', '0123456789abcdef'])
|
|
assert.equal(root.profile.name, 'openclaw')
|
|
assert.equal(root.profile.displayName, 'OpenClaw Bot')
|
|
assert.equal(root.profile.about, 'Nostr DM assistant')
|
|
assert.equal(root.profile.picture, 'https://example.com/avatar.png')
|
|
assert.equal(root.profile.website, 'https://example.com')
|
|
assert.equal(root.profile.nip05, 'openclaw@example.com')
|
|
assert.equal(root.profile.lud16, 'openclaw@example.com')
|
|
assert.equal(Object.hasOwn(root, 'accounts'), false)
|
|
assert.equal(cfg.plugins.entries.nostr.enabled, true)
|
|
})
|
|
|
|
test('Nostr 读取和诊断会回显 relay、访问控制和 profile 字段', () => {
|
|
const values = buildMessagingPlatformFormValues('nostr', {
|
|
enabled: true,
|
|
name: 'nostr-bot',
|
|
privateKey: 'nsec1example',
|
|
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
|
dmPolicy: 'allowlist',
|
|
allowFrom: ['npub1sender'],
|
|
profile: {
|
|
name: 'openclaw',
|
|
displayName: 'OpenClaw Bot',
|
|
about: 'Nostr DM assistant',
|
|
picture: 'https://example.com/avatar.png',
|
|
website: 'https://example.com',
|
|
nip05: 'openclaw@example.com',
|
|
lud16: 'openclaw@example.com',
|
|
},
|
|
})
|
|
const missingKey = buildOpenClawChannelDiagnosis({
|
|
platform: 'nostr',
|
|
configExists: true,
|
|
channelEnabled: true,
|
|
form: { relays: 'wss://relay.damus.io' },
|
|
})
|
|
const ready = buildOpenClawChannelDiagnosis({
|
|
platform: 'nostr',
|
|
configExists: true,
|
|
channelEnabled: true,
|
|
form: values,
|
|
})
|
|
|
|
assert.equal(values.enabled, 'true')
|
|
assert.equal(values.name, 'nostr-bot')
|
|
assert.equal(values.privateKey, 'nsec1example')
|
|
assert.equal(values.relays, 'wss://relay.damus.io, wss://nos.lol')
|
|
assert.equal(values.dmPolicy, 'allowlist')
|
|
assert.equal(values.allowFrom, 'npub1sender')
|
|
assert.equal(values.profileName, 'openclaw')
|
|
assert.equal(values.profileDisplayName, 'OpenClaw Bot')
|
|
assert.equal(values.profileAbout, 'Nostr DM assistant')
|
|
assert.equal(values.profilePicture, 'https://example.com/avatar.png')
|
|
assert.equal(values.profileWebsite, 'https://example.com')
|
|
assert.equal(values.profileNip05, 'openclaw@example.com')
|
|
assert.equal(values.profileLud16, 'openclaw@example.com')
|
|
assert.equal(missingKey.checks.find(item => item.id === 'credentials')?.ok, false)
|
|
assert.match(missingKey.checks.find(item => item.id === 'credentials')?.detail || '', /Private Key/)
|
|
assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true)
|
|
})
|
|
|
|
test('IRC 渠道保存会写入多账号服务器、NickServ 和频道访问结构并启用插件', () => {
|
|
const cfg = { channels: {} }
|
|
|
|
mergeOpenClawMessagingPlatformConfig(cfg, {
|
|
platform: 'irc',
|
|
accountId: 'libera',
|
|
form: {
|
|
enabled: 'true',
|
|
name: 'Libera Ops',
|
|
host: 'irc.libera.chat',
|
|
port: '6697',
|
|
tls: 'true',
|
|
nick: 'openclaw-bot',
|
|
username: 'openclaw',
|
|
realname: 'OpenClaw Bot',
|
|
password: 'server-password',
|
|
passwordFile: '/run/secrets/irc-password',
|
|
nickservEnabled: 'true',
|
|
nickservService: 'NickServ',
|
|
nickservPassword: 'nickserv-password',
|
|
nickservPasswordFile: '/run/secrets/irc-nickserv',
|
|
nickservRegister: 'false',
|
|
nickservRegisterEmail: 'bot@example.com',
|
|
channels: '#openclaw, #ops',
|
|
dmPolicy: 'allowlist',
|
|
allowFrom: 'alice!ident@example.org, bob',
|
|
defaultTo: '#openclaw',
|
|
groupPolicy: 'allowlist',
|
|
groups: '#openclaw, #ops',
|
|
groupAllowFrom: 'alice!ident@example.org',
|
|
requireMention: 'false',
|
|
mentionPatterns: 'openclaw:, @openclaw',
|
|
historyLimit: '80',
|
|
dmHistoryLimit: '20',
|
|
mediaMaxMb: '25',
|
|
textChunkLimit: '350',
|
|
chunkMode: 'newline',
|
|
blockStreaming: 'true',
|
|
responsePrefix: '[IRC]',
|
|
dangerouslyAllowNameMatching: 'true',
|
|
},
|
|
})
|
|
|
|
const root = cfg.channels.irc
|
|
const account = root.accounts.libera
|
|
assert.equal(root.defaultAccount, 'libera')
|
|
assert.equal(account.enabled, true)
|
|
assert.equal(account.name, 'Libera Ops')
|
|
assert.equal(account.host, 'irc.libera.chat')
|
|
assert.equal(account.port, 6697)
|
|
assert.equal(account.tls, true)
|
|
assert.equal(account.nick, 'openclaw-bot')
|
|
assert.equal(account.username, 'openclaw')
|
|
assert.equal(account.realname, 'OpenClaw Bot')
|
|
assert.equal(account.password, 'server-password')
|
|
assert.equal(account.passwordFile, '/run/secrets/irc-password')
|
|
assert.deepEqual(account.nickserv, {
|
|
enabled: true,
|
|
service: 'NickServ',
|
|
password: 'nickserv-password',
|
|
passwordFile: '/run/secrets/irc-nickserv',
|
|
register: false,
|
|
registerEmail: 'bot@example.com',
|
|
})
|
|
assert.deepEqual(account.channels, ['#openclaw', '#ops'])
|
|
assert.equal(account.dmPolicy, 'allowlist')
|
|
assert.deepEqual(account.allowFrom, ['alice!ident@example.org', 'bob'])
|
|
assert.equal(account.defaultTo, '#openclaw')
|
|
assert.equal(account.groupPolicy, 'allowlist')
|
|
assert.deepEqual(Object.keys(account.groups), ['#openclaw', '#ops'])
|
|
assert.equal(account.groups['#openclaw'].requireMention, false)
|
|
assert.deepEqual(account.groupAllowFrom, ['alice!ident@example.org'])
|
|
assert.deepEqual(account.mentionPatterns, ['openclaw:', '@openclaw'])
|
|
assert.equal(account.historyLimit, 80)
|
|
assert.equal(account.dmHistoryLimit, 20)
|
|
assert.equal(account.mediaMaxMb, 25)
|
|
assert.equal(account.textChunkLimit, 350)
|
|
assert.equal(account.chunkMode, 'newline')
|
|
assert.equal(account.blockStreaming, true)
|
|
assert.equal(account.responsePrefix, '[IRC]')
|
|
assert.equal(account.dangerouslyAllowNameMatching, true)
|
|
assert.equal(cfg.plugins.entries.irc.enabled, true)
|
|
})
|
|
|
|
test('IRC 读取和诊断会回显服务器、NickServ 与频道字段', () => {
|
|
const values = buildMessagingPlatformFormValues('irc', {
|
|
enabled: true,
|
|
name: 'Libera Ops',
|
|
host: 'irc.libera.chat',
|
|
port: 6697,
|
|
tls: true,
|
|
nick: 'openclaw-bot',
|
|
username: 'openclaw',
|
|
realname: 'OpenClaw Bot',
|
|
password: 'server-password',
|
|
passwordFile: '/run/secrets/irc-password',
|
|
nickserv: {
|
|
enabled: true,
|
|
service: 'NickServ',
|
|
password: 'nickserv-password',
|
|
passwordFile: '/run/secrets/irc-nickserv',
|
|
register: false,
|
|
registerEmail: 'bot@example.com',
|
|
},
|
|
channels: ['#openclaw', '#ops'],
|
|
dmPolicy: 'allowlist',
|
|
allowFrom: ['alice!ident@example.org'],
|
|
defaultTo: '#openclaw',
|
|
groupPolicy: 'allowlist',
|
|
groups: {
|
|
'#openclaw': { requireMention: false },
|
|
'#ops': { requireMention: false },
|
|
},
|
|
groupAllowFrom: ['alice!ident@example.org'],
|
|
mentionPatterns: ['openclaw:', '@openclaw'],
|
|
historyLimit: 80,
|
|
dmHistoryLimit: 20,
|
|
mediaMaxMb: 25,
|
|
textChunkLimit: 350,
|
|
chunkMode: 'newline',
|
|
blockStreaming: true,
|
|
responsePrefix: '[IRC]',
|
|
dangerouslyAllowNameMatching: true,
|
|
})
|
|
const missingNick = buildOpenClawChannelDiagnosis({
|
|
platform: 'irc',
|
|
configExists: true,
|
|
channelEnabled: true,
|
|
form: { host: 'irc.libera.chat' },
|
|
})
|
|
const ready = buildOpenClawChannelDiagnosis({
|
|
platform: 'irc',
|
|
configExists: true,
|
|
channelEnabled: true,
|
|
form: values,
|
|
})
|
|
|
|
assert.equal(values.enabled, 'true')
|
|
assert.equal(values.name, 'Libera Ops')
|
|
assert.equal(values.host, 'irc.libera.chat')
|
|
assert.equal(values.port, '6697')
|
|
assert.equal(values.tls, 'true')
|
|
assert.equal(values.nick, 'openclaw-bot')
|
|
assert.equal(values.nickservEnabled, 'true')
|
|
assert.equal(values.nickservService, 'NickServ')
|
|
assert.equal(values.nickservPassword, 'nickserv-password')
|
|
assert.equal(values.nickservRegister, 'false')
|
|
assert.equal(values.channels, '#openclaw, #ops')
|
|
assert.equal(values.groups, '#openclaw, #ops')
|
|
assert.equal(values.requireMention, 'false')
|
|
assert.equal(values.groupAllowFrom, 'alice!ident@example.org')
|
|
assert.equal(values.mentionPatterns, 'openclaw:, @openclaw')
|
|
assert.equal(values.blockStreaming, 'true')
|
|
assert.equal(values.dangerouslyAllowNameMatching, 'true')
|
|
assert.equal(missingNick.checks.find(item => item.id === 'credentials')?.ok, false)
|
|
assert.match(missingNick.checks.find(item => item.id === 'credentials')?.detail || '', /Nick/)
|
|
assert.equal(ready.checks.find(item => item.id === 'credentials')?.ok, true)
|
|
})
|
|
|
|
test('Tlon 默认账号保存会写入上游根节点配置并启用插件', () => {
|
|
const cfg = { channels: {} }
|
|
|
|
mergeOpenClawMessagingPlatformConfig(cfg, {
|
|
platform: 'tlon',
|
|
accountId: 'default',
|
|
form: {
|
|
enabled: 'true',
|
|
name: 'Main Ship',
|
|
ship: '~sampel-palnet',
|
|
url: 'https://urbit.example.com',
|
|
code: 'lidlut-tabwed-pillex-ridrup',
|
|
dangerouslyAllowPrivateNetwork: 'true',
|
|
groupChannels: 'chat/~host-ship/general, chat/~host-ship/support',
|
|
dmAllowlist: 'zod, ~nec',
|
|
groupInviteAllowlist: '~bus',
|
|
autoDiscoverChannels: 'true',
|
|
showModelSignature: 'false',
|
|
responsePrefix: '[Tlon]',
|
|
autoAcceptDmInvites: 'true',
|
|
autoAcceptGroupInvites: 'false',
|
|
ownerShip: '~sampel-palnet',
|
|
defaultAuthorizedShips: '~zod, ~nec',
|
|
},
|
|
})
|
|
|
|
const root = cfg.channels.tlon
|
|
assert.equal(root.enabled, true)
|
|
assert.equal(root.name, 'Main Ship')
|
|
assert.equal(root.ship, '~sampel-palnet')
|
|
assert.equal(root.url, 'https://urbit.example.com')
|
|
assert.equal(root.code, 'lidlut-tabwed-pillex-ridrup')
|
|
assert.deepEqual(root.network, { dangerouslyAllowPrivateNetwork: true })
|
|
assert.deepEqual(root.groupChannels, ['chat/~host-ship/general', 'chat/~host-ship/support'])
|
|
assert.deepEqual(root.dmAllowlist, ['zod', '~nec'])
|
|
assert.deepEqual(root.groupInviteAllowlist, ['~bus'])
|
|
assert.equal(root.autoDiscoverChannels, true)
|
|
assert.equal(root.showModelSignature, false)
|
|
assert.equal(root.responsePrefix, '[Tlon]')
|
|
assert.equal(root.autoAcceptDmInvites, true)
|
|
assert.equal(root.autoAcceptGroupInvites, false)
|
|
assert.equal(root.ownerShip, '~sampel-palnet')
|
|
assert.deepEqual(root.defaultAuthorizedShips, ['~zod', '~nec'])
|
|
assert.equal(Object.hasOwn(root, 'accounts'), false)
|
|
assert.equal(cfg.plugins.entries.tlon.enabled, true)
|
|
})
|
|
|
|
test('Tlon 命名账号保存会写入 accounts 并保留根节点共享字段', () => {
|
|
const cfg = {
|
|
channels: {
|
|
tlon: {
|
|
enabled: true,
|
|
defaultAuthorizedShips: ['~zod'],
|
|
},
|
|
},
|
|
}
|
|
|
|
mergeOpenClawMessagingPlatformConfig(cfg, {
|
|
platform: 'tlon',
|
|
accountId: 'support',
|
|
form: {
|
|
enabled: 'true',
|
|
ship: '~support-palnet',
|
|
url: 'https://support.example.com',
|
|
code: 'fodwyt-ragful-sivnys-nivlup',
|
|
groupChannels: 'chat/~host-ship/support',
|
|
dmAllowlist: '~zod',
|
|
autoDiscoverChannels: 'false',
|
|
ownerShip: '~support-palnet',
|
|
},
|
|
})
|
|
|
|
const root = cfg.channels.tlon
|
|
const account = root.accounts.support
|
|
assert.deepEqual(root.defaultAuthorizedShips, ['~zod'])
|
|
assert.equal(account.enabled, true)
|
|
assert.equal(account.ship, '~support-palnet')
|
|
assert.equal(account.url, 'https://support.example.com')
|
|
assert.equal(account.code, 'fodwyt-ragful-sivnys-nivlup')
|
|
assert.deepEqual(account.groupChannels, ['chat/~host-ship/support'])
|
|
assert.deepEqual(account.dmAllowlist, ['~zod'])
|
|
assert.equal(account.autoDiscoverChannels, false)
|
|
assert.equal(account.ownerShip, '~support-palnet')
|
|
assert.equal(cfg.plugins.entries.tlon.enabled, true)
|
|
})
|
|
|
|
test('Tlon 读取和诊断会回显 Ship、URL、登录码和安全配置', () => {
|
|
const values = buildMessagingPlatformFormValues('tlon', {
|
|
enabled: true,
|
|
name: 'Main Ship',
|
|
ship: '~sampel-palnet',
|
|
url: 'https://urbit.example.com',
|
|
code: 'lidlut-tabwed-pillex-ridrup',
|
|
network: { dangerouslyAllowPrivateNetwork: true },
|
|
groupChannels: ['chat/~host-ship/general', 'chat/~host-ship/support'],
|
|
dmAllowlist: ['~zod', '~nec'],
|
|
groupInviteAllowlist: ['~bus'],
|
|
autoDiscoverChannels: true,
|
|
showModelSignature: false,
|
|
responsePrefix: '[Tlon]',
|
|
autoAcceptDmInvites: true,
|
|
autoAcceptGroupInvites: false,
|
|
ownerShip: '~sampel-palnet',
|
|
defaultAuthorizedShips: ['~zod', '~nec'],
|
|
})
|
|
const missingCode = buildOpenClawChannelDiagnosis({
|
|
platform: 'tlon',
|
|
configExists: true,
|
|
channelEnabled: true,
|
|
form: { ship: '~sampel-palnet', url: 'https://urbit.example.com' },
|
|
})
|
|
const ready = buildOpenClawChannelDiagnosis({
|
|
platform: 'tlon',
|
|
configExists: true,
|
|
channelEnabled: true,
|
|
form: values,
|
|
})
|
|
|
|
assert.equal(values.enabled, 'true')
|
|
assert.equal(values.name, 'Main Ship')
|
|
assert.equal(values.ship, '~sampel-palnet')
|
|
assert.equal(values.url, 'https://urbit.example.com')
|
|
assert.equal(values.code, 'lidlut-tabwed-pillex-ridrup')
|
|
assert.equal(values.dangerouslyAllowPrivateNetwork, 'true')
|
|
assert.equal(values.groupChannels, 'chat/~host-ship/general, chat/~host-ship/support')
|
|
assert.equal(values.dmAllowlist, '~zod, ~nec')
|
|
assert.equal(values.groupInviteAllowlist, '~bus')
|
|
assert.equal(values.autoDiscoverChannels, 'true')
|
|
assert.equal(values.showModelSignature, 'false')
|
|
assert.equal(values.responsePrefix, '[Tlon]')
|
|
assert.equal(values.autoAcceptDmInvites, 'true')
|
|
assert.equal(values.autoAcceptGroupInvites, 'false')
|
|
assert.equal(values.ownerShip, '~sampel-palnet')
|
|
assert.equal(values.defaultAuthorizedShips, '~zod, ~nec')
|
|
assert.equal(missingCode.checks.find(item => item.id === 'credentials')?.ok, false)
|
|
assert.match(missingCode.checks.find(item => item.id === 'credentials')?.detail || '', /Code/)
|
|
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('渠道账号列表会使用 IRC Nick 作为安全展示标识', () => {
|
|
const accounts = listPlatformAccounts({
|
|
accounts: {
|
|
libera: {
|
|
nick: 'openclaw-bot',
|
|
},
|
|
},
|
|
})
|
|
|
|
assert.deepEqual(accounts, [
|
|
{ accountId: 'libera', appId: 'openclaw-bot' },
|
|
])
|
|
})
|
|
|
|
test('渠道账号列表会使用 Tlon Ship 作为安全展示标识', () => {
|
|
const accounts = listPlatformAccounts({
|
|
accounts: {
|
|
support: {
|
|
ship: '~support-palnet',
|
|
},
|
|
},
|
|
})
|
|
|
|
assert.deepEqual(accounts, [
|
|
{ accountId: 'support', appId: '~support-palnet' },
|
|
])
|
|
})
|
|
|
|
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')
|
|
})
|