/**
* 通信设置页面 — 消息、广播、命令、音频等 openclaw.json 配置的可视化编辑器
* 对应上游 Dashboard 的「通信」+「自动化」合并页
*/
import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { icon } from '../lib/icons.js'
import { t } from '../lib/i18n.js'
import { wsClient } from '../lib/ws-client.js'
let _page = null, _config = null, _dirty = false
export async function render() {
const page = document.createElement('div')
page.className = 'page'
_page = page
page.innerHTML = `
`
// Tab 切换
page.querySelectorAll('.comm-tab').forEach(tab => {
tab.onclick = () => {
page.querySelectorAll('.comm-tab').forEach(t => { t.classList.remove('active', 'btn-primary'); t.classList.add('btn-secondary') })
tab.classList.remove('btn-secondary'); tab.classList.add('active', 'btn-primary')
renderTab(page, tab.dataset.tab)
}
})
// 保存按钮
page.querySelector('#btn-comm-save').onclick = saveConfig
await loadConfig(page)
return page
}
export function cleanup() { _page = null; _config = null; _dirty = false }
async function loadConfig(page) {
try {
_config = await api.readOpenclawConfig()
if (!_config) _config = {}
renderTab(page, 'messages')
} catch (e) {
page.querySelector('#comm-content').innerHTML = `${t('communication.loadFailed')}: ${esc(e?.message || e)}
`
}
}
function markDirty() {
_dirty = true
const btn = _page?.querySelector('#btn-comm-save')
if (btn) btn.disabled = false
}
async function saveConfig() {
if (!_config || !_dirty) return
const btn = _page?.querySelector('#btn-comm-save')
if (btn) { btn.disabled = true; btn.textContent = t('communication.saving') }
try {
// 从当前表单收集值到 _config
collectCurrentTab()
await api.writeOpenclawConfig(_config)
_dirty = false
toast(t('communication.configSaved'), 'info')
try { await api.reloadGateway(); toast(t('communication.gwReloaded'), 'success') } catch {}
} catch (e) {
toast(t('communication.saveFailed') + ': ' + e, 'error')
} finally {
if (btn) { btn.disabled = !_dirty; btn.innerHTML = `${icon('save', 14)} ${t('communication.save')}` }
}
}
function collectCurrentTab() {
if (!_page) return
const activeTab = _page.querySelector('.comm-tab.active')?.dataset.tab
if (activeTab === 'messages') collectMessages()
else if (activeTab === 'broadcast') collectBroadcast()
else if (activeTab === 'commands') collectCommands()
else if (activeTab === 'hooks') collectHooks()
else if (activeTab === 'approvals') collectApprovals()
}
// ── Tab 渲染 ──
function renderTab(page, tab) {
const el = page.querySelector('#comm-content')
if (tab === 'messages') renderMessages(el)
else if (tab === 'broadcast') renderBroadcast(el)
else if (tab === 'commands') renderCommands(el)
else if (tab === 'hooks') renderHooks(el)
else if (tab === 'approvals') renderApprovals(el)
}
// ── 消息设置 ──
function renderMessages(el) {
const m = _config?.messages || {}
const sr = m.statusReactions || {}
el.innerHTML = `
${t('communication.replySettings')}
${t('communication.statusReactions')}
${t('communication.messageQueue')}
${t('communication.groupChat')}
`
el.querySelectorAll('input, select').forEach(inp => {
inp.addEventListener('change', markDirty)
inp.addEventListener('input', markDirty)
})
}
function collectMessages() {
if (!_config) return
const g = (id) => _page?.querySelector('#' + id)
const v = (id) => g(id)?.value?.trim() || undefined
const n = (id) => { const x = parseInt(g(id)?.value); return isNaN(x) ? undefined : x }
const c = (id) => g(id)?.checked || false
if (!_config.messages) _config.messages = {}
const m = _config.messages
m.responsePrefix = v('msg-responsePrefix')
m.ackReaction = v('msg-ackReaction')
m.ackReactionScope = v('msg-ackReactionScope') || undefined
m.removeAckAfterReply = c('msg-removeAckAfterReply') || undefined
m.suppressToolErrors = c('msg-suppressToolErrors') || undefined
if (!m.statusReactions) m.statusReactions = {}
m.statusReactions.enabled = c('msg-sr-enabled') || undefined
const debounceMs = n('msg-debounceMs')
if (debounceMs != null) {
if (!m.inbound) m.inbound = {}
m.inbound.debounceMs = debounceMs
}
const cap = n('msg-queueCap')
if (cap != null) {
if (!m.queue) m.queue = {}
m.queue.cap = cap
}
const groupHistoryLimit = n('msg-groupHistoryLimit')
if (groupHistoryLimit != null) {
if (!m.groupChat) m.groupChat = {}
m.groupChat.historyLimit = groupHistoryLimit
}
}
// ── 广播设置 ──
function renderBroadcast(el) {
const b = _config?.broadcast || {}
el.innerHTML = `
${t('communication.broadcastStrategy')}
`
el.querySelectorAll('input, select').forEach(inp => {
inp.addEventListener('change', markDirty)
})
}
function collectBroadcast() {
if (!_config) return
const strategy = _page?.querySelector('#bc-strategy')?.value
if (strategy) {
if (!_config.broadcast) _config.broadcast = {}
_config.broadcast.strategy = strategy
}
}
// ── 命令配置 ──
function renderCommands(el) {
const cmd = _config?.commands || {}
el.innerHTML = `
${t('communication.slashCommands')}
${toggleRow('cmd-text', t('communication.cmdText'), t('communication.cmdTextHint'), cmd.text !== false)}
${toggleRow('cmd-bash', t('communication.cmdBash'), t('communication.cmdBashHint'), !!cmd.bash)}
${toggleRow('cmd-config', t('communication.cmdConfig'), t('communication.cmdConfigHint'), !!cmd.config)}
${toggleRow('cmd-debug', t('communication.cmdDebug'), t('communication.cmdDebugHint'), !!cmd.debug)}
${toggleRow('cmd-restart', t('communication.cmdRestart'), t('communication.cmdRestartHint'), cmd.restart !== false)}
${t('communication.nativeCommands')}
`
el.querySelectorAll('input, select').forEach(inp => {
inp.addEventListener('change', markDirty)
})
}
function collectCommands() {
if (!_config) return
const c = (id) => _page?.querySelector('#' + id)?.checked
if (!_config.commands) _config.commands = {}
const cmd = _config.commands
cmd.text = c('cmd-text') === false ? false : undefined
cmd.bash = c('cmd-bash') || undefined
cmd.config = c('cmd-config') || undefined
cmd.debug = c('cmd-debug') || undefined
cmd.restart = c('cmd-restart') === false ? false : undefined
const native = _page?.querySelector('#cmd-native')?.value
cmd.native = native === 'true' ? true : native === 'false' ? false : 'auto'
}
// ── Webhook ──
function renderHooks(el) {
const h = _config?.hooks || {}
el.innerHTML = `
${t('communication.webhookSettings')}
${toggleRow('hooks-enabled', t('communication.webhookEnabled'), t('communication.webhookEnabledHint'), !!h.enabled)}
`
el.querySelectorAll('input, select').forEach(inp => {
inp.addEventListener('change', markDirty)
inp.addEventListener('input', markDirty)
})
}
function collectHooks() {
if (!_config) return
const v = (id) => _page?.querySelector('#' + id)?.value?.trim() || undefined
const n = (id) => { const x = parseInt(_page?.querySelector('#' + id)?.value); return isNaN(x) ? undefined : x }
const c = (id) => _page?.querySelector('#' + id)?.checked
if (!_config.hooks) _config.hooks = {}
const h = _config.hooks
h.enabled = c('hooks-enabled') || undefined
h.path = v('hooks-path')
h.token = v('hooks-token')
h.defaultSessionKey = v('hooks-defaultSessionKey')
h.maxBodyBytes = n('hooks-maxBodyBytes')
}
// ── 执行审批 ──
function renderApprovals(el) {
const a = _config?.approvals?.exec || {}
el.innerHTML = `
${t('communication.approvalsTitle')}
${t('communication.approvalsDesc')}
${toggleRow('approvals-enabled', t('communication.approvalsEnabled'), t('communication.approvalsEnabledHint'), !!a.enabled)}
${toggleRow('approvals-forwardExec', t('communication.approvalsForwardExec'), t('communication.approvalsForwardExecHint'), !!a.enabled)}
${t('communication.pendingApprovals')}
${t('communication.approvalsLoadingQueue')}
`
el.querySelectorAll('input, select').forEach(inp => {
inp.addEventListener('change', markDirty)
})
el.querySelector('#btn-refresh-approvals')?.addEventListener('click', () => loadApprovalQueue(el))
loadApprovalQueue(el)
}
async function loadApprovalQueue(el) {
const container = (el || _page)?.querySelector('#approval-queue')
if (!container) return
if (!wsClient.connected || !wsClient.gatewayReady) {
container.innerHTML = `${esc(t('communication.approvalsGwNotReady'))}
`
return
}
container.innerHTML = `${esc(t('communication.approvalsLoadingQueue'))}
`
let execItems = [], pluginItems = [], unsupported = false
try {
const [execRes, pluginRes] = await Promise.allSettled([
wsClient.execApprovalList(),
wsClient.pluginApprovalList(),
])
if (execRes.status === 'fulfilled') execItems = execRes.value?.approvals || execRes.value?.items || []
else {
const msg = String(execRes.reason?.message || '').toLowerCase()
if (msg.includes('unknown method') || msg.includes('not found')) unsupported = true
}
if (pluginRes.status === 'fulfilled') pluginItems = pluginRes.value?.approvals || pluginRes.value?.items || []
} catch {}
if (unsupported) {
container.innerHTML = `${esc(t('communication.approvalsUnsupported'))}
`
return
}
const allItems = [
...execItems.map(i => ({ ...i, _type: 'exec' })),
...pluginItems.map(i => ({ ...i, _type: 'plugin' })),
]
if (!allItems.length) {
container.innerHTML = `${esc(t('communication.approvalsQueueEmpty'))}
`
return
}
container.innerHTML = allItems.map(item => {
const id = item.id || item.approvalId || ''
const type = item._type === 'plugin' ? 'Plugin' : 'Exec'
const cmd = item.command || item.description || item.name || id
const status = item.status || 'pending'
const ts = item.createdAt || item.timestamp || 0
const timeStr = ts ? new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts).toLocaleString() : ''
return `
${esc(type)}${esc(cmd)}
${esc(status)}${timeStr ? ' · ' + timeStr : ''}
`
}).join('')
}
function collectApprovals() {
if (!_config) return
const c = (id) => _page?.querySelector('#' + id)?.checked
const v = (id) => _page?.querySelector('#' + id)?.value
if (!_config.approvals) _config.approvals = {}
if (!_config.approvals.exec) _config.approvals.exec = {}
const a = _config.approvals.exec
a.enabled = c('approvals-enabled') || undefined
a.mode = v('approvals-mode') || undefined
}
// ── 工具函数 ──
function toggleRow(id, label, hint, checked) {
return `
`
}
function esc(str) {
return (str || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"')
}